Introduction
PGO is only valuable if it demonstrably improves performance. Rigorous benchmarking before and after PGO is essential — both to justify the operational overhead of profile collection and to detect cases where PGO provides minimal benefit.
Key Concepts
- A/B benchmarking: Comparing the same workload between a PGO-enabled and PGO-disabled build.
- benchstat: A Go tool that statistically compares benchmark results, accounting for variance.
- Warm-up: PGO benefits are most visible after the binary has warmed up (JIT-like optimizations are baked in at compile time, but caches and branch predictors need warm-up).
Real World Context
Not every service benefits equally from PGO. An I/O-bound service waiting on database queries may see < 1% improvement, while a CPU-bound JSON serialization service might see 10-14%. Measuring tells you where the investment pays off.
Deep Dive
Setting Up A/B Benchmarks
Build two binaries from the same source:
bash# Baseline: no PGO go build -pgo=off -o server-baseline ./cmd/server # PGO-enabled go build -o server-pgo ./cmd/server # Uses default.pgo
Using Go Benchmarks with benchstat
bash# Run benchmarks 10 times each go test -bench=. -count=10 -pgo=off ./... > nopgo.txt go test -bench=. -count=10 ./... > pgo.txt # Compare statistically benchstat nopgo.txt pgo.txt
Example output:
name old time/op new time/op delta
ParseJSON-8 125µs ± 2% 112µs ± 1% -10.40% (p=0.000 n=10+10)
ServeHTTP-8 89µs ± 3% 82µs ± 2% -7.87% (p=0.000 n=10+10)
What PGO Optimizes
PGO focuses on three main optimizations:
- Inlining: Functions that are hot in the profile are inlined more aggressively, even if they exceed normal size limits.
- Devirtualization: Interface method calls where the profile shows a single dominant concrete type are converted to direct calls.
- Block layout: Code blocks are reordered so the common execution path is linear (better CPU branch prediction).
Checking Which Optimizations Applied
bash# See PGO-driven inlining decisions go build -gcflags='-m -m' -pgo=default.pgo ./cmd/server 2>&1 | grep 'PGO'
Common Pitfalls
- Running too few benchmark iterations — Run at least 10 iterations (
-count=10) for statistical significance. Usebenchstatto verify the difference is real. - Benchmarking I/O-bound code — PGO optimizes CPU-bound code paths. If your bottleneck is database or network I/O, PGO won't help there.
Best Practices
- Use benchstat for comparison — Never eyeball benchmark results. benchstat accounts for variance and reports confidence intervals.
- Benchmark on the same hardware — CPU architecture, cache sizes, and memory speed all affect results. Compare on identical machines.
Summary
- Always measure PGO impact with A/B benchmarks using
-pgo=offas baseline. - Use
benchstatfor statistically rigorous comparison. - PGO optimizes inlining, devirtualization, and block layout.
- Benefits are greatest for CPU-bound workloads (2-14% typical).
- Use
-gcflags='-m -m'to see which PGO optimizations the compiler applied.
Code Examples
bash
# Install benchstat
go install golang.org/x/perf/cmd/benchstat@latest
# Run benchmarks with and without PGO (10 iterations each)
go test -bench=BenchmarkServe -count=10 -pgo=off ./... > baseline.txt
go test -bench=BenchmarkServe -count=10 ./... > withpgo.txt
# Statistical comparison
benchstat baseline.txt withpgo.txt
# Output shows delta with confidence intervals