Repository navigation
cmd/compile: signed division on ppc64x clobbers CR0 without clobberFlags, so a later branch tests the wrong condition #81842
Description
Go version
go version go1.27.1 linux/amd64 (cross-compiling for GOARCH=ppc64le and ppc64). Also reproduced with go1.26.8, go1.21.13, and tip (2ff5743).
Output of go env in your module/workspace:
GOARCH='ppc64le' # also ppc64; any GOPPC64 (power8, power9, power10)
GOOS='linux'
GOAMD64='v1'
CGO_ENABLED='0'What did you do?
Built and ran this program for GOARCH=ppc64le (and ppc64), under qemu-user 10.2.2 and 11.1.1:
package main
import "fmt"
var sinkA, sinkQ, sinkB int64
//go:noinline
func f(c bool, a, b int64) {
if b == 0 { // b != 0 below, so the division has no zero check of its own
return
}
if c {
sinkA = 1
}
sinkQ = a / b
if !c {
sinkB = 1
}
}
func main() {
f(false, 7, 2)
fmt.Println(sinkA, sinkQ, sinkB) // want 0 3 1
}What did you see happen?
On ppc64le and ppc64 it prints 0 3 0. On amd64, 386, arm, arm64, loong64, mips64, mips64le, mipsle, riscv64, and s390x it prints 0 3 1.
The if !c branch tests the condition register left by the compare inside the expansion of a / b, not c. go1.26.8, ppc64le:
CMP R5,$0 ; b == 0 ?
BEQ ret
ANDCC R3,$1,R3 ; CR0 := c&1, the flag for both `if c` and `if !c`
BEQ L1
MOVD $1,R3 ; MOVD R3,sinkA
BR L2
L1: MOVD $1,R3
L2: CMP R5,$-1 ; the DIVD expansion writes CR0 := (b ? -1)
BEQ L3
DIVD R5,R4,R4
BR L4
L3: NEG R4,R4
L4: MOVD R4,sinkQ
BNE ret ; meant c&1 != 0, tests b != -1
MOVD R3,sinkB
ssaGenValue expands OpPPC64DIVD (and OpPPC64DIVW, with CMPW) into CMP arg1,$-1; BEQ; DIVD; BR; NEG to avoid divd's undefined result for MinInt/-1. That compare writes CR0, but DIVD and DIVW in _gen/PPC64Ops.go are not marked clobberFlags, so flagalloc keeps a flags value live across them. -d=ssa/flagalloc/dump shows the flag defined before the division and used unchanged as the control of the block that holds the division.
It needs a signed division by a non-constant divisor already known to be nonzero (an earlier check, an earlier division by it, or a loop bound), with a flag live across it. The typical shape is the same comparison on both sides of the division. With an ordered compare, the branch instead follows the sign of b+1. % is affected the same way at GOPPC64=power8 (power9+ uses MODSD/MODSW). An ISEL consumer in a loop is affected too. aix/ppc64 emits the same code.
s390x and amd64/386 mark their division ops that do the same fixup as clobberFlags: true. arm64, riscv64, mips64, and loong64 emit no compare for division.
What did you expect to see?
0 3 1 on every architecture. Marking DIVD and DIVW clobberFlags: true fixes it: flagalloc regenerates the flag after the division. With that change, the program above and nine variants (int/int32/int16/int8 division, % on power8, ordered compares, an ISEL in a loop) print the amd64 results on ppc64le and ppc64, and MinInt/-1 still gives MinInt. I'll send a CL with that change and a regression test.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
gopherbot commentedon Sep 29, 2026
Change https://go.dev/cl/841125 mentions this issue:
cmd/compile: mark ppc64 signed division ops as clobbering flagsprattmic commentedon Sep 29, 2026
cc @golang/ppc64
pmur commentedon Sep 29, 2026
divdanddivwshouldn't modifycr0. This shouldn't be confused with the dotteddivd.form (akaDIVDCC) which does write tocr0.pmur commentedon Sep 29, 2026
Ugh, I skimmed over the issue report... sorry. The analysis is right, and the fix is OK.
I wonder ifDIVDandDIVWshould just lower to the expected single ppc64 opcode. This seems like an optimization which should be made in the generic pass.Likewise, I think most ppc64 processors optimize for certain divisors. I wonder if we could just get rid of the optimization?Nevermind, that is go-ism for dividing by -1.
cmd/compile: mark ppc64x signed division ops as clobbering flags