29.6 b.ReportAllocs and b.ResetTimer
Right, let’s talk about making your benchmarks actually mean something. You’ve probably written a simple one, run it with go test -bench=., and stared at a number. But that number is a liar. It’s a filthy, opportunistic liar that will include all the setup time you did outside the loop, the time it took to generate your test data, and the cost of that one fmt.Printf you left in there “just for debugging.” We’re not here to be lied to. We’re here to get the truth, and for that, we have b.ResetTimer() and b.ReportAllocs().
Think of the benchmark function as a stopwatch. When you start it, the stopwatch is already running. So if you do this:
func BenchmarkSomethingExpensive(b *testing.B) {
// This setup is brutally slow
expensiveThing := doSomethingThatTakesFiveSeconds()
for i := 0; i < b.N; i++ {
doTheActualThingWeCareAbout(expensiveThing)
}
}
Your benchmark results will include those five seconds of setup for every single run b.N is set to. The benchmarking framework is clever, but it’s not that clever. It doesn’t know your setup isn’t part of the code you’re trying to measure. You have to tell it. This is where you stop the stopwatch for the setup and start it again for the real work.
b.ResetTimer(): Your Stopwatch’s Reset Button
You call b.ResetTimer() to tell the benchmark runner, “Ignore everything that happened up to this point. Wipe the slate clean. Start timing now.” You use it after any expensive setup that is not part of the operation you’re trying to measure.
func BenchmarkFileRead(b *testing.B) {
b.StopTimer() // You can even stop it manually first, but ResetTimer does it for you.
// This is gnarly setup we don't want to time
hugeData := make([]byte, 1024*1024*100) // 100 MB of data
rand.Read(hugeData) // This takes a moment
filename := "/tmp/mybigtestfile"
os.WriteFile(filename, hugeData, 0644)
file, _ := os.Open(filename)
defer file.Close()
defer os.Remove(filename)
b.ResetTimer() // "Forget all that I/O and allocation time, start NOW."
for i := 0; i < b.N; i++ {
file.Seek(0, 0)
io.Copy(io.Discard, file) // Time this, and only this.
}
}
You’ll see ResetTimer used in almost every non-trivial benchmark. It’s the first line of defense against benchmarking your setup code.
b.ReportAllocs(): The Memory Truth Serum
Now, let’s say your function is fast. Really fast. The ns/op (nanoseconds per operation) looks great. But you have a sneaking suspicion it’s achieving that speed by being a memory pig, allocating all over the place. This is where b.ReportAllocs() comes in. It’s a simple call that tells the benchmark to also report memory allocations.
func BenchmarkAllocationHeaven(b *testing.B) {
b.ReportAllocs() // "I want to see the dirty details."
for i := 0; i < b.N; i++ {
// This function returns a new slice every time. Terrible for allocations.
data := createANewSliceEveryTime(i)
_ = data
}
}
When you run this, you’ll get beautiful, horrifying output like:
BenchmarkAllocationHeaven-16 123456 9120 ns/op 16384 B/op 1 allocs/op
That B/op (bytes per operation) and allocs/op are the real story. Your function might be fast in terms of CPU time, but if it’s allocating 16KB on every single call, that’s putting immense pressure on the garbage collector. In a high-throughput server, that GC pressure will be your undoing. ReportAllocs exposes this cost instantly. It’s the difference between “this is fast in a vacuum” and “this will perform well in the real world.”
Common Pitfalls and The Golden Rule
The biggest pitfall is forgetting to use them. It’s so easy to benchmark a function and miss the huge cost of the setup or the silent death by a thousand allocations.
A more subtle pitfall is calling ResetTimer multiple times or in the loop. Don’t. Call it once, after your setup, before the loop. The benchmark runner is not designed for you to flip the timer on and off like a light switch.
The golden rule is this: Your benchmark loop should contain only the operation you want to measure. Everything else is setup and belongs on the other side of a ResetTimer(). And if you care about the production performance of your code (you should), always use ReportAllocs(). It costs nothing and gives you invaluable data. It turns a simple speed check into a holistic performance profile. Now go forth and benchmark what you mean to benchmark, not what you accidentally wrote.