4.7 KiB
4.7 KiB
Go Performance Profiling
Toolchain
| Tool | Purpose | Command |
|---|---|---|
go tool pprof |
CPU / heap / block / mutex profiles | go tool pprof -text -cum -nodecount=30 profile.pb.gz |
go test -bench |
Micro-benchmarks | go test -bench=BenchmarkX -benchtime=3s -benchmem -count=3 |
go test -cpuprofile |
Capture CPU profile during benchmarks | go test -bench=BenchmarkX -cpuprofile=cpu.prof |
go test -memprofile |
Capture heap profile during benchmarks | go test -bench=BenchmarkX -memprofile=mem.prof |
go build -gcflags="-m" |
Escape analysis (what allocates on heap) | go build -gcflags="-m" ./path/to/pkg 2>&1 | grep "escapes to heap" |
go test -trace |
Execution trace (goroutine scheduling) | go test -bench=BenchmarkX -trace=trace.out |
benchstat |
Statistical comparison of benchmarks | benchstat old.txt new.txt |
Data Extraction Script
Use scripts/go-pprof.sh to extract profiles into agent-readable markdown.
# CPU profile from a .prof file
.agents/skills/perf-optimization/scripts/go-pprof.sh cpu path/to/cpu.prof
# Heap profile from a live endpoint
.agents/skills/perf-optimization/scripts/go-pprof.sh heap http://localhost:6060/debug/pprof/heap
# CPU profile from a benchmark (generates and analyzes in one step)
.agents/skills/perf-optimization/scripts/go-pprof.sh bench ./internal/platform/server/... BenchmarkRequireAuth
Go-Specific Optimization Patterns
Escape Analysis
Before optimizing allocations, run escape analysis to understand WHY values escape to heap:
go build -gcflags="-m" ./internal/platform/server/ 2>&1 | grep "escapes to heap"
Common escape reasons:
- Returning a pointer to a local variable
- Storing a value in an interface (boxing)
- Closure captures
- Slice/map growth beyond initial capacity
sync.Pool for Hot-Path Allocations
When to use: The profiler shows a specific type allocated/freed thousands of times per second on the same hot path.
When NOT to use: Objects that are long-lived, vary wildly in size, or have cleanup requirements.
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
defer func() { buf.Reset(); bufPool.Put(buf) }()
// use buf...
}
Pre-sized Slices and Maps
Symptom: runtime.growslice or runtime.mapassign in the profile — the runtime is repeatedly growing backing arrays.
// ❌ Grows 3 times: cap 0 → 1 → 2 → 4
items := []Item{}
for _, raw := range data { items = append(items, parse(raw)) }
// ✅ One allocation
items := make([]Item, 0, len(data))
for _, raw := range data { items = append(items, parse(raw)) }
strings.Builder Over fmt.Sprintf in Loops
Symptom: fmt.Sprintf or runtime.convTstring in allocation-heavy loops.
var b strings.Builder
b.Grow(estimatedLen)
for _, part := range parts { b.WriteString(part) }
result := b.String()
Benchmark Best Practices
func BenchmarkX(b *testing.B) {
// Setup OUTSIDE the loop — don't measure init cost
fixture := buildFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := doWork(fixture)
// Prevent compiler from eliminating the call
_ = result
}
}
Statistical validity: Always run with -count=5 minimum and compare with benchstat:
go test -bench=BenchmarkX -benchmem -count=5 > old.txt
# ... make changes ...
go test -bench=BenchmarkX -benchmem -count=5 > new.txt
benchstat old.txt new.txt
Go-Specific Anti-Patterns
- Don't use
sync.Poolfor everything. It's only effective for truly hot paths with uniform objects. Misuse adds complexity without measurable gain. - Don't fight the GC. Go's GC is tuned for latency. If throughput matters more, tune
GOGCbefore rewriting code. - Don't use
unsafefor performance. The compiler's escape analysis and inlining are usually sufficient.unsafebreaks memory safety guarantees. - Don't ignore
-racewhen benchmarking. Always verify correctness with-racebefore measuring performance without it.
Irreducible Floors in Go
These are performance costs you cannot eliminate — recognize them and stop:
| Function | What it is | Why it's irreducible |
|---|---|---|
p256MulInternal |
P-256 scalar multiplication (ECDSA) | Hardware-optimized assembly |
runtime.mallocgc |
Go memory allocator | Symptom, not cause — fix the caller |
runtime.gcBgMarkWorker |
GC mark phase | Proportional to allocation rate — fix allocs |
runtime.futex / runtime.usleep |
OS synchronization | Scheduling overhead |
syscall.Syscall |
Kernel calls (I/O) | Irreducible for actual I/O ops |
crypto/aes |
AES encryption | Hardware AES-NI, cannot improve |