blob: 1da7009a38491e8e8c7f70340de598a3ee678954 [file] [edit]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package arm64
import (
"cmd/internal/obj"
"cmd/internal/obj/arm64"
"cmd/internal/src"
"testing"
)
func TestPairSpills(t *testing.T) {
movdLoad := func(dst, base int16, off int64) *obj.Prog {
return &obj.Prog{
As: arm64.AMOVD,
From: obj.Addr{Type: obj.TYPE_MEM, Reg: base, Offset: off, Name: obj.NAME_AUTO},
To: obj.Addr{Type: obj.TYPE_REG, Reg: dst},
}
}
movdStore := func(src, base int16, off int64) *obj.Prog {
return &obj.Prog{
As: arm64.AMOVD,
From: obj.Addr{Type: obj.TYPE_REG, Reg: src},
To: obj.Addr{Type: obj.TYPE_MEM, Reg: base, Offset: off, Name: obj.NAME_AUTO},
}
}
// param rewrites p's memory operand to NAME_PARAM.
param := func(p *obj.Prog) *obj.Prog {
if p.From.Type == obj.TYPE_MEM {
p.From.Name = obj.NAME_PARAM
} else {
p.To.Name = obj.NAME_PARAM
}
return p
}
chain := func(progs ...*obj.Prog) *obj.Prog {
for i := 0; i < len(progs)-1; i++ {
progs[i].Link = progs[i+1]
}
return progs[0]
}
countAs := func(head *obj.Prog) map[obj.As]int {
m := map[obj.As]int{}
for p := head; p != nil; p = p.Link {
m[p.As]++
}
return m
}
// pairWant describes the expected operands of the fused LDP/STP:
// MOVDs of lo and hi (lo at the lower address off, hi at off+8)
// against base, with the memory operand's addressing class name.
type pairWant struct {
as obj.As
base int16
off int64
lo, hi int16
name obj.AddrName
}
tests := []struct {
name string
framesize int64
setup func() (*obj.Prog, []obj.JumpTable)
wantLDP int
wantSTP int
wantMOVD int
wantNOP int
wantPair *pairWant
}{
{
name: "adjacent LDRs fuse to LDP",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
movdLoad(arm64.REG_R1, arm64.REGSP, 24),
), nil
},
wantLDP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
},
{
name: "adjacent STRs fuse to STP",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdStore(arm64.REG_R0, arm64.REGSP, 16),
movdStore(arm64.REG_R1, arm64.REGSP, 24),
), nil
},
wantSTP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ASTP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
},
{
name: "reverse-order LDRs fuse to LDP",
setup: func() (*obj.Prog, []obj.JumpTable) {
// p loads from the higher address, q from the lower.
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 24),
movdLoad(arm64.REG_R1, arm64.REGSP, 16),
), nil
},
wantLDP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R1, arm64.REG_R0, obj.NAME_AUTO},
},
{
name: "reverse-order STRs fuse to STP",
setup: func() (*obj.Prog, []obj.JumpTable) {
// p stores to the higher address, q to the lower.
return chain(
movdStore(arm64.REG_R0, arm64.REGSP, 24),
movdStore(arm64.REG_R1, arm64.REGSP, 16),
), nil
},
wantSTP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ASTP, arm64.REGSP, 16, arm64.REG_R1, arm64.REG_R0, obj.NAME_AUTO},
},
{
name: "PARAM pair fuses",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
param(movdLoad(arm64.REG_R0, arm64.REGSP, 16)),
param(movdLoad(arm64.REG_R1, arm64.REGSP, 24)),
), nil
},
wantLDP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 16, arm64.REG_R0, arm64.REG_R1, obj.NAME_PARAM},
},
{
name: "mixed AUTO and PARAM do not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
param(movdLoad(arm64.REG_R1, arm64.REGSP, 24)),
), nil
},
wantMOVD: 2,
},
{
name: "non-adjacent offsets do not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
movdLoad(arm64.REG_R1, arm64.REGSP, 32),
), nil
},
wantMOVD: 2,
},
{
name: "different base registers do not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
movdLoad(arm64.REG_R1, arm64.REG_R28, 24),
), nil
},
wantMOVD: 2,
},
{
name: "load followed by store does not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
movdStore(arm64.REG_R1, arm64.REGSP, 24),
), nil
},
wantMOVD: 2,
},
{
name: "same destination register does not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
movdLoad(arm64.REG_R0, arm64.REGSP, 24),
), nil
},
wantMOVD: 2,
},
{
name: "first load writing the second's base does not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
// Executed sequentially, the second load computes its
// address from the value the first load just wrote into
// R1; LDP would compute both addresses from the original
// R1.
return chain(
movdLoad(arm64.REG_R1, arm64.REG_R1, 16),
movdLoad(arm64.REG_R2, arm64.REG_R1, 24),
), nil
},
wantMOVD: 2,
},
{
name: "second load writing the base fuses",
setup: func() (*obj.Prog, []obj.JumpTable) {
// No address depends on the base after the second load
// overwrites it, so LDP has identical semantics.
return chain(
movdLoad(arm64.REG_R0, arm64.REG_R5, 16),
movdLoad(arm64.REG_R5, arm64.REG_R5, 24),
), nil
},
wantLDP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ALDP, arm64.REG_R5, 16, arm64.REG_R0, arm64.REG_R5, obj.NAME_AUTO},
},
{
name: "store whose source is the base fuses",
setup: func() (*obj.Prog, []obj.JumpTable) {
// Stores never write registers, so there is no base
// hazard for STP.
return chain(
movdStore(arm64.REG_R5, arm64.REG_R5, 16),
movdStore(arm64.REG_R1, arm64.REG_R5, 24),
), nil
},
wantSTP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ASTP, arm64.REG_R5, 16, arm64.REG_R5, arm64.REG_R1, obj.NAME_AUTO},
},
{
name: "skip when second instruction is a branch target",
setup: func() (*obj.Prog, []obj.JumpTable) {
p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
br := &obj.Prog{As: arm64.AB, To: obj.Addr{Type: obj.TYPE_BRANCH}}
br.To.SetTarget(p2)
return chain(br, p1, p2), nil
},
wantMOVD: 2,
},
{
name: "skip when second instruction is a backward-branch target",
setup: func() (*obj.Prog, []obj.JumpTable) {
// The branch sits after the pair, so target collection
// must consider the whole Prog list, not just what
// precedes the pair.
p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
br := &obj.Prog{As: arm64.AB, To: obj.Addr{Type: obj.TYPE_BRANCH}}
br.To.SetTarget(p2)
return chain(p1, p2, br), nil
},
wantMOVD: 2,
},
{
name: "skip when second instruction is a jump-table target",
setup: func() (*obj.Prog, []obj.JumpTable) {
p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
return chain(p1, p2), []obj.JumpTable{{Targets: []*obj.Prog{p2}}}
},
wantMOVD: 2,
},
{
name: "skip when second instruction is a statement boundary",
setup: func() (*obj.Prog, []obj.JumpTable) {
// Statement-marked instructions (including those genssa
// reuses as inline marks, which it promotes to
// statements) must not become zero-sized. The statement
// bit needs a known position to stick to.
var tab src.PosTable
pos := tab.XPos(src.MakePos(src.NewFileBase("f.go", "f.go"), 1, 1))
p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
p2 := movdLoad(arm64.REG_R1, arm64.REGSP, 24)
p2.Pos = pos.WithIsStmt()
return chain(p1, p2), nil
},
wantMOVD: 2,
},
{
name: "at the encodable bound fuses",
setup: func() (*obj.Prog, []obj.JumpTable) {
// Resolved offset 496+0+8 = 504, LDP's maximum.
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 496),
movdLoad(arm64.REG_R1, arm64.REGSP, 504),
), nil
},
wantLDP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ALDP, arm64.REGSP, 496, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
},
{
name: "resolved offset out of LDP range does not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
// Resolved offset 504+0+8 = 512, just past the bound.
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 504),
movdLoad(arm64.REG_R1, arm64.REGSP, 512),
), nil
},
wantMOVD: 2,
},
{
name: "large frame pushes resolved offset out of range",
framesize: 1024,
setup: func() (*obj.Prog, []obj.JumpTable) {
// 16+1024+8 = 1048: fusing would force an
// assembler-synthesized address, no smaller than the
// original pair.
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
movdLoad(arm64.REG_R1, arm64.REGSP, 24),
), nil
},
wantMOVD: 2,
},
{
name: "deep spill slots fuse in a large frame",
framesize: 1024,
setup: func() (*obj.Prog, []obj.JumpTable) {
// -528+1024+8 = 504: encodable even though the
// pre-resolution offset is far below -512.
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, -528),
movdLoad(arm64.REG_R1, arm64.REGSP, -520),
), nil
},
wantLDP: 1, wantNOP: 1,
wantPair: &pairWant{arm64.ALDP, arm64.REGSP, -528, arm64.REG_R0, arm64.REG_R1, obj.NAME_AUTO},
},
{
name: "misaligned offsets do not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, -12),
movdLoad(arm64.REG_R1, arm64.REGSP, -4),
), nil
},
wantMOVD: 2,
},
{
name: "three adjacent loads fuse greedily",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
movdLoad(arm64.REG_R1, arm64.REGSP, 24),
movdLoad(arm64.REG_R2, arm64.REGSP, 32),
), nil
},
wantLDP: 1, wantNOP: 1, wantMOVD: 1,
},
{
name: "intervening instruction blocks fusion",
setup: func() (*obj.Prog, []obj.JumpTable) {
return chain(
movdLoad(arm64.REG_R0, arm64.REGSP, 16),
&obj.Prog{As: arm64.AHINT},
movdLoad(arm64.REG_R1, arm64.REGSP, 24),
), nil
},
wantMOVD: 2,
},
{
name: "pre-indexed addressing does not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
p1.Scond = arm64.C_XPRE
return chain(p1, movdLoad(arm64.REG_R1, arm64.REGSP, 24)), nil
},
wantMOVD: 2,
},
{
name: "post-indexed addressing does not fuse",
setup: func() (*obj.Prog, []obj.JumpTable) {
p1 := movdLoad(arm64.REG_R0, arm64.REGSP, 16)
p1.Scond = arm64.C_XPOST
return chain(p1, movdLoad(arm64.REG_R1, arm64.REGSP, 24)), nil
},
wantMOVD: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
head, jumpTables := tt.setup()
pairSpills(head, tt.framesize, jumpTables)
got := countAs(head)
if got[arm64.ALDP] != tt.wantLDP {
t.Errorf("ALDP count = %d, want %d", got[arm64.ALDP], tt.wantLDP)
}
if got[arm64.ASTP] != tt.wantSTP {
t.Errorf("ASTP count = %d, want %d", got[arm64.ASTP], tt.wantSTP)
}
if got[arm64.AMOVD] != tt.wantMOVD {
t.Errorf("AMOVD count = %d, want %d", got[arm64.AMOVD], tt.wantMOVD)
}
if got[obj.ANOP] != tt.wantNOP {
t.Errorf("ANOP count = %d, want %d", got[obj.ANOP], tt.wantNOP)
}
if w := tt.wantPair; w != nil {
var fused *obj.Prog
for p := head; p != nil; p = p.Link {
if p.As == arm64.ALDP || p.As == arm64.ASTP {
fused = p
break
}
}
if fused == nil {
t.Fatal("no LDP/STP emitted")
}
mem, regs := &fused.From, &fused.To // LDP order
if fused.As == arm64.ASTP {
regs, mem = &fused.From, &fused.To
}
if fused.As != w.as {
t.Errorf("fused As = %v, want %v", fused.As, w.as)
}
if mem.Type != obj.TYPE_MEM || mem.Reg != w.base || mem.Offset != w.off || mem.Name != w.name {
t.Errorf("memory operand = {Type %v, Reg %v, Offset %d, Name %v}, want {TYPE_MEM, %v, %d, %v}",
mem.Type, mem.Reg, mem.Offset, mem.Name, w.base, w.off, w.name)
}
if regs.Type != obj.TYPE_REGREG || regs.Reg != w.lo || regs.Offset != int64(w.hi) {
t.Errorf("register pair = {Type %v, Reg %v, Offset %v}, want {TYPE_REGREG, %v, %v}",
regs.Type, regs.Reg, regs.Offset, w.lo, w.hi)
}
}
})
}
}