# Go benchmark output explained: ns/op, -count, -benchtime, -16 suffix > [!summary] TL;DR > `go test -run XXX -bench . -benchmem` prints one line per run: `18.53 ns/op, 64 B/op, 1 allocs/op`. The iteration count is chosen by the testing package, not by you: it ramps `b.N` up until the run fills `-benchtime` (default `1s`). The `-16` suffix is `GOMAXPROCS`. The same function written with `for b.Loop()` reports about 3 ns/op and `0 allocs/op` because the compiler keeps the buffer on the stack. `b.Loop` needs Go 1.24, but the `0 allocs/op` result needs Go 1.26: on 1.24 and 1.25 the loop version shows 1 alloc/op too. Verify with `-gcflags=-m`. ## Key points - A benchmark is a function named `BenchmarkXxx` that takes a `*testing.B` and does its work `b.N` times. A package-level `sink` keeps the compiler from throwing the work away. - `-bench .` selects benchmarks by regular expression; by default none run. `-run XXX` skips the tests (nothing is named `XXX`) so their timing does not muddy the result. `-benchmem` adds the `B/op` and `allocs/op` columns. - Every number in the source post is from one run on one machine: Apple M4 Max, Go 1.27.1, load average near 28. The outputs are being recaptured on a quiet machine with a reproduction kit and will be updated. - The testing package runs the function once with `b.N = 1`, predicts how many iterations would fill the benchmark time, adds 20% headroom, grows by at most 100x per step and stops at a billion. - `ns/op` is the run's total time divided by `b.N`, so every line is a mean. Each `-count` repetition is its own ramp and its own mean: one sample per line. - With `-count=3` on the same binary, the slowest sample was about 35% slower than the fastest. Go has no JIT warmup, but one iteration is still not representative: `-benchtime=1x` gave 2500, 1667 and 2000 ns/op (cold caches, the first allocation and timer granularity are the suspects). - `-benchtime` sets the target time. The default is `1s`; a value like `100x` means a fixed number of iterations. - The `-16` suffix is `GOMAXPROCS`; it is dropped when the value is 1. A missing suffix in someone else's output means they ran with `GOMAXPROCS=1`. - Go 1.24 added `testing.B.Loop` as the replacement for the `b.N` loop. It resets the timer on its first call and stops it when it returns false, so setup before it is not measured. The function runs once per `-count`, and the compiler keeps the arguments and results of calls in the body alive. - Go 1.26 changed how `b.Loop` keeps values alive: inlining in the body is no longer blocked. On Go 1.24 and 1.25 the loop version shows 1 alloc/op too. - The `b.Loop` version of the buffer benchmark reports about 3 ns/op and `0 allocs/op`, against about 20 ns/op and `1 allocs/op` for the `b.N` version. `-gcflags=-m` shows why: with `b.Loop`, `make([]byte, 64) does not escape`. - `-S` prints assembly: the `b.N` benchmark calls `runtime.makeslice`, the `b.Loop` benchmark does not. - Add `-cpuprofile` and read the profile with `go tool pprof -top`. The demo hash benchmark spent 92.86% of CPU in `crypto/internal/fips140/sha256.blockSHA2`; if your profile is dominated by `runtime` and `testing` frames, you may be timing the test framework. - Versions, links and commands checked on 2 October 2026. ## How to write and run a Go benchmark The benchmark allocates a 64-byte buffer and keeps it. It lives in a scratch module (`go mod init tiny`, one file, `tiny_test.go`): ```go package tiny import "testing" func makeBuffer(n int) []byte { return make([]byte, n) } var sink []byte func BenchmarkMakeBuffer(b *testing.B) { var s []byte for range b.N { s = makeBuffer(64) } sink = s } ``` The package-level `sink` is there so the compiler cannot throw the work away; [[resources/documentation/Go/Benchmarks/Go Benchmarks - Compiler Honesty and Dead Code Elimination|part 1 of the series]] is about what happens without it. Here is the line being decoded: ```text BenchmarkMakeBuffer-16 99632604 18.53 ns/op 64 B/op 1 allocs/op ``` The run command and its full output: ```console $ go test -run XXX -bench . -benchmem goos: darwin goarch: arm64 pkg: tiny cpu: Apple M4 Max BenchmarkMakeBuffer-16 99632604 18.53 ns/op 64 B/op 1 allocs/op PASS ok tiny 2.105s ``` The flags: `-bench .` selects benchmarks by regular expression, and by default none run. `-run XXX` does the same for tests; nothing is named `XXX`, so no test runs and the timing is not muddied by them. `-benchmem` adds the last two columns: bytes and heap allocations per iteration. The first number is how many times the loop ran, and the second, `ns/op`, is the time per iteration. Every number in the post is from one run on one machine: an Apple M4 Max, Go 1.27.1, with a load average near 28 when checked. The outputs are being recaptured on a quiet machine with a reproduction kit, and will be updated when the new numbers are in. ## How the b.N ramp picks the iteration count Nobody chose 99,632,604. The testing package runs the function once with `b.N = 1`, then predicts how many iterations would fill the benchmark time, adds 20% headroom, grows by at most 100x per step and stops at a billion. To watch it happen, a `fmt.Fprintln(os.Stderr, "b.N =", b.N)` was temporarily added to a copy of the benchmark: ```console $ go test -run XXX -bench Ramp 2>&1 | grep -E 'b\.N|ns/op' b.N = 1 BenchmarkRamp-16 b.N = 100 b.N = 10000 b.N = 1000000 b.N = 28893382 b.N = 47900115 47900115 22.76 ns/op ``` The function ran six times, and the printed line comes from the last run alone. The result is its total time divided by `b.N`, which is why `ns/op` is a mean. The ramp is not a fixed ladder: on an earlier run the last `b.N` was 99,632,604. The target is time, not a count, and `-benchtime` sets it. The default is `1s`; a value like `100x` means a fixed number of iterations. Every `-count` repetition is its own ramp and its own mean: ```console $ go test -run XXX -bench . -benchmem -count=3 BenchmarkMakeBuffer-16 96041296 23.08 ns/op 64 B/op 1 allocs/op BenchmarkMakeBuffer-16 61289114 23.37 ns/op 64 B/op 1 allocs/op BenchmarkMakeBuffer-16 100000000 17.26 ns/op 64 B/op 1 allocs/op ``` Same binary, same code, and the slowest sample is about 35% slower than the fastest. Each line is one mean, so each is one sample; [[resources/documentation/Go/Benchmarks/Go Benchmarks - Statistics and benchstat|part 2]] is about what to do with a handful of them. Go is compiled ahead of time, so there is no JIT warmup for the ramp to wait out. One iteration still is not representative: with `-benchtime=1x` the same benchmark reported 2500, 1667 and 2000 ns/op (not chased down; cold caches, the first allocation and timer granularity are the suspects). ## The -16 suffix is GOMAXPROCS The `-16` is `GOMAXPROCS`, and the suffix is dropped when the value is 1. With `-cpu 1,4` both forms appear: ```console $ go test -run XXX -bench 'MakeBuffer -cpu 1,4 BenchmarkMakeBuffer 83583531 18.06 ns/op BenchmarkMakeBuffer-4 100000000 20.72 ns/op ``` A missing suffix in someone else's output means they ran with `GOMAXPROCS=1`. ## testing.B.Loop (Go 1.24+) and the zero-allocs result Go 1.24 added `testing.B.Loop` as a replacement for the `b.N` loop. A second benchmark uses it, with no sink: ```go func BenchmarkMakeBufferLoop(b *testing.B) { for b.Loop() { makeBuffer(64) } } ``` `Loop` resets the timer on its first call and stops it when it returns false, so setup before it is not measured. The benchmark function runs once per `-count`, and the compiler keeps the arguments and results of calls in the body alive, so it cannot optimize away the whole loop body. (It has had bugs; part 1 lists them.) Go 1.26 changed how it keeps them alive: inlining in the body is no longer blocked. Here are both benchmarks together: ```console $ go test -run XXX -bench . -benchmem -count=3 BenchmarkMakeBuffer-16 92055165 21.90 ns/op 64 B/op 1 allocs/op BenchmarkMakeBuffer-16 88833778 19.98 ns/op 64 B/op 1 allocs/op BenchmarkMakeBuffer-16 100000000 23.30 ns/op 64 B/op 1 allocs/op BenchmarkMakeBufferLoop-16 494076908 3.145 ns/op 0 B/op 0 allocs/op BenchmarkMakeBufferLoop-16 548045512 2.975 ns/op 0 B/op 0 allocs/op BenchmarkMakeBufferLoop-16 562811305 2.071 ns/op 0 B/op 0 allocs/op ``` Same function, yet the loop version reports about 3 ns against about 20, and zero allocations. ## Ask the compiler and the profiler `-gcflags=-m` prints the compiler's inlining and escape-analysis decisions. The code lives in a `_test.go` file, so it has to be `go test`, and `-run XXX -bench XXX` compiles everything without running anything. The two lines about the buffer: ```console $ go test -gcflags=-m -run XXX -bench XXX . 2>&1 | grep 'make(\[\]byte, 64)' ./tiny_test.go:14:17: make([]byte, 64) escapes to heap ./tiny_test.go:21:13: make([]byte, 64) does not escape ``` Line 14 is the `b.N` benchmark: the buffer goes to the heap, hence `1 allocs/op`. Line 21 is the `b.Loop` one: the buffer stays on the stack, so the zero is honest. `-S` goes one level lower and prints the assembly. The `b.N` benchmark calls the allocator (the file path was trimmed; the same grep on `BenchmarkMakeBufferLoop STEXT` prints nothing): ```console $ go test -gcflags=-S -run XXX -bench XXX . 2>&1 | grep -A40 'tiny.BenchmarkMakeBuffer STEXT' | grep 'CALL.*makeslice' 0x0040 00064 (tiny_test.go:6) CALL runtime.makeslice(SB) ``` A benchmark tells you how long; a profile tells you where. The buffer benchmark is a poor subject, since its profile was dominated by runtime frames, so the series demo's hashing benchmark, `BenchmarkHash_BLoop`, is used instead. Add `-cpuprofile` to the run and read it with `go tool pprof -top`, keeping three rows: ```console $ go test -run XXX -bench BenchmarkHash_BLoop -benchmem -benchtime=2s -cpuprofile hash.prof . $ go tool pprof -top hash.prof flat flat% sum% cum cum% 1.95s 92.86% 92.86% 1.95s 92.86% crypto/internal/fips140/sha256.blockSHA2 0.09s 4.29% 97.14% 0.09s 4.29% runtime.pthread_cond_signal 0.02s 0.95% 98.10% 1.97s 93.81% crypto/internal/fips140/sha256.block ``` The benchmark spent 92.86% of its CPU in `blockSHA2`, the SHA-256 block function, which is what a hash benchmark should be doing. If the profile of your own benchmark is dominated by `runtime` and `testing` frames instead, you may be timing the test framework. ## FAQ ### What does the -16 suffix mean in Go benchmark output? It is `GOMAXPROCS`. The suffix is dropped when the value is 1, so a missing suffix means the run had `GOMAXPROCS=1`. Use `-cpu 1,4` to see both forms in one output. ### Where does the iteration count in go test -bench come from? Nobody chooses it. The testing package runs the function once with `b.N = 1`, predicts the iterations to fill `-benchtime`, adds 20% headroom, grows by at most 100x per step and stops at a billion. ### What does ns/op mean in a Go benchmark? It is the run's total time divided by `b.N`, so each line is a mean for one sample. With `-benchtime=1x` the same benchmark reported 2500, 1667 and 2000 ns/op. ### Why does my b.Loop benchmark report 0 allocs/op? With `for b.Loop()` the compiler keeps arguments and results alive but, since Go 1.26, no longer blocks inlining; the buffer stays on the stack and the zero is honest. On Go 1.24 and 1.25 the loop version shows 1 alloc/op. ### How do I check what the compiler did to my benchmark? Run `go test -gcflags=-m -run XXX -bench XXX .` to print inlining and escape analysis, and `-gcflags=-S` for assembly. The `b.N` buffer benchmark shows `make([]byte, 64) escapes to heap` and a `CALL runtime.makeslice`. ### How do I profile a Go benchmark? Add `-cpuprofile` to the run and read it with `go tool pprof -top`. If the profile is dominated by `runtime` and `testing` frames, you may be timing the test framework. ### How do I run the buffer benchmark myself? Make a directory, run `go mod init tiny`, put the two benchmarks in `tiny_test.go` and run `go test -run XXX -bench . -benchmem -count=3`. Then add `-benchtime=1x`, `-cpu 1,4` and `-gcflags=-m` one at a time and watch which column moves. On Go 1.26 or later your `allocs/op` column should match; your other numbers will differ. ## Related - [[resources/documentation/Go/Benchmarks/Go Benchmarks|Go Benchmarks]] (series hub) - [[resources/documentation/Go/Benchmarks/Go Benchmarks - Compiler Honesty and Dead Code Elimination|Compiler Honesty and Dead Code Elimination]] (part 1: what happens without a sink, and the b.Loop bugs) - [[resources/documentation/Go/Benchmarks/Go Benchmarks - Statistics and benchstat|Statistics and benchstat]] (part 2: what to do with a handful of samples) ## Sources - [Why Your Go Benchmarks Are Lying (talk, GopherCon UK 2026, GoLab 2026)](https://kakkoyun.me/talks/why-your-go-benchmarks-are-lying/) - [Why Your Go Benchmarks Are Lying (series)](https://kakkoyun.me/series/why-your-go-benchmarks-are-lying/) - [go command: testing flags](https://pkg.go.dev/cmd/go#hdr-Testing_flags) - [testing/benchmark.go: the b.N ramp (Go 1.27.1)](https://github.com/golang/go/blob/go1.27.1/src/testing/benchmark.go#L304-L326) - [testing/benchmark.go: ns/op is total time / b.N](https://github.com/golang/go/blob/go1.27.1/src/testing/benchmark.go#L555) - [testing/benchmark.go: the GOMAXPROCS suffix](https://github.com/golang/go/blob/go1.27.1/src/testing/benchmark.go#L671) - [Go 1.24 release notes](https://go.dev/doc/go1.24) - [Go 1.26 release notes](https://go.dev/doc/go1.26) - [testing.B.Loop package documentation](https://pkg.go.dev/testing#B.Loop) - [Demo repository at commit fdce88cc](https://github.com/kakkoyun/gopherconuk-26/tree/fdce88cc0ce129b7d2edfb1a20d02fe647c17eeb/talks/go-benchmarks-lying/demo)