blob: 5a9dec40ab7cc21557cd13efae2667e97780aff7 [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/compile/internal/base"
"cmd/compile/internal/objw"
"cmd/internal/obj"
"cmd/internal/obj/arm64"
"cmd/internal/src"
)
// movdImmMemInfo describes a plain "MOVD reg, off(base)" or "MOVD off(base), reg"
// instruction. base, off, name, and sym record the memory operand's base
// register, offset, addressing class (NAME_AUTO or NAME_PARAM), and symbol;
// reg is the register stored or loaded, and isLoad distinguishes the two
// forms.
type movdImmMemInfo struct {
base, reg int16
off int64
name obj.AddrName
sym *obj.LSym
isLoad bool
}
// movdImmMem decodes p into a movdImmMemInfo. ok reports whether p is a
// plain MOVD load or store using an immediate-offset addressing form
// suitable for LDP/STP coalescing.
func movdImmMem(p *obj.Prog) (info movdImmMemInfo, ok bool) {
if p.As != arm64.AMOVD || p.Scond != 0 || p.Reg != 0 {
return movdImmMemInfo{}, false
}
var a *obj.Addr
switch {
case p.From.Type == obj.TYPE_MEM && p.To.Type == obj.TYPE_REG:
// Plain load: MOVD mem, reg.
a = &p.From
info.reg = p.To.Reg
info.isLoad = true
case p.From.Type == obj.TYPE_REG && p.To.Type == obj.TYPE_MEM:
// Plain store: MOVD reg, mem.
a = &p.To
info.reg = p.From.Reg
default:
return movdImmMemInfo{}, false
}
if a.Index != 0 || (a.Name != obj.NAME_AUTO && a.Name != obj.NAME_PARAM) {
return movdImmMemInfo{}, false
}
info.base, info.off, info.name, info.sym = a.Reg, a.Offset, a.Name, a.Sym
return info, true
}
// ssaGenFinish runs after genssa has emitted all of a function's Progs,
// resolved its branch and jump-table targets, and finalized the frame size
// in defframe.
func ssaGenFinish(pp *objw.Progs) {
if base.Flag.N != 0 {
// Keep unoptimized builds unoptimized.
return
}
pairSpills(pp.Text, pp.Text.To.Offset, pp.CurFunc.LSym.Func().JumpTables)
}
// pairSpills fuses strictly-adjacent MOVD load or store pairs that target the
// same base register at consecutive 8-byte offsets into a single LDP or STP.
// text is the function's first Prog, framesize its final frame size, and
// jumpTables its resolved jump tables.
//
// This recovers spill/reload pairings that the SSA-level pair pass
// (cmd/compile/internal/ssa/pair.go) cannot perform: that pass runs before
// regalloc and only sees source-level loads/stores, so the STR/LDR pairs that
// regalloc later inserts for OpStoreReg/OpLoadReg never get a chance to be
// paired. Doing the fusion here, as the last step of code generation, keeps the
// optimization in the compiler so the assembler can stay a simple translator.
func pairSpills(text *obj.Prog, framesize int64, jumpTables []obj.JumpTable) {
// Branch and jump-table targets, collected lazily once a candidate pair
// survives the cheaper checks; most functions have no candidates.
// Fusing into the first of a pair whose second instruction is a target
// would silently change control flow: paths that jump directly to the
// second instruction would skip the work the LDP/STP now does at the
// first instruction's position.
var isTarget map[*obj.Prog]bool
targets := func() map[*obj.Prog]bool {
if isTarget == nil {
isTarget = map[*obj.Prog]bool{}
for p := text; p != nil; p = p.Link {
if t := p.To.Target(); t != nil {
isTarget[t] = true
}
}
for _, jt := range jumpTables {
for _, t := range jt.Targets {
isTarget[t] = true
}
}
}
return isTarget
}
for p := text; p != nil; p = p.Link {
q := p.Link
if q == nil {
continue
}
a, ok := movdImmMem(p)
if !ok {
continue
}
b, ok := movdImmMem(q)
if !ok || a.isLoad != b.isLoad {
continue
}
if a.base != b.base || a.name != b.name {
continue
}
if a.reg == b.reg {
// LDP with Rt1 == Rt2 is CONSTRAINED UNPREDICTABLE. (STP with
// identical source registers would be fine, but spills never
// store the same register twice, so don't bother.)
continue
}
if a.isLoad && a.reg == a.base {
// The first load overwrites the base register: executed
// sequentially, the second load computes its address from the
// just-loaded value, while LDP computes both addresses from
// the original base. (The second load overwriting the base is
// fine; no address depends on it.)
continue
}
if q.Pos.IsStmt() == src.PosIsStmt {
// q carries a statement boundary, and may also be registered
// as an inline mark: genssa promotes instructions it reuses
// as inline marks to statements. Reducing q to a 0-byte ANOP
// would drop the statement from the line tables and violate
// the invariant that inline marks are never zero-sized (they
// are identified by PC).
continue
}
lo, hi := a, b
if lo.off > hi.off {
lo, hi = hi, lo
}
if hi.off != lo.off+8 {
continue
}
// Fuse only when the result will encode as a single LDP/STP, whose
// signed 7-bit immediate scaled by 8 covers offsets [-512, 504].
// The assembler resolves a NAME_AUTO offset to off+framesize+8
// (the 8 is the saved LR) and a NAME_PARAM offset to
// off+framesize+24 or +32 depending on frame alignment padding —
// or off+8 in a frameless leaf, which is not decided until
// assembly, so a frameless PARAM must fit both. An out-of-range
// LDP/STP needs an assembler-synthesized address (ADD + LDP),
// which is no smaller than the original pair and serializes the
// accesses through REGTMP.
var biasLo, biasHi int64
switch {
case lo.name == obj.NAME_AUTO:
// Autos exist only in functions with a frame, and their offsets
// are negative from FP, so framesize+8 rebases them onto the
// non-negative SP-relative range the LDP/STP immediate must reach.
biasLo = framesize + 8
biasHi = biasLo
case framesize == 0:
biasLo, biasHi = 8, 24
case framesize%16 == 0:
biasLo = framesize + 24
biasHi = biasLo
default:
biasLo = framesize + 32
biasHi = biasLo
}
if lo.off%8 != 0 || lo.off+biasLo < -512 || lo.off+biasHi > 504 {
continue
}
if targets()[q] {
continue
}
mem := obj.Addr{Type: obj.TYPE_MEM, Reg: lo.base, Offset: lo.off, Name: lo.name, Sym: lo.sym}
regs := obj.Addr{Type: obj.TYPE_REGREG, Reg: lo.reg, Offset: int64(hi.reg)}
if a.isLoad {
p.As = arm64.ALDP
p.From, p.To = mem, regs
} else {
p.As = arm64.ASTP
p.From, p.To = regs, mem
}
// Reduce q to a 0-byte ANOP rather than unlinking it so that
// branch targets referencing q remain valid.
obj.Nopout(q)
}
}