golang/go
View on GitHubcmd/compile: inconsistent check insertion for division and bounds checks
Open
#77433 opened on Feb 4, 2026
NeedsDecisionPerformancecompiler/runtimehelp wanted
Description
Summary
The Go compiler exhibits unexpected behavior when inserting panic checks for slice indexing operations involving modulo expressions.
What version of Go are you using (gotip version)?
$ gotip version
go version go1.27-devel_a3688ab1 Tue Feb 3 20:24:21 2026 -0800 linux/amd64
Output of go env in your module/workspace:
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/denk/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/denk/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2520897468=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/denk/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/denk/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/denk/sdk/gotip'
GOSUMDB='sum.golang.org'
GOTELEMETRY='off'
GOTELEMETRYDIR='/home/denk/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/denk/sdk/gotip/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.27-devel_5aa006d Thu Jan 29 16:48:45 2026 -0800'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Consider the following code that demonstrates the inconsistency:
package main
var funcs = [...]func(uint){
// Case 1: No guard - only panicdivide inserted (why no panicBounds, although there is in 2?)
func(n uint) {
buf := make([]int, n)
var i uint
buf[i%n] = 0 // runtime.panicdivide only, no panicBounds
},
// Case 2: Guard before make - panicBounds inserted (redundant)
func(n uint) {
if n == 0 {
return
}
buf := make([]int, n)
var i uint
buf[i%n] = 0 // runtime.panicBounds only
},
// Case 3: Guard after make - NO panic inserted (optimized)
func(n uint) {
buf := make([]int, n)
var i uint
if n == 0 {
return
}
buf[i%n] = 0 // NO panic - optimized
},
// Case 4: Guard using len(buf) == 0 - NO panic inserted (optimized)
func(n uint) {
buf := make([]int, n)
var i uint
if len(buf) == 0 {
return
}
buf[i%n] = 0 // NO panic - optimized
},
// Case 5: Using uint(len(buf)) - panicdivide inserted
func(n uint) {
buf := make([]int, n)
var i uint
buf[i%uint(len(buf))] = 0 // runtime.panicdivide
},
// Case 6: Guard before make + uint(len(buf)) - panicdivide inserted (redundant)
func(n uint) {
if n == 0 {
return
}
buf := make([]int, n)
var i uint
buf[i%uint(len(buf))] = 0 // runtime.panicdivide
},
// Case 7: Guard using len(buf) == 0 + uint(len(buf)) - NO panic inserted (optimized)
func(n uint) {
buf := make([]int, n)
var i uint
if len(buf) == 0 {
return
}
buf[i%uint(len(buf))] = 0 // NO panic - optimized
},
}
func main() {}
Compile output:
$ gotip tool compile -S main.go | grep panic
0x0060 00096 (/main.go:8) CALL runtime.panicdivide(SB)
rel 97+4 t=R_CALL runtime.panicdivide+0
0x0065 00101 (/main.go:18) CALL runtime.panicBounds(SB)
rel 102+4 t=R_CALL runtime.panicBounds+0
0x0060 00096 (/main.go:45) CALL runtime.panicdivide(SB)
rel 97+4 t=R_CALL runtime.panicdivide+0
0x0065 00101 (/main.go:55) CALL runtime.panicdivide(SB)
rel 102+4 t=R_CALL runtime.panicdivide+0
What did you expect to see?
Case 1: Since buf[i%n] performs slice indexing, I would expect only panicdivide
Case 2, 6: Since n != 0 is guaranteed by the guard, expect no panic at all
Key question: Why does the presence of the guard if n == 0 { return } cause the compiler to switch from inserting panicdivide (Case 1) to inserting panicBounds (Case 2), rather than eliminating the panic check entirely (as in Cases 3, 4, 7)?
What did you see instead?
- Case 1,6: Only
panicdivide— no bounds check - Case 2: Only
panicBounds— no divide check - Cases 3, 4, 7: No panic at all (optimized away)