16.5 Benchmarking and Performance Profiling
Measure before optimizing. Where a program spends its time often contradicts intuition, and a single
reading or two cannot establish that a change really helped. The Go toolchain offers three kinds of
tools for this. A benchmark measures how long one execution of a piece of code takes and how
much memory it allocates. benchstat applies a statistical test to decide whether the difference
between two sets of benchmark results exceeds the noise. Profiling samples the program and
attributes CPU time, memory allocation and blocking time to specific call stacks; the result is
called a profile.
This section covers how the testing package chooses the iteration count and keeps time, what
problems B.Loop (introduced in Go 1.24) solves, which statistical methods benchstat uses and how
many samples it needs, and how the runtime collects CPU, blocking and lock-contention profiles.
Execution tracing is covered in 16.3 Execution Tracing, runtime metrics in
16.6 Runtime Metrics, and the sampling principle of the heap profile in
12.8.5 The Heap Profile.
16.5.1 Iteration Counts and Timing
A benchmark function has the form func BenchmarkXxx(b *testing.B) and is run by go test -bench.
An operation that takes nanoseconds cannot be timed on its own: the resolution of the clock and the
cost of reading it are of the same order as the operation. The testing package therefore runs the
code under test b.N times and divides the total time by b.N to get the time per operation
(ns/op). The framework chooses b.N: it first runs with b.N = 1, then repeatedly estimates the
count for the next round until a round’s timing reaches -benchtime (1 second by default). The
estimate is made by predictN [Go Authors, 2026b]:
| |
The estimate extrapolates linearly from the speed of the previous round to the target time, plus a 20% margin. Because the previous timing may be off, each round grows by at most a factor of 100, by at least one iteration, and the total never exceeds 1 billion. The comment on multiplying before dividing records an integer-arithmetic detail: for a very fast operation the iteration count and the number of nanoseconds are close, so dividing first yields 0 or 1 and hides an order of magnitude of difference.
Each round is executed by runN. It first calls runtime.GC() to clear the garbage left by the
previous round, so that all rounds start from similar conditions, then resets the timer and calls
the whole benchmark function. Every change of b.N therefore runs the benchmark function from the
start, and any setup or cleanup code outside the loop is repeated with it. The following program
runs benchmarks with testing.Benchmark from main and records the value of b.N on each call of
the benchmark function:
| |
One run with Go 1.26.10 on arm64 produced the following output; iteration counts and times vary with the machine:
b.N form: benchmark function called 5 times, b.N values [1 100 10000 1000000 22206732]
final 22206732 iterations, 110 ns/op
b.Loop form: benchmark function called 1 times, b.N on entry [1]
final 13740837 iterations, 105 ns/op
The counts of the second through fourth rounds are exactly 100 times the previous round: those rounds take far less than 1 second, and the linear extrapolation exceeds the growth cap. The count of the fifth round comes from the extrapolation; its timing exceeds 1 second, which ends the measurement. The second half of the output is discussed in the next subsection.
The timed region is controlled by StartTimer, StopTimer and ResetTimer, which allow setup work
to be excluded. These methods carry an easily overlooked cost: to account for memory allocation,
both StartTimer and StopTimer call runtime.ReadMemStats
[Go Authors, 2026b], and each call stops the world
(12.8.4 Two Read Interfaces). If a loop body calls a
StopTimer/StartTimer pair on every iteration, the timing covers only the code under test, but
the wall-clock time is dominated by stop-the-world pauses. Since predictN works from the timing
rather than the wall-clock time, it keeps scaling up the iteration count, and the actual run time
far exceeds -benchtime. This is the origin of the report in Go issue #27217 that a tiny benchmark
using StopTimer never finishes [peterGo, 2018]; it and the earlier #20875
[Bleecher Snyder, 2017] are both still open.
16.5.2 B.Loop
The b.N form has two long-standing problems. The first is described above: the benchmark function
is called several times, so expensive setup is repeated and must be excluded by calling ResetTimer
by hand. The second is that the compiler may delete the code under test: once a function called in
the loop body is inlined and its result is unused, dead-code elimination can remove the whole call,
and the benchmark measures an empty loop. In 2023 Austin Clements proposed testing.B.Loop (Go
issue #61515), and Go 1.24 adopted it [Clements, 2023b;
Go Authors, 2025]. Its documentation explains how each problem is solved
[Go Authors, 2026b]:
| |
The timing problem is solved by moving the estimation of the iteration count inside the loop. The
first time B.Loop is called, it sets the target count to 1 and resets the timer; whenever the
iterations reach the target, it estimates a new target with the same predictN, until the timing
reaches -benchtime. All of this happens within a single call of the benchmark function, and
launch sees that the iterations are complete and does not call the benchmark function again. The
second half of the output above confirms this: the benchmark function is called only once, and the
b.N on entry is the 1 set by the initial call. Since Go 1.25, b.N is 0 inside the loop and the
total iteration count is written only after the loop ends, so that users do not confuse the two
forms and misuse is easier to spot [Clements, 2025]. To avoid adding
noticeable cost to each iteration, the fast path of Loop consists of one comparison and one
increment and can be inlined; when the timer is stopped, StopTimer sets the top bit of the
iteration counter, which forces the next call into the slow path, where it reports an error
[Go Authors, 2026b]:
| |
The interactive figure below applies the rules of predictN, launch and B.Loop to compute each
round’s iteration count in both forms, and draws the wall time each form spends on one time axis,
assuming every iteration costs the same. Drag the slider to see which rule sets the count of each
round. At 316 ms per iteration, for example, both forms run 1, 3 and 4 iterations, but the b.N
form reruns every round from scratch and takes about twice the wall time of the B.Loop form. Setup
before the loop widens the gap, since the b.N form redoes it in every round.
The dead-code problem is solved by a compiler transformation that recognizes only loops written
literally as for b.Loop() { ... }. Go 1.24 and 1.25 disabled inlining inside such loop bodies.
That does preserve the code under test, but it introduces another distortion. Go issue #73137 gives
an example: a function makes a slice internally and returns it to its caller. In a real program the
function is inlined, so escape analysis can keep the slice on the caller’s stack
(15.5 Escape Analysis); inside a B.Loop body it cannot be inlined, the
slice must be allocated on the heap, and the benchmark shows a heap allocation that does not exist
in the real program. Go 1.26 keeps inlining and instead wraps the function arguments, return values
and assigned variables in the loop body with runtime.KeepAlive, so their computation cannot be
removed [Shao, 2025]. The release notes therefore expect that all benchmarks can
be changed from the b.N form to the B.Loop form without side effects
[Go Authors, 2026a].
Keeping the code under test from being optimized away is a problem common to all microbenchmark
frameworks. Java’s JMH requires users to hand results to Blackhole.consume
[Shipilëv, n.d.], so the user marks explicitly which values must not be removed.
B.Loop leaves this work to the compiler, at the price of a restricted syntax: the loop condition
must be written literally as b.Loop(), and the transformation applies only to statements
syntactically inside the loop body.
16.5.3 Allocation Statistics and Custom Metrics
With -benchmem, or a call to b.ReportAllocs() in the benchmark function, the result gains two
columns: the bytes allocated per operation (B/op) and the number of objects (allocs/op). They come
from the differences in MemStats.TotalAlloc and MemStats.Mallocs between the two reads in
StartTimer and StopTimer [Go Authors, 2026b]. Both counters are
cumulative for the whole process, so allocations by other goroutines during the timed region are
counted as well.
allocs/op is often more stable than ns/op and easier to interpret. Every heap allocation adds work
for the garbage collector (Chapter 13 Garbage Collector); when allocs/op
changes from 0 to 1, a value has usually escaped to the heap, and the escape analysis output of
go build -gcflags=-m shows which line causes the allocation
(15.5 Escape Analysis).
When the built-in metrics are not enough, b.ReportMetric(n, unit) reports a custom metric, such as
the number of elements processed per operation or a cache hit rate. The unit must not contain
whitespace; if it has the same name as a built-in unit (such as allocs/op), it overrides the
built-in value, and setting ns/op to 0 suppresses the time report
[Go Authors, 2026b].
16.5.4 Deciding Whether a Difference Is Significant
The Benchmark Data Format and Noise
The output of go test -bench is itself a data format. In 2016 Russ Cox and Austin Clements fixed
it in a design document as the standard format for recording Go benchmark data. They wrote that they
could not find any standard format for recording raw benchmark data; even SPEC CPU2006 publishes
only results after analysis. In this format, configuration lines of the form key: value (such as
goos, goarch, pkg) describe the environment, and each result line consists of the benchmark
name, the iteration count and several “value unit” pairs
[Cox and Clements, 2016]. The testing package also appends the
runtime GOMAXPROCS to the name, as in -18, and omits it when it is 1
[Go Authors, 2026b]. With an agreed format, the programs that produce data
and the programs that analyze it can evolve independently.
Repeating the same benchmark on the same machine also gives fluctuating readings. Dynamic CPU
frequency scaling, thermal limits, background processes and hyper-threads sharing a physical core
all move readings by a few percent or more. Clements wrote perflock, which serializes benchmarks
on a shared machine with a system-wide lock and, in exclusive mode, also fixes the CPU frequency for
the duration of the run at a point 90% of the way between the lowest and highest frequency; once the
frequency is fixed, dynamic scaling no longer affects the readings
[Clements, n.d.]. Even so, noise cannot be eliminated entirely, and comparing
only a before reading and an after reading is unreliable.
The Statistics of benchstat
benchstat was originally written by Russ Cox and merged into golang.org/x/perf in 2017
[Cox, 2017b]. In 2023 Clements rewrote its statistics
[Clements, 2023a]. The old version first discarded outliers by the
interquartile range and then reported the mean and range of the remaining samples; but any outlier
rejection assumes a distribution of the data, here the normal distribution, which benchmark data
usually does not follow. The new version discards no samples, and so must switch to statistics that
are insensitive to outliers: each group reports the median and its 95% confidence interval. Two
groups are still compared with the Mann-Whitney U test, but now on the complete samples, which
raises the power of the test.
Mann and Whitney published the test in 1947 [Mann and Whitney, 1947];
it is equivalent to the rank-sum test proposed by Wilcoxon in 1945
[Wilcoxon, 1945]. Its idea is to look only at the ranks of the two samples
after sorting them together, not at the values themselves. If both samples come from the same
distribution (the null hypothesis), every arrangement of ranks is equally likely, and the p value is
the probability, under the null hypothesis, of an arrangement at least as extreme as the observed
one. It makes no assumption about the shape of the distribution, so it suits benchmark data with
outliers or even multimodal distributions. The default significance level α of benchstat is 0.05;
when the p value is below α it reports the percentage change, and otherwise it prints ~ to
indicate that no significant difference was detected [Go Authors, 2026g].
The resolving power of a rank test is limited by the sample count. With n samples per group, the
most extreme arrangement puts all samples of one group before all samples of the other, and under
the null hypothesis its probability is : 0.1 for n = 3, and about 0.029 for n = 4.
So with fewer than 4 samples per group, the p value cannot fall below 0.05 no matter how far apart
the groups are. The confidence interval of the median has a similar limit: with the minimum and
maximum of the sample as endpoints, the probability that the interval contains the population median
is , which is 0.9375 for n = 5 and exceeds 0.95 only at n = 6. benchstat
tabulates these two lower bounds and warns directly when the samples are insufficient
[Go Authors, 2026g].
A Complete Comparison
The following compares two implementations of string concatenation. The old one concatenates with
+=, allocating a new string on every concatenation; the new one first computes the total length
and then writes everything at once with a strings.Builder:
| |
BenchmarkJoin concatenates 32 words repeatedly with for b.Loop(); BenchmarkCount measures
strings.Count, which is unrelated to this change, as a control. The benchstat documentation
recommends alternating runs of the two versions so that noise is spread evenly over both samples,
rather than running the old version 10 times in a row and then the new version 10 times in a row
[Go Authors, 2026g]. Compile the two test programs in advance with
go test -c and run them alternately. The following uses Go 1.26.10 and revision be2c69f of
golang.org/x/perf:
| |
Many other processes were running on the machine during this run, so the confidence intervals of the
times are unusually wide, which shows well what the statistical test does. The two samples of Join
do not overlap at all, and even with this much noise the p value is close to 0. The medians of
Count differ by about 12%, yet the p value is 0.631, and benchstat concludes that the difference
cannot be told apart from noise. The ± 35% is the width of the 95% confidence interval of the
median relative to the median, not the dispersion of the samples. The geometric mean in the last row
of each table summarizes the change over all benchmarks: if one of n benchmarks becomes twice as
large, the geometric mean becomes times as large. The geometric mean requires every
value to be greater than 0; Count allocates no memory, so the geometric means in the last two
tables carry a warning. The allocation statistics have no noise: the old version allocates 31 times
to concatenate 32 words (the left operand of the first concatenation is the empty string, so the
runtime returns the right operand directly without allocating), and the new version allocates once,
in Grow.
With only 3 runs per group, benchstat reports the two lower bounds derived in the previous
subsection:
| |
The benchstat documentation also warns of two statistical traps
[Go Authors, 2026g]. First, the sample count should be fixed in advance (at
least 10, preferably 20), and the benchmark should not be rerun repeatedly when the result is not
significant until it becomes so: α = 0.05 means that even with no difference at all there is a 5%
chance of reporting one, and repeated reruns will eventually hit it. Second, for the same reason,
when many benchmarks are compared at once, about 5% of them should be expected to show a significant
change even if the code did not change.
16.5.5 How the Runtime Collects Profiles
A benchmark tells us how fast a piece of code is; profiling answers which call stacks a running
program spends its time and memory on. runtime/pprof predefines the following profiles
[Go Authors, 2026c]:
| |
The CPU profile is not among them. It writes data to an io.Writer continuously while profiling, so
it has its own interface, StartCPUProfile and StopCPUProfile. Although goroutineleak appears
in the documentation, in Go 1.26 it is registered only when building with
GOEXPERIMENT=goroutineleakprofile [Go Authors, 2026a;
Go Authors, 2026f]. It uses the garbage collector to find goroutines that
can never be woken up, and it becomes officially available in Go 1.27
[Go Authors, 2026h]; the detection principle is in
16.1 Runtime Deadlock Detection. The heap and allocs profiles sample by
recording on average once per 512 KiB allocated; the principle is in
12.8.5 The Heap Profile. The following discusses how CPU,
blocking and lock-contention profiles are collected.
The CPU Profile
The CPU profile relies on timer-signal sampling: at fixed intervals of CPU time the kernel sends the
process a SIGPROF, and the signal handler records the current call stack. The fraction of samples
in which a function appears approximates the fraction of CPU time it uses. The sampling frequency is
fixed at 100 Hz, and the comment in StartCPUProfile gives the reason
[Go Authors, 2026c]:
| |
From signal to profile file, the data passes through the following steps:
flowchart TB
T["Kernel timer<br/>Linux: per-thread timer_create<br/>Other Unix systems: process-wide setitimer"] -->|SIGPROF| H["Signal handler sigprof<br/>unwinds the current call stack"]
H -->|cpuprof.add| B["profBuf<br/>single-writer, single-reader lock-free ring buffer"]
B -->|readProfile| W["profileWriter goroutine"]
W --> P["gzip-compressed protobuf profile<br/>written to an io.Writer"]A signal handler cannot allocate memory or acquire a lock that may already be held, so sigprof
only writes the call stack into profBuf. profBuf is a lock-free ring buffer that allows exactly
one writer and one reader; the writer may be a signal handler with no user goroutine. Besides the
call stack, each record carries a pointer to the profiling labels of the current goroutine (set by
pprof.Do). Cox reimplemented this buffer in 2017 to support labels, shipped in Go 1.9
[Cox, 2017a]. The reader, profileWriter, is an ordinary goroutine that
continually takes records from the buffer and aggregates them into a profile in protobuf format.
This format was introduced by Michael Matloob in Go 1.8 to replace the earlier legacy pprof format:
the protobuf format is easier to extend and is the format preferred by the pprof tool, and profiling
labels can be written only in this format [Matloob, 2016].
The choice of timer was corrected once. setitimer counts the CPU time consumed by the whole
process, but Linux settles a process’s CPU time only periodically (usually every 4 ms); if more than
one signal should be sent between two settlements (every 10 ms at the default 100 Hz), the kernel
can often deliver only one of them. As a result, when a program runs more than about 2.5 goroutines
in parallel, the CPU profile systematically undercounts. Rhys Hiltner reported this in Go issue
#35057 and, in Go 1.18, switched to creating one timer with timer_create for each thread managed
by the runtime; each timer counts that thread’s own CPU time, and the signal is sent directly to the
thread [Hiltner, 2021]. Threads created by cgo are not managed by the runtime
and still need the process-wide setitimer to cover them, and validSIGPROF deduplicates by the
origin of the signal: a thread that has an M and its own timer accepts only signals from
timer_create, and a thread without an M accepts only signals from setitimer
[Go Authors, 2026c]. Per-thread timing brings a new bias: a short-lived
thread, or one that runs only briefly now and then (such as a garbage collection worker thread on an
idle system), may stop running before the first signal arrives. For this reason the first expiry of
each timer is chosen uniformly at random between zero and one full sampling period.
The Blocking Profile and the Mutex Profile
Both profiles are off by default. They need to take timestamps on the slow paths of synchronization
primitives, which costs more than CPU and heap sampling, and the user decides whether it is worth
it: runtime.SetBlockProfileRate turns on the blocking profile and
runtime.SetMutexProfileFraction turns on the mutex profile. The blocking profile was added by
Dmitriy Vyukov in Go 1.1 [Vyukov, 2012], and the mutex profile by Peter
Weinberger in Go 1.8 [Weinberger, 2016].
The two record different call stacks. The blocking profile records the stack of the waiter:
however long a goroutine blocks on a channel, select, sync.Cond or a mutex, that time is charged
to the call stack where it blocked. The mutex profile records the stack of the holder: when a
waiter is woken, semrelease1 charges the waiting time to the goroutine that performed the unlock
[Go Authors, 2026d]:
| |
If N more goroutines are still queued after the one being woken, the unlocker is charged not only the waiting time of the woken goroutine but also the estimated waiting time of the rest, computed as the average of the head and tail waiting times multiplied by N, so that a critical section that blocks many goroutines looks more expensive. The blocking profile thus answers “where is the waiting,” and the mutex profile answers “whose critical section causes the waiting.”
Accumulating by the number of queued goroutines was added only in Go 1.22. The mutex profile was
meant to follow the lock-contention profiler that Mike Burrows designed for Google’s C++ library
(today’s Abseil), which computes blocking time per thread: 100 threads blocked on the same lock for
10 ms at the same time count as 1000 ms of contention. The original Go implementation missed this
and counted only 10 ms. In 2023 Russ Cox changed it to accumulate by the number of blocked
goroutines, while still charging the time to the unlock that caused the backlog rather than to the
later unlocks affected by the congestion [Cox, 2023]. Locks inside the runtime have
also been counted in the mutex profile since Go 1.22, but at first only against a placeholder frame,
runtime._LostContendedRuntimeLock [Go Authors, 2024]; only from Go 1.25
are they treated like sync.Mutex and charged to the end of the critical section that caused the
wait, that is, to the unlocker’s call stack [Hiltner, 2025].
The following program has holder hold the lock for 50 ms while waiter waits for it, and then
reads out the call stacks belonging to package main from the two profiles:
| |
The program skips records whose top frame is main.main, which are main’s own blocking on the
channel and the WaitGroup. The output is:
block profile: [main.waiter]
mutex profile: [main.holder main.main.func1]
The same wait is attributed to waiter, which waits for the lock, in one profile, and to holder,
which holds it, in the other.
The two profiles also sample differently, but both weight the samples so that the total time in the profile is an unbiased estimate of the actual total [Go Authors, 2026d]:
| |
The blocking profile samples by time: every event that blocks for at least rate is recorded, and a
shorter event is recorded with probability “blocking time / rate” and scaled up by the inverse of
that probability. Before Go 1.17, a short event that was sampled was counted once as it was, so the
profile favored few long blockings and underestimated many short ones; Felix Geisendörfer fixed this
bias on a suggestion from Vyukov [Geisendörfer, 2021]. The mutex profile
samples by event: on average one in every rate contention events is recorded, scaled up by a
factor of rate. The heap profile recovers the allocation volume from the sampling probability
(12.8.5 The Heap Profile) and uses the same weighting by
inverse probability.
The Goroutine Profile and Stack Depth
The goroutine profile needs the call stacks of all goroutines at a single instant. Early implementations walked all goroutines while the world was stopped, so the pause grew in proportion to the number of goroutines. Go 1.19 changed this to do only a fixed amount of work during the stop-the-world pause, including setting up a barrier: after the program resumes, if a goroutine is scheduled before it has been recorded, the scheduler first records its call stack as of the stop time and then lets it run; the profiler records the remaining goroutines one by one while the program runs, and finally stops the world once more to remove the barrier. The pause no longer grows with the number of goroutines, at the price of more CPU time spent in profiling itself and a longer wall-clock time for the call [Hiltner, 2022]. In addition, all profiles other than the CPU profile originally recorded only the 32 innermost frames of a call stack, which were often truncated in production; Go 1.23 raised the limit to 128 frames, the same as execution tracing [Geisendörfer, 2024].
16.5.6 From Profiles to Conclusions
The standard format of a profile is gzip-compressed protobuf, read by go tool pprof.
runtime/pprof writes profiles to a file, and net/http/pprof serves the same data over HTTP under
/debug/pprof/, so it can be fetched directly from a running service [Cox, 2011;
Go Authors, 2026e]:
| |
/debug/pprof/profile?seconds=30 collects a 30-second CPU profile; for cumulative profiles such as
heap, block and mutex, the seconds parameter returns the delta over that period instead.
Since Go 1.22 these paths accept only GET requests [Go Authors, 2026e].
These endpoints expose the internal state of the program, and normally they should listen only on a
local address or require authentication.
| |
go tool pprof -http opens a web interface, which since Go 1.26 shows a flame graph by default
[Go Authors, 2026a]. The flame graph was proposed by Brendan Gregg
[Gregg, 2016; Gregg, n.d.]. Each rectangle is a frame of the
call stack, stacked level by level according to the calling relationship, with a width proportional
to the number of samples that contain the frame; the horizontal order has no meaning in time.
Gregg’s original layout puts the root at the bottom with call stacks growing upward, and the frames
on the top edge are the functions executing on the CPU when the sample was taken; the flame graph in
the pprof web interface puts the root at the top and lets callees grow downward, the opposite
direction [Go Authors, 2026c]. In either direction the height reflects only
the depth of the call stack, and when looking for hot spots one should look for wide,
flat “plateaus” on the leaf side, that is, functions that account for many samples themselves. Flame
graphs do not depend on a particular profiler; call stacks collected by DTrace or Linux perf can
be presented the same way.
Profiles can also leave the developer’s machine and become part of infrastructure. Google-Wide
Profiling at Google collects profiles continuously and at a low rate on the machines of its data
centers and stores them centrally, so performance analysis can cover long-term data from the whole
fleet [Ren et al., 2010]. The low overhead of sampling is what makes
continuous profiling possible. Profiles can also be handed to the compiler: since Go 1.21, a
default.pgo in the main package directory is used by default for profile-guided optimization
(PGO), on whose basis the compiler inlines and devirtualizes hot spots more aggressively. The Go
1.21 release notes report a typical gain of 2% to 7% [Go Authors, 2023]; Go
1.22 let PGO devirtualize more calls, and its release notes report the gain rising to 2% to 14%
[Go Authors, 2024]. The mechanism is in
15.3 The Optimizer.
Profiling answers where time goes overall. When the problem is occasional tail latency, the statistical aggregation in a profile hides the experience of individual requests; there an execution trace, which records scheduling, garbage collection and system call events in time order, is needed (16.3 Execution Tracing).
16.5.7 Summary
Benchmarks, statistical tests and profiling answer “how fast,” “did it really change” and “where
does it go.” The testing package determines the iteration count by exponential growth followed by
extrapolation; B.Loop moves this process inside the loop and prevents the code under test from
being deleted with a compiler transformation, and Go 1.26 makes that transformation stop interfering
with inlining. benchstat handles benchmark data that does not follow a normal distribution with
the median, confidence intervals and the Mann-Whitney U test; the nature of the rank test requires
at least 4 samples per group, and the confidence interval of the median requires at least 6. The
runtime collects the CPU profile with a 100 Hz timer signal, using per-thread timers on Linux to
avoid undercounting on multiple cores; the blocking profile and the mutex profile charge the same
wait to the waiter and to the holder respectively, and both weight by sampling probability to stay
unbiased.
Further Reading
- [Bleecher Snyder, 2017] Josh Bleecher Snyder. Go issue #20875. testing: consider calling ReadMemStats less during benchmarking. 2017. https://go.dev/issue/20875
- [Clements, 2023a] Austin Clements. cmd/benchstat: new version of benchstat. golang.org/x/perf commit 02c55175bb, 2023. https://go.dev/cl/309969 Switches to the median, the confidence interval of the median and a U test over all samples, and no longer discards outliers.
- [Clements, 2023b] Austin Clements. Go issue #61515. testing: add testing.B.Loop for iteration. 2023. https://go.dev/issue/61515
- [Clements, 2025] Austin Clements. testing: separate b.Loop counter from b.N. Go commit 9003154206, 2025. https://go.dev/cl/659655 Shipped in Go 1.25.
- [Clements, n.d.] Austin Clements. perflock. https://github.com/aclements/perflock Runs benchmarks serially under a system-wide lock and adjusts the CPU frequency for the duration of the run.
- [Cox, 2011] Russ Cox. Profiling Go Programs. The Go Blog, 2011 (updated by Shenghou Ma in 2013). https://go.dev/blog/pprof Uses a benchmark program as the example to show how CPU and heap profiles are collected and analyzed.
- [Cox, 2017a] Russ Cox. runtime: new profile buffer implementation supporting label pointers. Go commit b788fd80e6, 2017. https://go.dev/cl/36713 Shipped in Go 1.9.
- [Cox, 2017b] Russ Cox. cmd/benchstat: import from rsc.io/benchstat. golang.org/x/perf commit 1da04cc6cd, 2017. https://go.dev/cl/35503
- [Cox, 2023] Russ Cox. runtime: change mutex profile to count every blocked goroutine. Go commit 1c00354013, 2023. https://go.dev/cl/506415 Shipped in Go 1.22; fixes Go issue #61015. The commit message explains the origin of the mutex profile in the Abseil lock profiler.
- [Cox and Clements, 2016] Russ Cox, Austin Clements.
Proposal: Go Benchmark Data Format. Go design document, 2016.
https://go.googlesource.com/proposal/+/master/design/14313-benchmark-format Fixes the output of
go test -benchas the standard format for recording benchmark data. - [Geisendörfer, 2021] Felix Geisendörfer. runtime/pprof: fix block profile bias. Go commit 0ae9c3b98b, 2021. https://go.dev/cl/299991 Shipped in Go 1.17; fixes Go issue #44192.
- [Geisendörfer, 2024] Felix Geisendörfer. runtime: increase profiling stack depth to 128. Go commit 1b9dc3e178, 2024. https://go.dev/cl/572396 Shipped in Go 1.23.
- [Go Authors, 2023] The Go Authors. Go 1.21 Release Notes. 2023.
https://go.dev/doc/go1.21
PGO becomes generally available;
-pgodefaults toauto. - [Go Authors, 2024] The Go Authors. Go 1.22 Release Notes. 2024. https://go.dev/doc/go1.22 The mutex profile accumulates by the number of blocked goroutines and starts counting locks inside the runtime; the PGO gain rises to 2% to 14%.
- [Go Authors, 2025] The Go Authors. Go 1.24 Release Notes. 2025.
https://go.dev/doc/go1.24
Introduces
testing.B.Loop. - [Go Authors, 2026a] The Go Authors. Go 1.26 Release
Notes. 2026. https://go.dev/doc/go1.26
B.Loopno longer interferes with inlining; the experimental goroutine leak profile; the pprof web interface shows a flame graph by default. - [Go Authors, 2026b] The Go Authors. src/testing/benchmark.go
(
predictN,launch,runN,Loop,StartTimer,StopTimer). Go 1.26 source. https://github.com/golang/go/blob/release-branch.go1.26/src/testing/benchmark.go - [Go Authors, 2026c] The Go Authors. src/runtime/pprof/pprof.go
(predefined profiles,
StartCPUProfile,profileWriter), src/runtime/cpuprof.go, src/runtime/profbuf.go, src/runtime/os_linux.go (validSIGPROF), src/cmd/vendor/github.com/google/pprof/internal/driver/html/stacks.js (drawing direction of the flame graph). Go 1.26 source. https://github.com/golang/go/tree/release-branch.go1.26/src/runtime - [Go Authors, 2026d] The Go Authors.
src/runtime/mprof.go (
blocksampled,mutexevent,saveBlockEventStack), src/runtime/sema.go (semrelease1). Go 1.26 source. https://github.com/golang/go/tree/release-branch.go1.26/src/runtime - [Go Authors, 2026e] The Go Authors. Package runtime/pprof, net/http/pprof. https://pkg.go.dev/runtime/pprof ; https://pkg.go.dev/net/http/pprof The meaning of each profile, and the parameters of the HTTP endpoints.
- [Go Authors, 2026f] The Go Authors. testing.B.Loop (introduced
in Go 1.24). https://pkg.go.dev/testing#B.Loop ; for the
goroutineleakprofile seesrc/runtime/pprof/pprof.go(in Go 1.26 it must be built withGOEXPERIMENT=goroutineleakprofile; generally available from Go 1.27). - [Go Authors, 2026g] The Go Authors. benchstat.
https://pkg.go.dev/golang.org/x/perf/cmd/benchstat ; for the implementation see
golang.org/x/perf/benchmath(Mann-Whitney U test). This section follows revision be2c69f: the median with a 95% confidence interval, the default α = 0.05, the lower bounds on sample counts and the usage advice. - [Go Authors, 2026h] The Go Authors. Go 1.27 Release
Notes. 2026. https://go.dev/doc/go1.27 The goroutine leak profile becomes generally available,
and the
goroutineleakprofileexperiment is removed. - [Gregg, 2016] Brendan Gregg. “The Flame Graph.” Communications of the ACM, 59(6): 48–57, 2016. https://doi.org/10.1145/2909476 A systematic introduction to the notation by its originator.
- [Gregg, n.d.] Brendan Gregg. Flame Graphs. https://www.brendangregg.com/flamegraphs.html ; Systems Performance. 2nd ed. Pearson, 2020.
- [Hiltner, 2021] Rhys Hiltner. runtime: profile with per-thread timers on Linux. Go commit 8cfd8c3db8, 2021. https://go.dev/cl/324129 Shipped in Go 1.18; corresponds to Go issue #35057.
- [Hiltner, 2022] Rhys Hiltner. runtime: decrease STW pause for goroutine profile. Go commit 52bd1c4d6c, 2022. https://go.dev/cl/387415 Shipped in Go 1.19.
- [Hiltner, 2025] Rhys Hiltner. runtime: blame unlocker for mutex delay. Go commit 4d0f955a5e, 2025. https://go.dev/cl/667615 Shipped in Go 1.25; fixes Go issue #66999.
- [Mann and Whitney, 1947] H. B. Mann, D. R. Whitney. “On a
Test of Whether one of Two Random Variables is Stochastically Larger than the Other.” The Annals
of Mathematical Statistics, 18(1): 50–60, 1947. https://doi.org/10.1214/aoms/1177730491 The
original paper of the U test used by
benchstat. - [Matloob, 2016] Michael Matloob. runtime/pprof: output CPU profiles in pprof protobuf format. Go commit 76f12cdaa2, 2016. https://go.dev/cl/33071 Shipped in Go 1.8; fixes Go issue #16093 (runtime/pprof: generate profiles in gzipped proto format).
- [peterGo, 2018] peterGo. Go issue #27217. testing: tiny benchmark with StopTimer runs forever. 2018. https://go.dev/issue/27217
- [Ren et al., 2010] Gang Ren, Eric Tune, Tipp Moseley, Yixin Shi, Silvius Rus, Robert Hundt. “Google-Wide Profiling: A Continuous Profiling Infrastructure for Data Centers.” IEEE Micro, 30(4): 65–79, 2010. https://doi.org/10.1109/MM.2010.68 The founding work on continuous profiling.
- [Shao, 2025] Junyang Shao. cmd/compile: change testing.B.Loop keep alive semantic. Go commit 22f24f90b5, 2025. https://go.dev/cl/716660 Shipped in Go 1.26; fixes Go issue #73137.
- [Shipilëv, n.d.] Aleksey Shipilëv. JMH (Java Microbenchmark
Harness). https://openjdk.org/projects/code-tools/jmh/
Blackholecounters dead-code elimination, the same problem thatB.Loopsolves. - [Vyukov, 2012] Dmitriy Vyukov. pprof: add goroutine blocking profiling. Go commit 4cc7bf326a, 2012. https://go.dev/cl/6443115 Shipped in Go 1.1.
- [Weinberger, 2016] Peter Weinberger. runtime: Profile goroutines holding contended mutexes. Go commit ca922b6d36, 2016. https://go.dev/cl/29650 Shipped in Go 1.8.
- [Wilcoxon, 1945] Frank Wilcoxon. “Individual Comparisons by Ranking Methods.” Biometrics Bulletin, 1(6): 80–83, 1945. https://doi.org/10.2307/3001968
- This book: 15.3 The Optimizer, 16.3 Execution Tracing, 16.1 Runtime Deadlock Detection, Chapter 13 Garbage Collector, 15.5 Escape Analysis.