$npx -y skills add samber/cc-skills-golang --skill golang-benchmarkGolang 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 investiga
| 1 | **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. |
| 2 | |
| 3 | **Thinking mode:** Use `ultrathink` for benchmark analysis, profile interpretation, and performance comparison tasks. Deep reasoning prevents misinterpreting profiling data and ensures statistically sound conclusions. |
| 4 | |
| 5 | **Dependencies:** |
| 6 | |
| 7 | - benchstat: `go install golang.org/x/perf/cmd/benchstat@latest` |
| 8 | |
| 9 | # Go Benchmarking & Performance Measurement |
| 10 | |
| 11 | Performance improvement does not exist without measures — if you can measure it, you can improve it. |
| 12 | |
| 13 | 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. |
| 14 | |
| 15 | ## Writing Benchmarks |
| 16 | |
| 17 | ### File and Ordering Conventions |
| 18 | |
| 19 | 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`) keeps `go test -bench=. ./pkg/parser` output free of unrelated `Test*` noise, and 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-testing` skill), just with the `_bench` suffix marking its narrower purpose. |
| 20 | |
| 21 | 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`. |
| 22 | |
| 23 | ### `b.Loop()` (Go 1.24+) — preferred |
| 24 | |
| 25 | 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. |
| 26 | |
| 27 | ```go |
| 28 | func BenchmarkParse(b *testing.B) { |
| 29 | data := loadFixture("large.json") // setup — excluded from timing |
| 30 | for b.Loop() { |
| 31 | Parse(data) // compiler cannot eliminate this call |
| 32 | } |
| 33 | } |
| 34 | ``` |
| 35 | |
| 36 | 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. |
| 37 | |
| 38 | ### Memory tracking |
| 39 | |
| 40 | ```go |
| 41 | func BenchmarkAlloc(b *testing.B) { |
| 42 | b.ReportAllocs() // or run with -benchmem flag |
| 43 | var sink []byte |
| 44 | for b.Loop() { |
| 45 | sink = make([]byte, 1024) |
| 46 | } |
| 47 | _ = sink |
| 48 | } |
| 49 | ``` |
| 50 | |
| 51 | `b.ReportMetric()` adds custom metrics (e.g., throughput): |
| 52 | |
| 53 | ```go |
| 54 | b.ReportMetric(float64(totalBytes)/b.Elapsed().Seconds(), "bytes/s") // b.Elapsed() is only valid inside b.Loop() |
| 55 | ``` |
| 56 | |
| 57 | ### Sub-benchmarks and table-driven |
| 58 | |
| 59 | ```go |
| 60 | func BenchmarkEncode(b *testing.B) { |
| 61 | for _, size := range []int{64, 256, 4096} { |
| 62 | b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { |
| 63 | data := make([]byte, size) |
| 64 | for b.Loop() { |
| 65 | Encode(data) |
| 66 | } |