golang/go

cmd/compile: inconsistent check insertion for division and bounds checks

Open

#77433 opened on Feb 4, 2026

View on GitHub
 (5 comments) (0 reactions) (0 assignees)Go (133,883 stars) (19,008 forks)batch import
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)

Contributor guide