Compiler, SSA, Inlining, and PGO

Updated

September 8, 2026

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.