134 lines
4.7 KiB
Markdown
134 lines
4.7 KiB
Markdown
# 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.
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
// ❌ 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.
|
|
|
|
```go
|
|
var b strings.Builder
|
|
b.Grow(estimatedLen)
|
|
for _, part := range parts { b.WriteString(part) }
|
|
result := b.String()
|
|
```
|
|
|
|
### Benchmark Best Practices
|
|
|
|
```go
|
|
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`:
|
|
|
|
```bash
|
|
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
|
|
|
|
1. **Don't use `sync.Pool` for everything.** It's only effective for truly hot paths with uniform objects. Misuse adds complexity without measurable gain.
|
|
2. **Don't fight the GC.** Go's GC is tuned for latency. If throughput matters more, tune `GOGC` before rewriting code.
|
|
3. **Don't use `unsafe` for performance.** The compiler's escape analysis and inlining are usually sufficient. `unsafe` breaks memory safety guarantees.
|
|
4. **Don't ignore `-race` when benchmarking.** Always verify correctness with `-race` before 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 |
|