Compiler, SSA, Inlining, and PGO
Compiler, SSA, Inlining, and PGO
Overview
The Go compiler turns packages into machine code through a pipeline: parse → typecheck → mid-end optimizations (SSA) → architecture lowering → assemble. Knowing the pipeline tells you which knobs are real (-gcflags, inlining, escape, PGO) versus cargo cult.
Diagram: Compiler pipeline
source → typecheck → IR → SSA → machine code → link
▲ ▲
└── PGO profile ─┘ (hot edges)
-gcflags=-m shows escape / inline decisions
Pipeline (Simplified)
source .go
-> parse / syntax
-> typecheck
-> IR build
-> SSA optimize (prove, rewrite, deadcode, nilcheck elim, ...)
-> lower to arch
-> encode object
link -> executable
Escape analysis runs early enough to decide stack vs heap; inlining expands call sites so later passes see a larger region.
Essential Flags
# Escape and inline decisions
go build -gcflags='-m' .
# More detail
go build -gcflags='-m -m' .
# Disable optimizations (debug)
go build -gcflags='-N -l' .
# Assembly listing
go build -gcflags='-S' . 2> asm.txt
# Shared / large projects: apply to specific package
go build -gcflags='all=-m' ./...| Flag | Meaning |
|---|---|
-m |
Print optimization decisions |
-l |
Disable inlining (with -N for debug) |
-N |
Disable optimizations |
-B |
Disable bounds checks (unsafe; special cases) |
Inlining
Small functions may inline into callers, enabling further constant folding and escape improvements.
//go:noinline
func sink(x int) int { return x }Use //go:noinline only for benchmarks that must measure call cost. Prefer clear small functions; the compiler is good at inlining them.
Mid-stack inlining and more aggressive budgets improved across versions — re-benchmark when upgrading major Go releases.
Bounds Check Elimination (BCE)
SSA proves many index checks redundant:
for i := range s {
s[i] = i // often single check or proved safe
}Idioms that help BCE: for i := range s, length hoisting the compiler can see, slice expressions with proven bounds. Do not micro-obfuscate for BCE without profiles.
Profile-Guided Optimization (PGO)
From Go 1.20+ (improved since):
# 1) Collect CPU profile from production-like load
# 2) Build with profile
go build -pgo=default.pgo -o app .
# or auto:
go build -pgo=auto .PGO feeds the compiler hot call edges so inlining/devirtualization favor real paths. Gains of a few percent are common; bigger when interfaces dominate.
Treat PGO profiles as build inputs — keep them reasonably fresh.
What the Compiler Will Not Do
- Fix algorithmic O(n²)
- Remove channel/mutex coordination costs
- Make reflect cheap
- Cross-package miracle opts without inlining visibility
Experiment
go mod init example
go build -gcflags='-m' . 2>&1 | sed -n '1,40p'package main
import "fmt"
func add(a, b int) int { return a + b }
func main() {
x := 3
y := add(x, 4)
fmt.Println(y)
}Typical messages include can inline add and whether x escapes (usually does via fmt).
Try next: Compare go test -bench=. with and without -pgo after collecting a profile from the benchmark binary.