Go's bound checks are a safety net, but on hot paths they waste cycles and bloat instruction cache. The compiler eliminates many checks when it can prove safety, but sometimes it can't. When you can prove safety but the compiler can't, unsafe pointer arithmetic is the escape hatch.
The cost of bound checks
A simple src[i] with bound checks enabled adds a CMPQ, JAE, and a CALL to runtime.panicBounds. That turns a leaf function into a non-leaf, adding prologue/epilogue overhead. The -B flag (disable bound checks) shows the difference: 4 instructions versus 7+.
Use go build -gcflags="-d=ssa/check_bce/debug=1" to list all bound checks the compiler sees.
Conventional BCE: slicing hints
The compiler often eliminates checks if you prove bounds first. Example from matchLen:
a = a[:limit]
b = b[:len(a)]
for ; i <= len(a)-8; i += 8 {
// a[i] and b[i] have no bound checks
}
Here, re-slicing and using i <= len(a)-8 lets the compiler prove all accesses are in bounds.
When conventional BCE fails: binary.LittleEndian.Uint32
encoding/binary.LittleEndian.Uint32(b) reads 4 bytes. The standard library already includes a hint _ = b[3] to eliminate checks for indexes 0-2, but one check remains. This function is used heavily in bit-packed data processing (e.g., brotli compression).
Unsafe solution
func loadU32LE(b []byte, i uint) uint32 {
return *(*uint32)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(b)), i))
}
unsafe.SliceData(b)returns a pointer to the first element without triggering a bound check (unlike&b[0]).unsafe.Pointerconverts to the generic pointer type.unsafe.Add(ptr, i)computes&b[i].- Cast to
*uint32and dereference loads 4 bytes as a singleMOVLinstruction.
This works only on little-endian architectures (amd64, arm64, etc.) — guarded by a build constraint.
Performance impact
Microbenchmark reading 4096 bytes in 4-byte chunks:
| Variant | Time/op | Throughput |
|---|---|---|
binary.LittleEndian.Uint32 | 600.7 ns/op | 6818 MB/s |
loadU32LE (unsafe) | 273.7 ns/op | 14966 MB/s |
Unsafe version is 2.2x faster.
Real-world brotli matchfinder benchmark (production-like workload):
| Before | After | Improvement |
|---|---|---|
| 90.39 MiB/s | 99.99 MiB/s | +10.6% |
Caveats
- You must prove safety yourself. The compiler's checks exist for a reason. Only skip them when you are certain the index is within bounds.
- The function signature changes:
data[offset:]slicing is replaced byloadU32LE(data, offset)to avoid a bound check at the call site. - Build constraints must limit the code to little-endian platforms.
When to use this technique
Reserve unsafe BCE for extreme hot paths where every instruction counts. Profiling should show bound checks as a bottleneck. If the compiler can't eliminate them despite your best hints, unsafe is the last resort.
Next steps
- Profile your hot loops with
-d=ssa/check_bce/debug=1to identify leftover bound checks. - Try conventional slicing hints first.
- If the compiler still fails, consider the unsafe pattern shown here — but only after proving safety and measuring the gain.





