# Go Perf Bounds Check Elimination

> Guides bounds-check elimination (BCE) in Go — the compiler checks every slice/array index and the SSA pass removes the ones it proves redundant. Covers seeing surviving checks with go build -gcflags="-d=ssa/check_bce/debug=1", prover-friendly idioms (the _ = b[n-1] / b = b[:n] hint, range loops, constant vs variable offsets), the inlining link, and why -B is a footgun not a fix. Fires on "eliminate bounds checks", "why is this loop slow", "bounds check in hot loop", "optimize this numeric loop". Routes should-I to go-perf-methodology, slices to go-perf-slices.

- Skill: `ctoth/go-perf-bounds-check-elimination` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-perf-bounds-check-elimination`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-perf-bounds-check-elimination/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-perf-bounds-check-elimination

---


# Go Bounds-Check Elimination

Go is memory-safe: every index `b[i]` and most slice expressions `b[:n]` carry an implicit runtime
check that panics on an out-of-range access. The SSA compiler runs a *bounds-check elimination* (BCE)
pass that **proves** many of those checks redundant and deletes them. This skill owns making that
pass visible and writing loops it can satisfy.

**Decide whether to bother first.** BCE wins are small — each check is one well-predicted
compare-and-branch, worth chasing only in a numeric/byte loop a profile has already proven hot. The
"should I optimize this at all" judgment routes to **`go-perf-methodology`**; slice growth/aliasing
to **`go-perf-slices`** and **`go-slices-and-maps`**.

All Go below builds clean under `gofmt`/`go vet`; the `check_bce` output shown was produced on **Go
1.26** (`go version go1.26.0`).

---

## 1. Seeing the Remaining Checks

The compiler's SSA passes accept debug flags via `-gcflags`. The BCE pass reports every check it
**could not** eliminate:

```sh
go build -gcflags="-d=ssa/check_bce/debug=1" .
```

It prints one line per *surviving* check — silence means a fully eliminated expression:

```
./bce.go:4:3: Found IsInBounds        # an index check b[i] that was NOT eliminated
./bce.go:26:7: Found IsSliceInBounds  # a slice-expression check b[:n] that survived
```

`IsInBounds` is the index check (`b[i]`); `IsSliceInBounds` is the slice-expression check
(`b[:n]`, `b[lo:hi]`). The flag applies to the package you build; add `all=` to instrument
dependencies. This output is the *only* trustworthy evidence — never claim a loop is
bounds-check-free without it. (To see the compare+branch a check costs, dump assembly with
`go build -gcflags=-S` and find the branch into `panicIndex`.)

---

## 2. What the Prover Already Does for Free

The most important measured fact: **the canonical loop needs no help.** Both of these emit **zero**
`Found IsInBounds` lines on Go 1.26 — the bound from the loop condition flows to the index:

```go
func sumIndex(b []byte) int {
	n := 0
	for i := 0; i < len(b); i++ {
		n += int(b[i]) // i < len(b) proven by the loop guard → check eliminated
	}
	return n
}

func sumRange(b []byte) int {
	n := 0
	for _, v := range b { // range never indexes past len → no check at all
		n += int(v)
	}
	return n
}
```

First rule: **don't fight a problem you don't have.** Write the obvious
`for i := 0; i < len(b); i++ { b[i] }` or `for i := range b { b[i] }`, run the flag, and only reach
for an idiom if a check survives. Adding `_ = b[len(b)-1]` to these loops changes nothing — they were
already clean.

---

## 3. The Hint Idiom — and Exactly How Far It Reaches

Hints matter when you index **several positions from one base** outside a counted loop. Three writes
to `b[i]`, `b[i+1]`, `b[i+2]` produce **three** surviving checks:

```go
func triple(b []byte, i int) {
	b[i] = 1   // Found IsInBounds
	b[i+1] = 2 // Found IsInBounds
	b[i+2] = 3 // Found IsInBounds
}
```

A single proof up front collapses them. Two forms work — measured to **one** check total, with the
indexed writes free:

```go
func tripleAssertHigh(b []byte) {
	_ = b[2] // one IsInBounds here; establishes len(b) > 2
	b[0] = 1 // free
	b[1] = 2 // free
	b[2] = 3 // free
}

func tripleConstSlice(b []byte) {
	b = b[:3] // one IsSliceInBounds here
	b[0] = 1  // free
	b[1] = 2  // free
	b[2] = 3  // free
}
```

**The critical, measured caveat:** the constant-index trick is what makes that work. With a
**variable** base it does *not* fully transfer — because the prover cannot establish `i >= 0`, so
`i` and `i+1` are not bounded below even after asserting `b[i+2]`:

```go
func tripleVar(b []byte, i int) {
	_ = b[i+2] // Found IsInBounds (the assert itself)
	b[i] = 1   // Found IsInBounds — STILL checked (i could be negative)
	b[i+1] = 2 // Found IsInBounds — STILL checked
	b[i+2] = 3 // free — identical to the asserted expression
}
```

Asserting `b[i+2]` only frees the *identical* expression `b[i+2]`. The hint is reliable for
**constant** offsets from a base; for variable offsets, verify with the flag, and prefer
restructuring to a counted loop the prover already handles (§2).

---

## 4. Two Slices, One Index

Copying `a` into `b` by index checks `b[i]` every iteration — the range proves `a[i]` but says
nothing about `b`:

```go
func copyLoop(a, b []int) {
	for i := range a {
		b[i] = a[i] // Found IsInBounds on b[i]; a[i] is the range element, free
	}
}
```

Cut `b` to `a`'s length **once** before the loop — trading a per-iteration index check for a single
slice check, so the loop body goes clean (measured: one `IsSliceInBounds`, zero `IsInBounds`):

```go
func copyLoopBCE(a, b []int) {
	b = b[:len(a)] // one IsSliceInBounds; len(b) >= len(a) now known in the loop
	for i := range a {
		b[i] = a[i] // free
	}
}
```

(For a plain copy use `copy(b, a)`; this pattern is for real per-element work on two slices.)

---

## 5. How BCE Interacts with Inlining

A bound proven in a caller reaches an index in a callee only if the callee is **inlined** — across a
real call boundary the compiler re-checks, since it can't assume the caller's invariant. A tiny
helper `func at(b []byte, i int) byte { return b[i] }` carries its own check at every call site;
inlining it lets the surrounding loop's proof eliminate it. So **inlining is frequently the
precondition for elimination.** The budget and `//go:noinline` are owned by **`go-perf-inlining`** —
check there first if a check survives only because a helper didn't inline.

---

## 6. Why `-B` Is a Footgun, Not a Fix

`-gcflags=-B` disables bounds checking globally — build with it and **all** `Found IsInBounds` output
disappears because the checks are simply gone. That is not BCE; it removes the safety net. A program
built with `-B` will **read or write out of bounds silently** instead of panicking, turning a clean
panic into memory corruption or a security hole. Use `-B` for exactly one thing: measuring the
*ceiling* — build both ways, benchmark, and see how much the surviving checks cost. If the gap is
small (it usually is), leave the checks in. Never ship `-B`. The same caution applies to `unsafe` to
dodge a check the compiler would have elided anyway — prove it survives with the flag first.

---

## 7. Don't

- **Don't claim a loop is bounds-check-free without the flag.** Run the `check_bce` build and read
  the output — confidence is not evidence.
- **Don't add hints to a canonical counted/range loop.** `for i := 0; i < len(b); i++` and
  `for i := range b` are already eliminated on 1.26; `_ = b[n-1]` there is noise (and a panic on the
  empty slice).
- **Don't assume the constant-index hint transfers to variable offsets.** `_ = b[i+2]` does **not**
  free `b[i]`/`b[i+1]` — the prover can't bound `i` below zero (§3).
- **Don't ship `-gcflags=-B`** — it deletes the safety check, not the redundancy; use it only to
  measure the ceiling (§6), and don't reach for `unsafe` to skip a check the flag shows is already
  gone (try **`go-perf-simd`** if a real win remains).
- **Don't reach for BCE before a profile proves the loop hot.** The win is one compare+branch per
  check; route the "is it worth it" to **`go-perf-methodology`**.
- **Don't re-slice inside the loop** — it re-introduces a check every iteration; slice once up
  front (§4).

---

## 8. Routing to Related Skills

**This plugin (`go-perf-*` depth):**

- `go-perf-methodology` — the should-I-optimize-this-at-all gate; BCE is a last-mile tweak.
- `go-perf-inlining` — why a check survives across a call boundary; inlining as BCE's precondition.
- `go-perf-slices` — slice growth, reuse, prealloc; re-slicing cost.
- `go-perf-simd` — when scalar BCE isn't enough and the loop wants vectorization.
- `go-perf-compiler-intrinsics` — `math/bits` and microarchitecture levels for hot numeric code.

**Marketplace (`go-*` correctness):**

- `go-slices-and-maps` — slice-is-a-view, aliasing, 3-index slices, `len`/`cap` semantics.
- `go-strings-bytes-runes` — `[]byte`/`string` indexing correctness.
- `go-performance` — the measure-first overview this plugin deepens.

---

## 9. Reference Files

Wrong/right BCE anti-patterns from LLM-generated Go, each with measured `check_bce` output and
citations:

[references/common-mistakes.md](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

