# Eliminating Golang bounds checks with unsafe

Andrii (blog.andr2i.com), writing an optimization-catalog series, on the last resort for bounds check elimination in Golang: when you can prove an index is in range but the compiler can't, `unsafe` pointer arithmetic removes the check the compiler insists on keeping.

## What a bounds check costs

Compile `func load(src []byte, i int) byte { return src[i] }` with `-B` (bounds checks off) and you get three instructions:

```
MOVQ  AX, 0x8(SP)
MOVZX 0(AX)(DI*1), AX
RET
```

Without `-B`, the compiler adds a `CMPQ DI, BX` / `JAE` pair and a `CALL runtime.panicBounds`. The `CALL` is the expensive part in a small function: it stops the function being a leaf, which pulls in `PUSHQ BP` / `MOVQ SP, BP` / `POPQ BP` as well. So on a tiny hot function, removing the check removes more than the compare-and-branch.

The author's argument for why bounds check elimination is worth reaching for first is not just the branch. Fewer instructions means less L1 icache and uop-cache pressure, fewer entries in the frontend's branch prediction structures, and lower register pressure. On a hot path already suffering capacity or conflict misses, shrinking the instruction stream helps twice.

You don't have to read assembly to find them:

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

## The conventional fix first

The compiler will drop a check if you prove the range to it, usually by touching the upper bound before the loop or by reshaping the loop condition. A real example from a `matchLen` function:

```go
a = a[:limit]
b = b[:len(a)]
i := 0
for ; i <= len(a)-8; i += 8 {
    xor := loadU64(a[i:]) ^ loadU64(b[i:])
    ...
}
```

`i <= len(a)-8` lets the compiler prove every `a` access is in range; `b = b[:len(a)]` transfers that proof to `b`. This gets better with each Golang release, and go101's BCE page collects the standard tricks. Reach for `unsafe` only after this fails.

## The unsafe load

The motivating case is `binary.LittleEndian.Uint32`, which already carries the well-known hint:

```go
func (littleEndian) Uint32(b []byte) uint32 {
    _ = b[3] // bounds check hint to compiler
    return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
```

The hint collapses four checks into one. The one that survives is the one `unsafe` removes:

```go
//go:build !purego && (amd64 || 386 || arm64 || loong64 || ppc64le || wasm)

func loadU32LE(b []byte, i uint) uint32 {
    return *(*uint32)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i))
}
```

`unsafe.SliceData(b)` gives the same pointer `&b[0]` would, without the check `b[0]` would introduce, and unlike `&b[0]` it is legal on an empty slice. `unsafe.Add` does the offset arithmetic that `&b[i]` would, and the result is cast to `*uint32` and dereferenced. Three calls to remove one check, all of which the compiler inlines away — the emitted code is `MOVQ AX, 0x8(SP)` / `MOVL 0(AX)(DI*1), AX` / `RET`.

The signature change matters as much as the body. The stdlib call site is `binary.LittleEndian.Uint32(data[offset:])`, and that slicing expression carries its own bounds check on the caller's side. Passing `(data, offset)` as two arguments is what moves the check out of the caller too.

## Where it is and isn't valid

The build tag is load-bearing twice over. Little-endian is the obvious constraint. The subtler one, raised by `ncruces` on HN after publication, is unaligned access: a `*uint32` dereference at an arbitrary byte offset is only safe on platforms that permit unaligned loads. The correct platform list is the intersection of little-endian targets with `unalignedOK` in `cmd/compile/internal/ssa/config.go` — not "every little-endian platform I can think of".

And the check is only removable if you can actually prove it unnecessary. The compiler inserted it for a reason; `unsafe` transfers that proof obligation to you and offers nothing in return if you're wrong.

## Numbers

Microbenchmark, summing 4096 bytes as `uint32` on an i5-12500:

```
BenchmarkLoadU32LE   273.7 ns/op   14966.04 MB/s
BenchmarkStdUint32   600.7 ns/op    6818.58 MB/s
```

More than 2x, which is what you'd expect from a benchmark that does nothing but the load. The real-world number is the interesting one: swapping stdlib LE loads for unsafe ones in `andybalholm/brotli`'s matchfinder moved a production-like workload from 90.39 MiB/s to 99.99 MiB/s, +10.62% (n=30, p=0.000). The technique is not novel — `klauspost/compress` has done this for years — just not widely written down.

The author's closing complaint is that Golang has no opt-in `nobounds` hint at the statement or function level, so unsafe pointer arithmetic is the only available way to say "I checked this already".

## Related

Same family as [[compiler-codegen-luck]]: the performance lives in generated code that the source doesn't show, and the fix is a rewrite that reads as cosmetic. The argument for writing the win down by hand rather than trusting the optimizer to keep finding it is made at length in [[everyone-should-know-simd]], where the objection to auto-vectorization is not that it fails but that an unrelated edit can silently undo it. [[pointer-provenance]] is the formal machinery that says why `unsafe.Add` on a slice's data pointer is the well-defined way to do this rather than casting an integer. For the other end of the same hot-path tuning spectrum, where the problem is the hardware rather than the instruction count, see [[false-sharing-alignment-128]].
