Writing Benchmarks in Go
Measure Go code speed and allocations with testing.B, b.Loop and -benchmem, avoid benchmarks the compiler optimizes away, and compare results with benchstat.
A benchmark measures how long code takes and how much memory it allocates. Go benchmarks live in _test.go files next to your tests and run with go test -bench. They are the right tool for questions like "is strings.Builder faster than += here?"
A First Benchmark
// file: concat_test.go
package concat
import (
"strings"
"testing"
)
func joinPlus(parts []string) string {
s := ""
for _, p := range parts {
s += p
}
return s
}
func joinBuilder(parts []string) string {
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
var parts = strings.Split(strings.Repeat("go,", 100), ",")
func BenchmarkJoinPlus(b *testing.B) {
for b.Loop() {
joinPlus(parts)
}
}
func BenchmarkJoinBuilder(b *testing.B) {
for b.Loop() {
joinBuilder(parts)
}
}
- Benchmark functions start with
Benchmarkand takeb *testing.B. for b.Loop()(Go 1.24) runs the body as many times as needed for a stable measurement.
Run them with -bench (a regular expression, . means all) and -benchmem for allocation counts:
go test -bench=. -benchmem
The output looks like this (numbers depend on your machine):
BenchmarkJoinPlus-8 52000 22800 ns/op 54500 B/op 100 allocs/op
BenchmarkJoinBuilder-8 690000 1700 ns/op 1016 B/op 7 allocs/op
Columns: name with the number of CPUs used, how many iterations ran, time per iteration, bytes allocated per iteration, and allocations per iteration. Here the builder version is clearly faster and allocates far less.
Tests run too:
go test -bench=. also runs your normal tests first. Add -run='^$' to skip them and run only benchmarks.
The Older b.N Loop
Code written before Go 1.24 uses b.N:
func BenchmarkJoinBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
joinBuilder(parts)
}
}
It still works. b.Loop is preferred in new code because it handles timer resets and keeps the compiler from removing the loop body.
Benchmarks the Compiler Optimizes Away
A common surprise is a benchmark reporting something like 0.3 ns/op. With the older b.N loop, if the result of the call is never used, the compiler may remove the call entirely, and you are timing an empty loop.
b.Loop prevents this for calls inside the loop. With b.N, the classic fix is to store the result in a package-level variable:
var sink string
func BenchmarkJoinBuilderOld(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = joinBuilder(parts)
}
}
Setup inside the timed loop:
Building test data inside the loop measures that work too. Do setup before the loop. With b.N loops, call b.ResetTimer() after expensive setup. With b.Loop, anything before the first b.Loop() call is not timed.
Benchmarks With Different Input Sizes
Use b.Run to benchmark several sizes in one function:
// file: sizes_test.go
package sizes
import (
"fmt"
"slices"
"testing"
)
func BenchmarkSort(b *testing.B) {
for _, n := range []int{10, 1000} {
b.Run(fmt.Sprint(n), func(b *testing.B) {
data := make([]int, n)
for i := range data {
data[i] = n - i
}
for b.Loop() {
s := slices.Clone(data)
slices.Sort(s)
}
})
}
}
The output shows BenchmarkSort/10 and BenchmarkSort/1000 as separate lines, which makes it easy to see how cost grows with input size.
Comparing Results Reliably
Single runs are noisy. To compare two versions of code:
go test -run='^$' -bench=Join -count=10 > old.txt
# change the code
go test -run='^$' -bench=Join -count=10 > new.txt
go run golang.org/x/perf/cmd/benchstat@latest old.txt new.txt
benchstat reports the change with a confidence measure, so you can tell a real improvement from noise.
Measure before optimizing:
Benchmarks show how fast a function is in isolation. Before rewriting code for speed, check with a CPU profile (go test -cpuprofile cpu.out and go tool pprof cpu.out) that the function actually matters to your program.
A trustworthy benchmark:
Setup happens outside the loop, the body runs inside b.Loop(), results include -benchmem, and comparisons use several runs with benchstat.
Summary
Benchmarks are BenchmarkXxx(b *testing.B) functions with a for b.Loop() body, run with go test -bench=. -benchmem. Keep setup out of the loop, watch for impossibly fast results that mean work was optimized away, use b.Run for several input sizes, and compare versions with -count and benchstat.