Introduction
Profile-Guided Optimization (PGO) lets the Go compiler use real-world performance data to generate faster code. Instead of making generic optimization decisions, the compiler uses a CPU profile from your production workload to optimize the exact code paths that matter most. PGO typically yields 2-14% performance improvements with zero code changes.
Key Concepts
- PGO (Profile-Guided Optimization): A compilation technique where the compiler uses runtime profiles to make better optimization decisions.
- default.pgo: A CPU profile file placed in your main package directory. The Go toolchain automatically detects and uses it during builds.
- Hot path: Code that executes frequently in production. PGO focuses optimizations on these paths.
- Devirtualization: Converting interface method calls to direct function calls when the profile shows only one concrete type is used.
Real World Context
Google reported 2-14% throughput improvements across their Go services after enabling PGO. The improvement is free — no code changes, no risk of behavior differences. You collect a profile from production, add it to your repository, and rebuild. The compiler does the rest.
Deep Dive
How PGO Works
- Build your application normally (without PGO).
- Deploy and collect a CPU profile from production (30-60 seconds is sufficient).
- Add the profile as
default.pgoin the main package directory. - Rebuild — the compiler automatically uses the profile.
The compiler uses the profile to:
- Inline hot functions more aggressively (even past normal size limits)
- Devirtualize interface calls when only one concrete type is observed
- Optimize branch prediction by arranging code for the common path
Placing the Profile
myapp/
├── main.go
├── default.pgo ← CPU profile here
├── go.mod
└── go.sum
The Go toolchain detects default.pgo automatically — no build flags needed:
bash# This build uses PGO if default.pgo exists go build -o myapp .
Verifying PGO is Active
bashgo build -v -o myapp . 2>&1 | grep -i pgo # Should show PGO-related messages
You can also check with go version -m myapp to confirm the binary was built with PGO.
Common Pitfalls
- Using benchmarks instead of production profiles — Benchmarks test isolated functions. PGO needs a profile reflecting real production traffic patterns to optimize the right paths.
- Stale profiles — If your code changes significantly, re-collect the profile. A profile from 6 months ago may optimize removed or changed code paths.
- Expecting huge gains on already-optimized code — PGO helps most when there are clear hot paths with interface calls or functions just below the inlining threshold.
Best Practices
- Automate profile collection — Set up a cron job or CI step to collect fresh profiles from production weekly.
- Commit default.pgo to your repository — This ensures every build benefits from PGO and makes the profile part of your version history.
Summary
- PGO uses production CPU profiles to guide compiler optimizations.
- Place a CPU profile as
default.pgoin your main package — the toolchain uses it automatically. - Typical improvement: 2-14% throughput with zero code changes.
- PGO enables aggressive inlining, devirtualization, and better branch prediction.
- Refresh the profile regularly to keep it aligned with current code.
Code Examples
# Step 1: Collect a CPU profile from production (30 seconds)
curl -o default.pgo \
http://prod-server:6060/debug/pprof/profile?seconds=30
# Step 2: Place it in your main package directory
mv default.pgo ./cmd/myapp/default.pgo
# Step 3: Rebuild — Go detects default.pgo automatically
go build -o myapp ./cmd/myapp
# Step 4: Verify PGO was used
go version -m myapp | grep build
# Shows: build -pgo=/path/to/default.pgo