Repository navigation
cmd/compile: regalloc reloads a value from a narrower phi's stack slot, losing its upper bits #81928
Description
Go version
go version go1.27.1 linux/amd64
go1.26.8 and tip (75f2c46, 2026-09-30) fail in the same way. go1.21.13 fails on arm64 only for this program.
Output of go env in your module/workspace:
GOARCH='amd64'
GOOS='linux'
GOAMD64='v1'
GOEXPERIMENT=''The arm64 results are from cross-compiling on that host (GOARCH=arm64). I ran them on an Android phone (Snapdragon 6 Gen 1) and under qemu-aarch64 10.2.2, with the same results.
What did you do?
package main
import "fmt"
var sink int64
//go:noinline
func g(x int64) { sink += x }
//go:noinline
func f(p []int64, c int) int64 {
a0, a1, a2, a3, a4, a5, a6 := p[62], p[21], p[45], p[17], p[12], p[44], p[10]
a7, a8, a9, a10, a11, a12, a13 := p[5], p[48], p[42], p[33], p[7], p[30], p[6]
a14, a15, a16, a17, a18, a19, a20 := p[49], p[31], p[9], p[50], p[23], p[60], p[24]
a21, a22, a23, a24, a25 := p[0], p[26], p[28], p[20], p[4]
var x0, x1, x2 int64
var y int32
if c&1 != 0 {
g(a9)
} else if c&2 != 0 {
t := p[44+c&7]
x1 = p[c&7]
x2 = t
y = int32(t)
} else {
x0 = a8
x1 = a0 + int64(c)
x2 = x1
}
h := int64(0)
h = h*31 + x0
h = h*31 + x1
h = h*31 + x2
h = h*31 + int64(y)
for _, a := range [...]int64{a0, a1, a2, a3, a4, a5, a6, a7, a8, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25} {
h = h*41 + a
}
return h
}
func main() {
p := make([]int64, 64)
for i := range p {
p[i] = int64(i+1)<<32 | int64(i)
}
want := [...]uint64{327951454604667456, 11694440227811961264, 6120305881949198018, 11694440227811961264}
for c, w := range want {
if got := uint64(f(p, c)); got != w {
panic(fmt.Sprintf("f(p, %d) = %#x, want %#x", c, got, w))
}
}
fmt.Println("ok")
}go run x.go
GOARCH=arm64 go build -o x.arm64 x.go # run on arm64
go run -gcflags='-N -l' x.go
What did you see happen?
On amd64 and on arm64:
panic: f(p, 2) = 0xdf87d5acda50d6c2, want 0x54efb1c5da50d6c2
The -N -l build prints ok. The low 32 bits of the result are right. The error comes from x2: its upper 32 bits become the sign extension of its low 32 bits.
What did you expect to see?
ok from every build.
Diagnosis
Trunc64to32 lowers to a Copy on amd64 and arm64. After copy elimination, on the edge from the c&2 != 0 block to the merge, the int32 phi y and the int64 phi x2 both take the same int64 value t. With this many values live, y is a stack phi, homed in a 4-byte LocalSlot.
edgeState.processDest stores t into y's slot with the slot's type, on purpose ("Make sure we spill with the size of the slot, not the size of x (which might be wider due to our dropping of narrowing conversions)"). edgeState.set then adds that StoreReg <int32> to e.cache[t] as a full copy of t. Two things follow on the same edge:
t's register no longer holds its only copy (len(e.cache[vid]) == 1is false, anduniqueRegsdrops it), so another destination may overwrite that register.x2's destination then finds only the 4-byte slot as a source and emitsLoadReg <int32>from it: a 32-bit sign-extending load into an int64 phi.
Output of -d=ssa/regalloc/debug=3 for the edge, on amd64 at 2ff5743 (from an equivalent program):
moving v178 to y0[int32]
sources of v178: R15:v178 [use R15]
v154 = StoreReg <int32> v178 : y0[int32]
v178 now available in y0[int32]:v154
...
moving v128 to R15
sources of v128: a11[int64]:v13 [use a11[int64]]
v178 no longer available in R15:v178
v155 = LoadReg <int64> v13 : R15
...
moving v178 to DI
sources of v178: y0[int32]:v154 [use y0[int32]]
v134 = LoadReg <int32> v154 : DI
In machine code (arm64, the original program): MOVW R10, 160(RSP), MOVD R6, R10, then MOVW 160(RSP), R17.
Other programs of the same shape (a fresh 64-bit value flowing into a wide phi and into a narrower stack phi on the same edge) fail on riscv64, s390x, loong64, mips64 and mips64le too. I have not seen it on ppc64le, where the truncation lowers to an explicit MOVWreg/MOVWZreg. Whether a given program fails depends on register pressure, so the program above shows it on amd64 and arm64 only.
A fix that works for every failing program I have: in edgeState.set, when loc is a LocalSlot narrower than the value (e.s.orig[vid].Type.Size()), record the slot in e.contents (the destination is satisfied) but do not add it to e.cache[vid]. I will send a CL.
This looks related to #80577 but is a different path. #80577 is about a value's own spill and restore choosing the extension from its type. Here a wide value is read back through another phi's narrower slot on an edge.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
gabyhelp commentedon Sep 30, 2026
Related Issues
Related Code Changes
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
smm-h commentedon Sep 30, 2026
Two corrections to the report above.
The extension depends on the architecture. The 4-byte load is
MOVLon amd64, which zero-extends, so there the upper half ofx2becomes zero. On arm64, riscv64, s390x, loong64 and mips64 it isMOVW, which sign-extends. The low halves in the program above are all below 2^31, so both give the same result there. The class is also not 64-bit only:Trunc32to16,Trunc32to8andTrunc16to8are Copies on 386, arm and mips too, and every truncation is a Copy on wasm.The fix suggested at the end (never add a narrower slot to
e.cache[vid]) is wrong: it livelocks the edge shuffle. When a value's only destinations on an edge are narrow phi slots, its register then stays protected as its last copy, and the cycle breaker inedgeState.processmoves it from register to register forever. Some generated programs make the compiler grow without bound with it. The CL I am preparing instead records, per value, the largest size any destination on the edge needs, and does not count a slot smaller than that as a copy. A narrow slot that holds all the edge needs of the value still counts, as before.dmitshur commentedon Oct 5, 2026
CC @golang/compiler.
dmitshur commentedon Oct 6, 2026
From triage meeting, this is likely not a regalloc bug but elsewhere. @cherrymui said that @randall77 had a suggested approach for this might work for this.