# 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