Go version
go1.28-devel_145001b82a (also reproduces on go1.26.x; go1.25 is fine)
Output of go env in your module/workspace:
GOARCH=arm64
GOOS=linux
GOARM64=v8.0
CGO_ENABLED=1
GOEXPERIMENT=
What did you do?
Compiled an index expression whose stride is not a power of two — the shape that shows up whenever a slice is used as a flat 2-D buffer or a record array:
//go:noinline
func U32Mul3(p []uint32, x int) uint32 { return p[3*x] }
GOOS=linux GOARCH=arm64 go build -gcflags=-S
What did you see happen?
Six instructions. 3*x is computed for the bounds check and then discarded; the byte offset is recomputed from x as 12*x, and the load gets no scaling:
ADD R3<<1, R3, R2 // 3x, used only by the bounds check
CMP R2, R1
BLS <panic>
LSL $2, R3, R1 // 4x
ADD R1<<1, R1, R1 // 4x + 8x = 12x
MOVWU (R0)(R1), R0 // no scale
The address-generation chain is also one level deeper (lsl -> add -> ldr instead of add -> ldr).
What did you expect to see?
The element-width scaling belongs in the addressing mode, and the 3*x already computed for the bounds check should be reused as the index. That is what go1.24.6 and go1.25.5 both emit — four instructions:
ADD R3<<1, R3, R2 // 3x, shared with the bounds check
CMP R2, R1
BLS <panic>
MOVWU (R0)(R2<<2), R0 // <<2 folded into the addressing mode
This was Introduced by CL 714160 (9bbda7c99d, "cmd/compile: make prove understand div, mod better", Go 1.26).
Its gate is reversible — middle opt turns the multiply back into a shift. What is not reversible is another rewrite consuming the multiply first, which is what generic.rules:1341 now does:
// C * (D * x) = (C * D) * x
(Mul64 (Const64 <t> [c]) (Mul64 (Const64 <t> [d]) x)) => (Mul64 (Const64 <t> [c*d]) x)
For p[3*x] on a []uint32 the byte offset is Mul(4, Mul(3, x)). Before CL 714160 the *4 was already Lsh by the time this rule could see it, so it never applied to an element scaling; now it merges the two constants into Mul64 x 12. By the time the gate lifts, 12 is not a power of two, so the shift is never recovered and the SLLconst [2] that (MOVWUloadidx ptr (SLLconst [2] idx) mem) => (MOVWUloadidx4 ptr idx mem) needs is never formed.
The sibling rule at generic.rules:421 shows the intended treatment: c * (d+x) is skipped when c is a power of two; c * (d*x) is not.
Go version
go1.28-devel_145001b82a (also reproduces on go1.26.x; go1.25 is fine)
Output of
go envin your module/workspace:What did you do?
Compiled an index expression whose stride is not a power of two — the shape that shows up whenever a slice is used as a flat 2-D buffer or a record array:
What did you see happen?
Six instructions.
3*xis computed for the bounds check and then discarded; the byte offset is recomputed fromxas12*x, and the load gets no scaling:The address-generation chain is also one level deeper (
lsl -> add -> ldrinstead ofadd -> ldr).What did you expect to see?
The element-width scaling belongs in the addressing mode, and the
3*xalready computed for the bounds check should be reused as the index. That is what go1.24.6 and go1.25.5 both emit — four instructions:This was Introduced by CL 714160 (
9bbda7c99d, "cmd/compile: make prove understand div, mod better", Go 1.26).Its gate is reversible —
middle optturns the multiply back into a shift. What is not reversible is another rewrite consuming the multiply first, which is whatgeneric.rules:1341now does:For
p[3*x]on a[]uint32the byte offset isMul(4, Mul(3, x)). Before CL 714160 the*4was alreadyLshby the time this rule could see it, so it never applied to an element scaling; now it merges the two constants intoMul64 x 12. By the time the gate lifts, 12 is not a power of two, so the shift is never recovered and theSLLconst [2]that(MOVWUloadidx ptr (SLLconst [2] idx) mem) => (MOVWUloadidx4 ptr idx mem)needs is never formed.The sibling rule at
generic.rules:421shows the intended treatment:c * (d+x)is skipped whencis a power of two;c * (d*x)is not.