Skip to content

cmd/compile: opt merges a power-of-two multiply into a non-power-of-two constant, losing scaled-index addressing on arm64 (regression from CL 714160) #80639

Description

@abypun

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsFixThe path to resolution is known, but the work has not been done.compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Status
    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions