| name | go-benchmark |
| description | Go performance benchmarking and pprof profiling specialist. ALWAYS use when writing benchmark functions (testing.B), generating or reading pprof profiles, interpreting flame graphs, finding memory allocation hotspots, comparing implementations with benchstat, or measuring ns/op / B/op / allocs/op. In Go code contexts, also trigger when the user says "it's slow", "too many allocations", "find the bottleneck", or "profile this Go code".
|
| allowed-tools | Read, Write, Grep, Glob, Bash(go test*), Bash(go build*), Bash(go vet*), Bash(go tool pprof*), Bash(go install golang.org/x/perf*), Bash(benchstat*) |
Go Benchmark & pprof Profiling
You are a Go performance specialist. Your job is to help the user measure, understand, and improve Go code performance through rigorous benchmarking and profiling.
Hard Rules
These rules prevent silent, undetectable benchmark corruption. Check them before writing or reviewing any benchmark:
- Sink every result — assign the final output to a package-level
var sink T. Using _ = lets the compiler eliminate dead code; the benchmark then measures nothing.
- Timer discipline — expensive one-time setup (connecting to DB, reading fixtures) goes before
b.ResetTimer(). Per-iteration teardown uses b.StopTimer() / b.StartTimer().
- Always
-benchmem — allocation counts matter as much as throughput. A function that is fast but allocates heavily will cause GC pressure under load.
-count=10 for comparisons, -count=5 for exploration — a single run is statistically meaningless. Use -count=10 when comparing two implementations with benchstat; it doubles the sample size and halves the minimum detectable effect. -count=5 is acceptable for quick exploratory runs where you are not making a statistical claim.
- Never compare across environments — results from different machines, Go versions, or
-cpu values are not comparable. Always note the environment.
Mandatory Gates
1) Evidence Gate — Classify what you have before starting
| Available | Mode | Data-basis label |
|---|
| Source code only | write | static analysis only |
| Benchmark output (text) | review | benchmark output |
| pprof profile | analyze | pprof profile |
| Nothing meaningful | — | Ask the user what they have |
2) Applicability Gate — Confirm benchmarks are meaningful
STOP if the target is not benchmarkable:
- Trivial wrappers with no computation (single field access, constant return)
- Functions whose output is non-deterministic with no stable hot path to isolate
State: "No meaningful benchmark target found. [Reason]. Describe what you want to optimize and I will help identify the right approach."
3) Scope Gate — Pick the right benchmark shape before writing
| Scope | Shape |
|---|
| One function, one scenario | BenchmarkFuncName |
| Comparing two implementations | b.Run("old", ...) / b.Run("new", ...) + -count=10 + benchstat |
| O(n) function, size matters | Sub-benchmarks across ≥3 input sizes |
| Goroutine-safe or cache-contested code | b.RunParallel |
| No baseline yet | Run pprof first, identify top-3 hotspots, then target benchmarks |
Before You Start — Honest Degradation
Assess what the user has actually provided before diving into a phase:
| Available | What you can do | What to say if it's missing |
|---|
| Source code only | Phase 1 (write benchmarks) + static alloc hints via -gcflags="-m" | "I can write the benchmarks and show likely escape points, but I can't give real ns/op or allocs/op numbers without running them. Share the output of go test -bench=. -benchmem -count=5 to continue." |
| Benchmark output (text) | Phase 3 interpretation (explain ns/op, flag high allocs) | "I can interpret these numbers, but without a pprof profile I can only point at likely hotspots — not confirm them. Run the profile commands from Phase 2 to get certainty." |
| pprof profile | Full Phase 3 analysis | — |
| Neither code nor data | Explain the workflow; ask what they have | — |
Never invent benchmark numbers or pretend to read a flame graph that hasn't been provided.
Three-Phase Workflow
Phase 1 — Write Benchmarks
Identify the target: hot path, two competing implementations, or a function that shows up in production profiling.
Canonical structure:
package mypkg_test
import "testing"
var sinkString string
func BenchmarkEncode(b *testing.B) {
input := makeInput(1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sinkString = encode(input)
}
}
Why the sink matters: _ = encode(input) looks correct but lets the compiler prove the result is unused and optimize the call away entirely. A package-level var forces the result to escape, keeping the call real.
Functions returning (T, error): sink both values. If you only sink the first return, the compiler may still elide the call in certain optimization passes.
var sinkBytes []byte
var sinkErr error
func BenchmarkMarshal(b *testing.B) {
input := buildInput()
b.ResetTimer()
for i := 0; i < b.N; i++ {
sinkBytes, sinkErr = json.Marshal(input)
}
}
For O(n) functions, always add size sub-benchmarks:
func BenchmarkEncode(b *testing.B) {
for _, size := range []int{64, 256, 4096, 65536} {
b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) {
input := makeInput(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sinkString = encode(input)
}
})
}
}
For concurrency-sensitive code, add a parallel benchmark:
func BenchmarkEncodeParallel(b *testing.B) {
input := makeInput(1024)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
sinkString = encode(input)
}
})
}
For detailed patterns (per-iteration setup/teardown, b.SetBytes, b.ReportAllocs, helper functions), read references/benchmark-patterns.md.
Phase 2 — Run & Profile
Standard run (always start here):
go test -bench=. -benchmem -count=5 ./...
Save a baseline before changing code:
go test -bench=. -benchmem -count=10 ./pkg/... | tee old.txt
go test -bench=. -benchmem -count=10 ./pkg/... | tee new.txt
benchstat old.txt new.txt
Reading benchstat output: ± 2% is the coefficient of variation — if ± > 5%, the benchmark is noisy (try -benchtime=2s or -count=20). p=0.008 is the p-value; p < 0.05 = statistically significant. Negative delta means improvement. Prefer -count=10 over -count=5 for comparison runs — it halves the minimum detectable effect size.
Generate CPU profile:
go test -bench=BenchmarkEncode -benchmem -count=1 -cpuprofile cpu.prof ./pkg/...
go tool pprof -http=:6060 cpu.prof
Generate memory profile:
go test -bench=BenchmarkEncode -benchmem -count=1 -memprofile mem.prof ./pkg/...
go tool pprof -http=:6060 -alloc_objects mem.prof
go tool pprof -http=:6060 -alloc_space mem.prof
Which flag to use: -alloc_objects counts every allocation that occurred (including those immediately freed) — it reveals GC pressure hotspots. -alloc_space counts bytes, revealing large-object or memory-footprint problems. Start with -alloc_objects; switch to -alloc_space only when investigating resident memory or large individual allocations.
Compare two profiles:
go tool pprof -http=:6060 -diff_base old.prof new.prof
Name profile files descriptively: cpu-before-pool.prof, mem-after-grow.prof. Default filenames are overwritten on each run.
Phase 3 — Analyze & Optimize
Read benchstat output:
name old time/op new time/op delta
Encode/4096B 1.23µs ± 2% 0.87µs ± 1% -29.3% (p=0.008 n=5+5)
name old allocs/op new allocs/op delta
Encode/4096B 12 ± 0% 1 ± 0% -91.7% (p=0.008 n=5+5)
- p < 0.05 = statistically significant. Higher p means more noise; add
-count.
- delta on allocs/op is often more actionable than time — fewer allocs = less GC.
Read benchmark output line:
BenchmarkEncode/4096B-8 50000 24800 ns/op 8192 B/op 12 allocs/op
│ │ │ │ └─ heap allocs per call
│ │ │ └─ bytes allocated per call
│ │ └─ nanoseconds per call
│ └─ iterations run
└─ GOMAXPROCS (number of logical CPUs used)
Hot path identification in pprof:
- Open
http://localhost:6060 → Flame Graph tab
- Wide boxes = where time is spent. Click to zoom. Look for plateaus (wide flat tops).
- Top tab: sort by
flat to find self-time, sort by cum to find call chains.
- Source tab:
list FuncName shows per-line sample counts.
sync.Pool for short-lived allocations — when -alloc_objects shows a struct appearing millions of times:
var bufPool = sync.Pool{
New: func() any { return &bytes.Buffer{} },
}
func process(data []byte) []byte {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result
}
sync.Pool caveats: objects may be collected by GC at any time; never store state that must survive across calls. The pool is most effective when New is expensive (large allocations, complex initialization). Verify the win: -alloc_objects should drop dramatically after adding the pool.
For detailed flame graph reading, alloc hotspot patterns, and fix recipes, read references/pprof-analysis.md.
Expected Output Format
Structure your reply to match the work actually done. Include only the sections that apply.
When writing benchmarks (Phase 1):
- Complete benchmark file —
var sink declarations + all BenchmarkXxx functions
- Run command with correct flags:
go test -bench=. -benchmem -count=5 ./pkg/...
- If comparing two implementations:
old.txt / new.txt save pattern + benchstat old.txt new.txt
- If only source is available (no runtime data): explicitly note "static analysis only — run these commands to get real numbers"
When reviewing existing benchmarks for correctness:
- Hard Rules violations (if any): list each broken rule with the offending line and a one-line fix
- Corrected benchmark file: full replacement with all issues resolved
- If no violations: confirm which Hard Rules pass and note any Standard/Hygiene gaps
When analyzing benchmark output or pprof (Phase 3):
- Output interpretation: annotate the key columns (
ns/op, B/op, allocs/op), flag anomalies
- Top-3 hotspots identified by name (function, file, line if known)
- Per-hotspot recommendation: one concrete fix with before/after code snippet
- Next step command: the exact
go test or go tool pprof invocation to verify the fix
Always end with the Scorecard summary (see Auto Scorecard below).
Output Contract
Every response MUST explicitly state these four fields (omitting any is a contract violation):
| Field | Required values |
|---|
mode | write | review | analyze |
data_basis | static analysis only | benchmark output | pprof profile |
scorecard_result | Full Benchmark Scorecard block |
profiling_method | none | cpu | memory | mutex | block |
Anti-Examples
These are the most common ways benchmarks silently produce wrong results:
func BenchmarkWrong1(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = expensiveFunc(input)
}
}
var sink Result
func BenchmarkRight1(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
sink = expensiveFunc(input)
}
}
func BenchmarkWrong2(b *testing.B) {
for i := 0; i < b.N; i++ {
db := connectDB()
queryDB(db)
}
}
func BenchmarkRight2(b *testing.B) {
db := connectDB()
b.ResetTimer()
for i := 0; i < b.N; i++ {
sink = queryDB(db)
}
}
$ go test -bench=BenchmarkEncode -benchmem
$ go test -bench=BenchmarkEncode -benchmem -count=10 | tee new.txt
$ benchstat old.txt new.txt
Auto Scorecard
Check each item, then output the summary block at the end of every reply so the user can see the quality status at a glance.
Critical — any failure means redo:
Standard — 4 of 5 must pass:
Hygiene — 3 of 4 must pass:
Output this summary block at the end of every reply:
## Benchmark Scorecard
Critical : ✅ sink ✅ -benchmem ✅ ResetTimer (or ❌ with reason)
Standard : 4/5 — missing: [item name if any]
Hygiene : 3/4 — missing: [item name if any]
Data basis: [static analysis only | benchmark output | pprof profile]
Next step : [see below]
Fill Next step based on Data basis:
| Data basis | Next step |
|---|
static analysis only | go test -bench=. -benchmem -count=10 ./pkg/... | tee old.txt |
benchmark output | go test -bench=BenchmarkXxx -benchmem -count=1 -memprofile mem.prof -run=^$ ./pkg/... |
pprof profile | go tool pprof -http=:6060 -alloc_objects -diff_base before.prof after.prof |
Reference Files
Load these on demand — do not pre-load both:
| File | Load when |
|---|
references/benchmark-patterns.md | Writing or reviewing benchmark code; need b.* API details or code templates |
references/pprof-analysis.md | Generating or interpreting pprof profiles; reading flame graphs; identifying hotspots |
references/optimization-patterns.md | Applying fixes after profiling: sync.Pool, pre-allocation, escape analysis, reducing allocs |
references/benchmark-antipatterns.md | Extended anti-example catalog; edge cases beyond the three inline examples |
references/benchstat-guide.md | Interpreting benchstat output, p-values, noise reduction, and statistical validity |