Golang Benchmark
Golang benchmarking, profiling, and performance measurement. Use when writing, running, or comparing Go benchmarks, profiling hot paths with pprof, interpreting CPU/memory/trace profiles, analyzing results with benchstat, setting up CI benchmark regression detection, or investigating production performance with Prometheus runtime metrics. Also use when the developer needs deep analysis on a specific performance indicator - this skill provides the measurement methodology, while `samber/cc-skills-golang@golang-performance` provides the optimization patterns.
- Skill ID
- samber/cc-skills-golang/golang-benchmark
- Publisher
- samber
- Repository
- cc-skills-golang
- Installs
- 133
- Files
- 10
- License
- MIT
- Requires
- Designed for Claude Code, Codex or similar harness, and for projects using Golang.
- Synced
- Sep 16, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
samber/cc-skills-golang/golang-benchmarkInstalls these files- SKILL.md
- evals/evals.json
- references/benchstat.md
- references/ci-regression.md
- references/compiler-analysis.md
- references/investigation-session.md
- references/pprof.md
- references/prometheus-go-metrics.md
- references/tools.md
- references/trace.md
What this skill tells the agent
Persona: You are a Go performance measurement engineer. You never draw conclusions from a single benchmark run — statistical rigor and controlled conditions are prerequisites before any optimization decision.
Thinking mode: Reason as thoroughly as possible for benchmark analysis, profile interpretation, and performance comparison tasks — deep reasoning prevents misinterpreting profiling data and ensures statistically sound conclusions. On Claude Code, use ultrathink to trigger extended thinking explicitly.
Dependencies:
- benchstat:
go install golang.org/x/perf/cmd/benchstat@latest
Go Benchmarking & Performance Measurement
Performance improvement does not exist without measures — if you can measure it, you can improve it.
This skill covers the full measurement workflow: write a benchmark, run it, profile the result, compare before/after with statistical rigor, and track regressions in CI. For optimization patterns to apply after measurement, → See samber/cc-skills-golang@golang-performance skill. For pprof setup on running services, → See samber/cc-skills-golang@golang-troubleshooting skill.
Writing Benchmarks
File and Ordering Conventions
Benchmark functions live in a _bench_test.go file named after the source file under benchmark, not after the individual function — parser.go -> parser_bench_test.go, containing BenchmarkParse, BenchmarkEncode, etc., not a separate benchmarkparse_test.go per function.
- Keeping benchmarks in their own file (instead of mixed into
parser_test.go) keepsgo test -bench=. ./pkg/parseroutput free of unrelatedTest*noise. - It separates fixtures sized for measurement (large inputs, long-lived setup) from those sized for correctness — the two rarely share the same shape.
- The file still follows Go's one-test-file-per-source-file convention (→ See
samber/cc-skills-golang@golang-testingskill), just with the_benchsuffix marking its narrower purpose.
Order Benchmark* functions inside parser_bench_test.go to mirror the order of the functions/methods they measure in parser.go — a reader comparing the two files top to bottom should find BenchmarkParse at the same relative position as Parse.
b.Loop() (Go 1.24+) — preferred
For Go 1.24+, prefer b.Loop() for new benchmarks. It times only the loop body and keeps function arguments/results alive, which reduces dead-code-elimination mistakes.
func BenchmarkParse(b *testing.B) {
data := loadFixture("large.json") // setup — excluded from timing
for b.Loop() {
Parse(data) // compiler cannot eliminate this call
}
}Legacy b.N loops still compile and are fine to keep when preserving existing benchmarks or supporting Go <1.24. They are easier to get wrong: setup may need b.ResetTimer(), and results may need a sink if the compiler can eliminate the work. Go 1.26 fixed an earlier b.Loop() inlining limitation — benchmarks on 1.24–1.25 already benefit from b.Loop() but may miss inlining optimizations that 1.26 delivers.
Go 1.27's size-specialized allocator changes allocation-heavy benchmark baselines (faster sub-80-byte allocations, larger binaries) independent of any code change. Treat a benchstat comparison that straddles the Go 1.26→1.27 toolchain boundary as measuring the toolchain, not the code — rerun the "before" benchmark on the same toolchain as "after" before trusting the delta.
Memory tracking
func BenchmarkAlloc(b *testing.B) {
b.ReportAllocs() // or run with -benchmem flag
var sink []byte
for b.Loop() {
sink = make([]byte, 1024)
}
_ = sink
}b.ReportMetric() adds custom metrics (e.g., throughput):
b.ReportMetric(float64(totalBytes)/b.Elapsed().Seconds(), "bytes/s") // b.Elapsed() is only valid inside b.Loop()Sub-benchmarks and table-driven
func BenchmarkEncode(b *testing.B) {
for _, size := range []int{64, 256, 4096} {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
data := make([]byte, size)
for b.Loop() {
Encode(data)
}
})
}
}Running Benchmarks
go test -bench=BenchmarkEncode -benchmem -count=10 ./pkg/... | tee bench.txt| Flag | Purpose |
|---|---|
-bench=. | Run all benchmarks (regexp filter) |
-benchmem | Report allocations (B/op, allocs/op) |
-count=10 | Run 10 times for statistical significance |
-benchtime=3s | Minimum time per benchmark (default 1s) |
-cpu=1,2,4 | Run with different GOMAXPROCS values |
-cpuprofile=cpu.prof | Write CPU profile |
-memprofile=mem.prof | Write memory profile |
-trace=trace.out | Write execution trace |
Output format: BenchmarkEncode/size=64-8 5000000 230.5 ns/op 128 B/op 2 allocs/op — the -8 suffix is GOMAXPROCS, ns/op is time per operation, B/op is bytes allocated per op, allocs/op is heap allocation count per op.
Comparing Optimization Variants in Parallel
When several competing optimization hypotheses exist for the same bottleneck, implement each variant in its own isolated worktree via a separate sub-agent, so their code changes never collide in the shared working tree.
Run the benchmarks serially, not concurrently. Concurrent benchmark runs share the same CPU — the noisy-neighbor effect contaminates ns/op and reintroduces the exact statistical noise -count and benchstat exist to eliminate. Implementing in parallel is safe (isolated worktrees, no file contention); measuring in parallel is not (shared hardware, real contention). Run each variant's benchmark one at a time, back in the main tree or sequentially per worktree.
Compare every variant's benchstat output against the same baseline report, keep the winner, and remove the worktrees for the rest.
Documenting Results in Commits
Paste benchstat output in the commit body when the change has a measurable performance impact. This documents _why_ an optimization was made, prevents future readers from reverting it, and lets reviewers verify the claim without re-running benchmarks.
Commit format:
perf(parser): reduce Parse allocations 50% with sync.Pool
Replace per-call []byte allocation with a pooled buffer.
goos: linux / goarch: amd64 / cpu: AMD Ryzen 9 5950X
│ old │ new │
│ sec/op │ sec/op vs base │
Parse-32 4.592µ ± 2% 3.041µ ± 1% -33.78% (p=0.000 n=10)
│ old │ new │
│ B/op │ B/op vs base │
Parse-32 1.024Ki ± 0% 0.512Ki ± 0% -50.00% (p=0.000 n=10)
│ old │ new │
│ allocs/op │ allocs/op vs base │
Parse-32 12.00 ± 0% 6.000 ± 0% -50.00% (p=0.000 n=10)Rules:
- Only include benchmarks directly affected by the change — strip unrelated rows
- Never paste results with
~(no statistical significance) — the improvement cannot be claimed - Include the hardware context line (
goos/goarch/cpu) so results are reproducible - Use
perf(scope):commit type for performance-only changes
Profiling from Benchmarks
Generate profiles directly from benchmark runs — no HTTP server needed:
# CPU profile
go test -bench=BenchmarkParse -cpuprofile=cpu.prof ./pkg/parser
go tool pprof cpu.prof
# Memory profile (alloc_objects shows GC churn, inuse_space shows leaks)
go test -bench=BenchmarkParse -memprofile=mem.prof ./pkg/parser
go tool pprof -alloc_objects mem.prof
# Execution trace
go test -bench=BenchmarkParse -trace=trace.out ./pkg/parser
go tool trace trace.out