Source file src/cmd/compile/internal/arm64/pair_test.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package arm64
     6  
     7  import (
     8  	"cmd/internal/obj"
     9  	"cmd/internal/obj/arm64"
    10  	"cmd/internal/src"
    11  	"testing"
    12  )
    13  
    14  func TestPairSpills(t *testing.T) {
    15  	movdLoad := func(dst, base int16, off int64) *obj.Prog {
    16  		return &obj.Prog{
    17  			As:   arm64.AMOVD,
    18  			From: obj.Addr{Type: obj.TYPE_MEM, Reg: base, Offset: off, Name: obj.NAME_AUTO},
    19  			To:   obj.Addr{Type: obj.TYPE_REG, Reg: dst},
    20  		}
    21  	}
    22  	movdStore := func(src, base int16, off int64) *obj.Prog {
    23  		return &obj.Prog{
    24  			As:   arm64.AMOVD,
    25  			From: obj.Addr{Type: obj.TYPE_REG, Reg: src},
    26  			To:   obj.Addr{Type: obj.TYPE_MEM, Reg: base, Offset: off, Name: obj.NAME_AUTO},
    27  		}
    28  	}
    29  	// param rewrites p's memory operand to NAME_PARAM.
    30  	param := func(p *obj.Prog) *obj.Prog {
    31  		if p.From.Type == obj.TYPE_MEM {
    32  			p.From.Name = obj.NAME_PARAM
    33  		} else {
    34  			p.To.Name = obj.NAME_PARAM
    35  		}
    36  		return p
    37  	}
    38  	chain := func(progs ...*obj.Prog) *obj.Prog {
    39  		for i := 0; i < len(progs)-1; i++ {
    40  			progs[i].Link = progs[i+1]
    41  		}
    42  		return progs[0]
    43  	}
    44  	countAs := func(head *obj.Prog) map[obj.As]int {
    45  		m := map[obj.As]int{}
    46  		for p := head; p != nil; p = p.Link {
    47  			m[p.As]++
    48  		}
    49  		return m
    50  	}
    51  
    52  	// pairWant describes the expected operands of the fused LDP/STP:
    53  	// MOVDs of lo and hi (lo at the lower address off, hi at off+8)
    54  	// against base, with the memory operand's addressing class name.
    55  	type pairWant struct {
    56  		as     obj.As
    57  		base   int16
    58  		off    int64
    59  		lo, hi int16
    60  		name   obj.AddrName
    61  	}
    62  
    63  	tests := []struct {
    64  		name      string
    65  		framesize int64
    66  		setup     func() (*obj.Prog, []obj.JumpTable)
    67  		wantLDP   int
    68  		wantSTP   int
    69  		wantMOVD  int
    70  		wantNOP   int
    71  		wantPair  *pairWant
    72  	}{
    73  		{
    74  			name: "adjacent LDRs fuse to LDP",
    75  			setup: func() (*obj.Prog, []obj.JumpTable) {
    76  				return chain(
    77  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
    78  					movdLoad(arm64.REG_R1, arm64.REGSP, 24),
    79  				), nil
    80  			},
    81  			wantLDP: 1, wantNOP: 1,
    82  			wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
    83  		},
    84  		{
    85  			name: "adjacent STRs fuse to STP",
    86  			setup: func() (*obj.Prog, []obj.JumpTable) {
    87  				return chain(
    88  					movdStore(arm64.REG_R0, arm64.REGSP, 16),
    89  					movdStore(arm64.REG_R1, arm64.REGSP, 24),
    90  				), nil
    91  			},
    92  			wantSTP: 1, wantNOP: 1,
    93  			wantPair: &pairWant{arm64.ASTP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
    94  		},
    95  		{
    96  			name: "reverse-order LDRs fuse to LDP",
    97  			setup: func() (*obj.Prog, []obj.JumpTable) {
    98  				// p loads from the higher address, q from the lower.
    99  				return chain(
   100  					movdLoad(arm64.REG_R0, arm64.REGSP, 24),
   101  					movdLoad(arm64.REG_R1, arm64.REGSP, 16),
   102  				), nil
   103  			},
   104  			wantLDP: 1, wantNOP: 1,
   105  			wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R1, arm64.REG_R0, obj.NAME_AUTO},
   106  		},
   107  		{
   108  			name: "reverse-order STRs fuse to STP",
   109  			setup: func() (*obj.Prog, []obj.JumpTable) {
   110  				// p stores to the higher address, q to the lower.
   111  				return chain(
   112  					movdStore(arm64.REG_R0, arm64.REGSP, 24),
   113  					movdStore(arm64.REG_R1, arm64.REGSP, 16),
   114  				), nil
   115  			},
   116  			wantSTP: 1, wantNOP: 1,
   117  			wantPair: &pairWant{arm64.ASTP, arm64.REGSP, 16, arm64.REG_R1, arm64.REG_R0, obj.NAME_AUTO},
   118  		},
   119  		{
   120  			name: "PARAM pair fuses",
   121  			setup: func() (*obj.Prog, []obj.JumpTable) {
   122  				return chain(
   123  					param(movdLoad(arm64.REG_R0, arm64.REGSP, 16)),
   124  					param(movdLoad(arm64.REG_R1, arm64.REGSP, 24)),
   125  				), nil
   126  			},
   127  			wantLDP: 1, wantNOP: 1,
   128  			wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_PARAM},
   129  		},
   130  		{
   131  			name: "mixed AUTO and PARAM do not fuse",
   132  			setup: func() (*obj.Prog, []obj.JumpTable) {
   133  				return chain(
   134  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   135  					param(movdLoad(arm64.REG_R1, arm64.REGSP, 24)),
   136  				), nil
   137  			},
   138  			wantMOVD: 2,
   139  		},
   140  		{
   141  			name: "non-adjacent offsets do not fuse",
   142  			setup: func() (*obj.Prog, []obj.JumpTable) {
   143  				return chain(
   144  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   145  					movdLoad(arm64.REG_R1, arm64.REGSP, 32),
   146  				), nil
   147  			},
   148  			wantMOVD: 2,
   149  		},
   150  		{
   151  			name: "different base registers do not fuse",
   152  			setup: func() (*obj.Prog, []obj.JumpTable) {
   153  				return chain(
   154  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   155  					movdLoad(arm64.REG_R1, arm64.REG_R28, 24),
   156  				), nil
   157  			},
   158  			wantMOVD: 2,
   159  		},
   160  		{
   161  			name: "load followed by store does not fuse",
   162  			setup: func() (*obj.Prog, []obj.JumpTable) {
   163  				return chain(
   164  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   165  					movdStore(arm64.REG_R1, arm64.REGSP, 24),
   166  				), nil
   167  			},
   168  			wantMOVD: 2,
   169  		},
   170  		{
   171  			name: "same destination register does not fuse",
   172  			setup: func() (*obj.Prog, []obj.JumpTable) {
   173  				return chain(
   174  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   175  					movdLoad(arm64.REG_R0, arm64.REGSP, 24),
   176  				), nil
   177  			},
   178  			wantMOVD: 2,
   179  		},
   180  		{
   181  			name: "first load writing the second's base does not fuse",
   182  			setup: func() (*obj.Prog, []obj.JumpTable) {
   183  				// Executed sequentially, the second load computes its
   184  				// address from the value the first load just wrote into
   185  				// R1; LDP would compute both addresses from the original
   186  				// R1.
   187  				return chain(
   188  					movdLoad(arm64.REG_R1, arm64.REG_R1, 16),
   189  					movdLoad(arm64.REG_R2, arm64.REG_R1, 24),
   190  				), nil
   191  			},
   192  			wantMOVD: 2,
   193  		},
   194  		{
   195  			name: "second load writing the base fuses",
   196  			setup: func() (*obj.Prog, []obj.JumpTable) {
   197  				// No address depends on the base after the second load
   198  				// overwrites it, so LDP has identical semantics.
   199  				return chain(
   200  					movdLoad(arm64.REG_R0, arm64.REG_R5, 16),
   201  					movdLoad(arm64.REG_R5, arm64.REG_R5, 24),
   202  				), nil
   203  			},
   204  			wantLDP: 1, wantNOP: 1,
   205  			wantPair: &pairWant{arm64.ALDP, arm64.REG_R5, 16, arm64.REG_R0, arm64.REG_R5, obj.NAME_AUTO},
   206  		},
   207  		{
   208  			name: "store whose source is the base fuses",
   209  			setup: func() (*obj.Prog, []obj.JumpTable) {
   210  				// Stores never write registers, so there is no base
   211  				// hazard for STP.
   212  				return chain(
   213  					movdStore(arm64.REG_R5, arm64.REG_R5, 16),
   214  					movdStore(arm64.REG_R1, arm64.REG_R5, 24),
   215  				), nil
   216  			},
   217  			wantSTP: 1, wantNOP: 1,
   218  			wantPair: &pairWant{arm64.ASTP, arm64.REG_R5, 16, arm64.REG_R5, arm64.REG_R1, obj.NAME_AUTO},
   219  		},
   220  		{
   221  			name: "skip when second instruction is a branch target",
   222  			setup: func() (*obj.Prog, []obj.JumpTable) {
   223  				p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
   224  				p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
   225  				br := &obj.Prog{As: arm64.AB, To: obj.Addr{Type: obj.TYPE_BRANCH}}
   226  				br.To.SetTarget(p2)
   227  				return chain(br, p1, p2), nil
   228  			},
   229  			wantMOVD: 2,
   230  		},
   231  		{
   232  			name: "skip when second instruction is a backward-branch target",
   233  			setup: func() (*obj.Prog, []obj.JumpTable) {
   234  				// The branch sits after the pair, so target collection
   235  				// must consider the whole Prog list, not just what
   236  				// precedes the pair.
   237  				p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
   238  				p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
   239  				br := &obj.Prog{As: arm64.AB, To: obj.Addr{Type: obj.TYPE_BRANCH}}
   240  				br.To.SetTarget(p2)
   241  				return chain(p1, p2, br), nil
   242  			},
   243  			wantMOVD: 2,
   244  		},
   245  		{
   246  			name: "skip when second instruction is a jump-table target",
   247  			setup: func() (*obj.Prog, []obj.JumpTable) {
   248  				p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
   249  				p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
   250  				return chain(p1, p2), []obj.JumpTable{{Targets: []*obj.Prog{p2}}}
   251  			},
   252  			wantMOVD: 2,
   253  		},
   254  		{
   255  			name: "skip when second instruction is a statement boundary",
   256  			setup: func() (*obj.Prog, []obj.JumpTable) {
   257  				// Statement-marked instructions (including those genssa
   258  				// reuses as inline marks, which it promotes to
   259  				// statements) must not become zero-sized. The statement
   260  				// bit needs a known position to stick to.
   261  				var tab src.PosTable
   262  				pos := tab.XPos(src.MakePos(src.NewFileBase("f.go", "f.go"), 1, 1))
   263  				p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
   264  				p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
   265  				p2.Pos = pos.WithIsStmt()
   266  				return chain(p1, p2), nil
   267  			},
   268  			wantMOVD: 2,
   269  		},
   270  		{
   271  			name: "at the encodable bound fuses",
   272  			setup: func() (*obj.Prog, []obj.JumpTable) {
   273  				// Resolved offset 496+0+8 = 504, LDP's maximum.
   274  				return chain(
   275  					movdLoad(arm64.REG_R0, arm64.REGSP, 496),
   276  					movdLoad(arm64.REG_R1, arm64.REGSP, 504),
   277  				), nil
   278  			},
   279  			wantLDP: 1, wantNOP: 1,
   280  			wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 496, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
   281  		},
   282  		{
   283  			name: "resolved offset out of LDP range does not fuse",
   284  			setup: func() (*obj.Prog, []obj.JumpTable) {
   285  				// Resolved offset 504+0+8 = 512, just past the bound.
   286  				return chain(
   287  					movdLoad(arm64.REG_R0, arm64.REGSP, 504),
   288  					movdLoad(arm64.REG_R1, arm64.REGSP, 512),
   289  				), nil
   290  			},
   291  			wantMOVD: 2,
   292  		},
   293  		{
   294  			name:      "large frame pushes resolved offset out of range",
   295  			framesize: 1024,
   296  			setup: func() (*obj.Prog, []obj.JumpTable) {
   297  				// 16+1024+8 = 1048: fusing would force an
   298  				// assembler-synthesized address, no smaller than the
   299  				// original pair.
   300  				return chain(
   301  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   302  					movdLoad(arm64.REG_R1, arm64.REGSP, 24),
   303  				), nil
   304  			},
   305  			wantMOVD: 2,
   306  		},
   307  		{
   308  			name:      "deep spill slots fuse in a large frame",
   309  			framesize: 1024,
   310  			setup: func() (*obj.Prog, []obj.JumpTable) {
   311  				// -528+1024+8 = 504: encodable even though the
   312  				// pre-resolution offset is far below -512.
   313  				return chain(
   314  					movdLoad(arm64.REG_R0, arm64.REGSP, -528),
   315  					movdLoad(arm64.REG_R1, arm64.REGSP, -520),
   316  				), nil
   317  			},
   318  			wantLDP: 1, wantNOP: 1,
   319  			wantPair: &pairWant{arm64.ALDP, arm64.REGSP, -528, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
   320  		},
   321  		{
   322  			name: "misaligned offsets do not fuse",
   323  			setup: func() (*obj.Prog, []obj.JumpTable) {
   324  				return chain(
   325  					movdLoad(arm64.REG_R0, arm64.REGSP, -12),
   326  					movdLoad(arm64.REG_R1, arm64.REGSP, -4),
   327  				), nil
   328  			},
   329  			wantMOVD: 2,
   330  		},
   331  		{
   332  			name: "three adjacent loads fuse greedily",
   333  			setup: func() (*obj.Prog, []obj.JumpTable) {
   334  				return chain(
   335  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   336  					movdLoad(arm64.REG_R1, arm64.REGSP, 24),
   337  					movdLoad(arm64.REG_R2, arm64.REGSP, 32),
   338  				), nil
   339  			},
   340  			wantLDP: 1, wantNOP: 1, wantMOVD: 1,
   341  		},
   342  		{
   343  			name: "intervening instruction blocks fusion",
   344  			setup: func() (*obj.Prog, []obj.JumpTable) {
   345  				return chain(
   346  					movdLoad(arm64.REG_R0, arm64.REGSP, 16),
   347  					&obj.Prog{As: arm64.AHINT},
   348  					movdLoad(arm64.REG_R1, arm64.REGSP, 24),
   349  				), nil
   350  			},
   351  			wantMOVD: 2,
   352  		},
   353  		{
   354  			name: "pre-indexed addressing does not fuse",
   355  			setup: func() (*obj.Prog, []obj.JumpTable) {
   356  				p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
   357  				p1.Scond = arm64.C_XPRE
   358  				return chain(p1, movdLoad(arm64.REG_R1, arm64.REGSP, 24)), nil
   359  			},
   360  			wantMOVD: 2,
   361  		},
   362  		{
   363  			name: "post-indexed addressing does not fuse",
   364  			setup: func() (*obj.Prog, []obj.JumpTable) {
   365  				p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
   366  				p1.Scond = arm64.C_XPOST
   367  				return chain(p1, movdLoad(arm64.REG_R1, arm64.REGSP, 24)), nil
   368  			},
   369  			wantMOVD: 2,
   370  		},
   371  	}
   372  
   373  	for _, tt := range tests {
   374  		t.Run(tt.name, func(t *testing.T) {
   375  			head, jumpTables := tt.setup()
   376  			pairSpills(head, tt.framesize, jumpTables)
   377  			got := countAs(head)
   378  			if got[arm64.ALDP] != tt.wantLDP {
   379  				t.Errorf("ALDP count = %d, want %d", got[arm64.ALDP], tt.wantLDP)
   380  			}
   381  			if got[arm64.ASTP] != tt.wantSTP {
   382  				t.Errorf("ASTP count = %d, want %d", got[arm64.ASTP], tt.wantSTP)
   383  			}
   384  			if got[arm64.AMOVD] != tt.wantMOVD {
   385  				t.Errorf("AMOVD count = %d, want %d", got[arm64.AMOVD], tt.wantMOVD)
   386  			}
   387  			if got[obj.ANOP] != tt.wantNOP {
   388  				t.Errorf("ANOP count = %d, want %d", got[obj.ANOP], tt.wantNOP)
   389  			}
   390  			if w := tt.wantPair; w != nil {
   391  				var fused *obj.Prog
   392  				for p := head; p != nil; p = p.Link {
   393  					if p.As == arm64.ALDP || p.As == arm64.ASTP {
   394  						fused = p
   395  						break
   396  					}
   397  				}
   398  				if fused == nil {
   399  					t.Fatal("no LDP/STP emitted")
   400  				}
   401  				mem, regs := &fused.From, &fused.To // LDP order
   402  				if fused.As == arm64.ASTP {
   403  					regs, mem = &fused.From, &fused.To
   404  				}
   405  				if fused.As != w.as {
   406  					t.Errorf("fused As = %v, want %v", fused.As, w.as)
   407  				}
   408  				if mem.Type != obj.TYPE_MEM || mem.Reg != w.base || mem.Offset != w.off || mem.Name != w.name {
   409  					t.Errorf("memory operand = {Type %v, Reg %v, Offset %d, Name %v}, want {TYPE_MEM, %v, %d, %v}",
   410  						mem.Type, mem.Reg, mem.Offset, mem.Name, w.base, w.off, w.name)
   411  				}
   412  				if regs.Type != obj.TYPE_REGREG || regs.Reg != w.lo || regs.Offset != int64(w.hi) {
   413  					t.Errorf("register pair = {Type %v, Reg %v, Offset %v}, want {TYPE_REGREG, %v, %v}",
   414  						regs.Type, regs.Reg, regs.Offset, w.lo, w.hi)
   415  				}
   416  			}
   417  		})
   418  	}
   419  }
   420  

View as plain text