Benchmark Methodology
Contents
- Writing Benchmarks
- Running Benchmarks
- Interpreting Results
- Using benchstat for Comparison
- Benchmark Examples from Performance Patterns
- Profiling with pprof
- Common Mistakes
Writing Benchmarks
Go benchmarks use the testing.B type and live in _test.go files. Function
names must start with Benchmark. On Go 1.24+, prefer b.Loop().
func BenchmarkStrconv(b *testing.B) {
for b.Loop() {
s := strconv.Itoa(rand.Int())
_ = s
}
}
func BenchmarkFmtSprint(b *testing.B) {
for b.Loop() {
s := fmt.Sprint(rand.Int())
_ = s
}
}Key rules:
- Use
b.Loop()on Go 1.24+; usefor i := 0; i < b.N; i++only when maintaining older Go versions - Assign results to a variable (or
_) to prevent the compiler from optimizing away the call - Use
b.ResetTimer()after expensive setup that shouldn't be measured - Use
b.ReportAllocs()or the-benchmemflag for allocation tracking
Sub-benchmarks
func BenchmarkConvert(b *testing.B) {
for _, size := range []int{10, 100, 1000} {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
data := make([]byte, size)
b.ResetTimer()
for b.Loop() {
_ = string(data)
}
})
}
}Running Benchmarks
# Run all benchmarks in a package
go test -bench=. ./...
# Run specific benchmark with memory stats
go test -bench=BenchmarkStrconv -benchmem ./...
# Run with count for statistical significance
go test -bench=. -benchmem -count=10 ./...The -benchmem flag reports allocations per operation. The -count flag runs
each benchmark N times for statistical significance.
Interpreting Results
BenchmarkStrconv-8 18705042 64.2 ns/op 16 B/op 1 allocs/op
BenchmarkFmtSprint-8 8249536 143.0 ns/op 16 B/op 2 allocs/op| Field | Meaning |
|---|---|
-8 |
GOMAXPROCS |
18705042 |
Number of iterations |
64.2 ns/op |
Time per operation |
16 B/op |
Bytes allocated per operation |
1 allocs/op |
Heap allocations per operation |
Using benchstat for Comparison
benchstat compares benchmark results statistically. Install it and save
benchmark output to files:
# Install benchstat
go install golang.org/x/perf/cmd/benchstat@latest
# Run benchmarks and save results
go test -bench=. -benchmem -count=10 ./... > old.txt
# Make changes, then run again
go test -bench=. -benchmem -count=10 ./... > new.txt
# Compare results
benchstat old.txt new.txtInterpreting benchstat Output
name old time/op new time/op delta
Strconv-8 64.2ns ± 2% 61.8ns ± 1% -3.74% (p=0.001 n=10+10)- delta: Percentage change (negative = faster)
- p-value: Statistical significance (p < 0.05 is significant)
- n: Number of valid samples used
Tips:
- Always use
-count=10or higher for reliable results - A small p-value confirms the change is real, not noise
- If benchstat shows
~(tilde), the difference is not statistically significant
Benchmark Examples from Performance Patterns
strconv vs fmt
| Approach | Speed | Allocations |
|---|---|---|
fmt.Sprint |
143 ns/op | 2 allocs/op |
strconv.Itoa |
64.2 ns/op | 1 allocs/op |
Repeated Byte Conversions
func BenchmarkRepeatedConversion(b *testing.B) {
var buf bytes.Buffer
for b.Loop() {
buf.Write([]byte("Hello world"))
}
}
func BenchmarkSingleConversion(b *testing.B) {
var buf bytes.Buffer
data := []byte("Hello world")
for b.Loop() {
buf.Write(data)
}
}| Approach | Speed |
|---|---|
| Repeated conversion | 22.2 ns/op |
| Single conversion | 3.25 ns/op |
Slice Capacity
func BenchmarkNoCapacity(b *testing.B) {
for b.Loop() {
data := make([]int, 0)
for k := 0; k < 1000; k++ {
data = append(data, k)
}
}
}
func BenchmarkWithCapacity(b *testing.B) {
for b.Loop() {
data := make([]int, 0, 1000)
for k := 0; k < 1000; k++ {
data = append(data, k)
}
}
}| Approach | Time (100M iterations) |
|---|---|
| No capacity | 2.48s |
| With capacity | 0.21s |
Profiling with pprof
Use pprof to identify bottlenecks before optimizing. Benchmarks measure
improvement; pprof finds where to improve.
CPU Profiling
# Generate a CPU profile from benchmarks
go test -bench=BenchmarkHotPath -cpuprofile=cpu.prof ./...
# Analyze with pprof
go tool pprof cpu.profCommon pprof commands:
(pprof) top10 # Top 10 functions by CPU time
(pprof) list funcName # Annotated source for a function
(pprof) web # Interactive graph in browserMemory Profiling
# Generate a memory profile
go test -bench=BenchmarkHotPath -memprofile=mem.prof ./...
# Analyze allocations
go tool pprof -alloc_space mem.profHTTP Profiling for Running Services
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... application code ...
}Access profiles at http://localhost:6060/debug/pprof/.
Profiling Workflow
- Benchmark the suspected hot path
- Profile with pprof to confirm where time is spent
- Optimize using patterns from this skill
- Re-benchmark to verify improvement with benchstat
- Re-profile to check for new bottlenecks
Common Mistakes
Ignoring the benchmark loop
The testing framework adjusts iteration counts to get stable timing. Using a fixed iteration count produces meaningless results:
// Bad: Ignores the benchmark loop, so the framework can't calibrate
func BenchmarkFixed(b *testing.B) {
for i := 0; i < 1000; i++ {
doWork()
}
}
// Good: Use b.Loop on Go 1.24+
func BenchmarkCorrect(b *testing.B) {
for b.Loop() {
doWork()
}
}Not preventing compiler elision
If the result of a function call is unused, the compiler may optimize the call away entirely. Assign results to a package-level variable:
// Bad: Compiler may optimize away the call
func BenchmarkElided(b *testing.B) {
for b.Loop() {
expensiveFunc()
}
}
// Good: Assign to package-level var to prevent elision
var benchResult int
func BenchmarkKept(b *testing.B) {
var r int
for b.Loop() {
r = expensiveFunc()
}
benchResult = r
}