Go: A Documentary

Middle end and SSA optimizer

Part I · 55 threads in 3 era slices, from Go 1.0 to Go 1.28. Each thread gives its design, then a per-release timeline of what landed (Done) and what was still open. Release labels in a timeline link to that release's page.

● = the thread had substantive commits in that cycle; hover for the stage summary, click to jump to it in the release page.

Thread map, Go 1.0 – 1.17

Thread1.01.11.21.31.41.51.61.71.81.91.101.111.121.131.141.151.161.17Status at go1.17 (or at the end of its era slice)
Middle end and SSA optimizer (Go 1.0–1.9)
SSA backend arrival (dev.ssa): bring-up, coverage, default-on●●●still the design at go1.17 (the SSA IR, pass pipeline and rulegen scheme introduced here is what the compiler uses); superseded only in detail by later register-ABI and typed-IR work.
Generic rewrite rules and rulegen●●●still the design at go1.17 (rules in ssa/gen/.rules, generated rewrite.go; extended with new rule syntax and later machinery).
Common subexpression elimination (cse) and its scalability●●●still the design at go1.17 (partition refinement by sorted argument classes).
Nil check elimination and the redesign of nil checks●●●still the design at go1.17 (NilCheck as a memory-ordered op, eliminated by a dominator-tree walk, with a separate arch-specific late pass).
Prove pass and bounds-check elimination●●●still the base design (dominator-walk with facts) but later rewritten around a poset/limit table (the 1.19 rework) → continues as Prove pass and bounds-check elimination: from poset relations to a dense limit table, topo-sorted walk, and division-aware proofs (1.19+).
Escape analysis (esc.c → esc.go)●●●●●●●●●●still the flood-based esc.go design at the end of this range (go1.9) → continues as Escape analysis: call modelling, closures, func-variable callees, and solver speed (1.18+).
Inlining (inl.c → inl.go)●●●●●●classic budget-80 inliner, still the design at go1.17 with later additions (mid-stack inlining, closures) → continues as Inliner: budget, closures, and the interleaved inline/devirtualize driver (1.18+).
Order, walk and temporary lifetimes (order.c, walk.c)●●●●●●order and walk stayed the pre-SSA lowering phases through the end of this range → continues as Range-loop and range-over-func lowering in walk (1.18+) for the range part.
Write-barrier insertion: from gc to the SSA writebarrier pass●●●●●SSA writebarrier pass with store ordering, still the design at go1.17 (hybrid barrier elision refinements later).
Switch, type-switch and select lowering●●●●binary search plus later jump tables → continues as Switch lowering: jump tables, type-switch caches, and switch lookup tables (1.19+ for jump tables).
Interface conversions, type assertions and devirtualization●●●●→ continues as Static devirtualization and shape-aware type-assertion pruning (1.18+).
Phi construction and the SSA builder's scalability●●●Sreedhar-Gao phi building for large functions, still the design at go1.17.
Dominators, loops, CFG editing and block-level passes●●●still the design at go1.17 (dominator tree + likelyadjust + looprotate + fuse/shortcircuit/trim).
Compile-time performance and determinism of the SSA package●●●the allocation caches and sparse structures remain.
Preparing the backend for concurrent compilation (Go 1.9)●completed within 1.9; no later thread.
Lowering of copies, strings and slices; intrinsics●●●●●→ continues as Lowering of builtins to memory ops: memmove/memclr/memequal inlining and generated equality (1.18+).
Middle end and SSA optimizer (Go 1.10–1.17)
Prove pass: unsatisfiability, partial-order facts, loop bounds-check elimination●●●●●●●●still the design at go1.17 (facts table + poset, induction variables found inside prove); loop BCE merged into prove in 1.11 → continues as Prove pass and bounds-check elimination: from poset relations to a dense limit table, topo-sorted walk, and division-aware proofs (1.18+)
Escape analysis: the esc.go fixes, the escape.go rewrite, and the move to package escape●●●●●●●●the escape.go (graph/location) design, now in its own package cmd/compile/internal/escape, handling closure conversion/capture analysis → continues as Escape analysis: call modelling, closures, func-variable callees, and solver speed (1.18+)
Inlining: budget, mid-stack inlining, closures, and what became inlinable●●●●●●●●classic AST-based inliner in cmd/compile/internal/inline with budget 80, mid-stack inlining, for loops and closures inlinable (closures-with-captures-in-functions disabled by -d=inlfuncswithclosures=0) → continues as Inliner: budget, closures, and the interleaved inline/devirtualize driver (1.18+)
Early devirtualization and static value tracing●●a small AST pass in a new devirtualize package (Func), invoked after inlining → continues as Static devirtualization and shape-aware type-assertion pruning (1.18+)
Open-coded defers and defer lowering●●●●stack-allocated defer records (1.13), open-coded defers (1.14), and go/defer wrapped into argument-less closures (1.17)
Late call expansion and the SSA side of the register ABI●●●expand_calls pass lowering aggregate args/results to OpArgReg/SelectN; compiler support for GOEXPERIMENT=regabi in place
Statement boundaries (is_stmt) and source positions through the optimizer●●●●●still the design (stmt marks on src.XPos lico, preserved by passes)
DWARF location lists from SSA●●●●the 1.11 design (direct SSA to location list, ssa/debug.go) remained in place
Switch lowering, select, and type switches in walk●●●●the 1.14 walk/switch.go design (binary search over runs of constants, string length-first) → continues as Switch lowering: jump tables, type-switch caches, and switch lookup tables (1.18+)
Range loops, append and map idioms in walk●●●●still the design (lowering to element pointer with late increment; memclr idiom) → continues as Range-loop and range-over-func lowering in walk (1.18+)
Rewrite-rule generator, typed Aux/AuxInt, and rule syntax●●●●●●●ssa/gen/rulegen.go builds an in-memory syntax tree with typed aux fields; untyped rules removed
Write-barrier elision and the writebarrier pass●●●●●●ssa/writebarrier.go inserts barriers and drops them for provably non-heap stores
Memory-op optimizations: memmove, Move/Zero, dead stores, nil checks●●●●●●generic rules plus dse/dead auto elim passes → continues as Memory-op optimization: memcombine, dead stores, store-to-load forwarding, alias analysis, mem2reg (1.18+), Lowering of builtins to memory ops: memmove/memclr/memequal inlining and generated equality
Arithmetic strength reduction, divisibility and intrinsics●●●●●●●●generic.rules and per-arch rules
CFG and control-flow passes: branchelim, fuse, shortcircuit, phiopt●●●●●●●branchelim on amd64/arm64, strengthened shortcircuit/fuse/phiopt → continues as Branch-free lowering: CondSelect-into-math and ARM64 CCMP (1.18+)
Compiler front of walk/order: ordering fixes and the dev.regabi IR refactor (package walk)●●●●●●●●walk, escape, devirtualize, ssagen are separate packages over the new ir node types
Diagnostics for the optimizer: ssa.html, GOSSAFUNC and optimizer logging●●●●●●●still the tooling at go1.17

Thread map, Go 1.18 – 1.28

Thread1.181.191.201.211.221.231.241.251.261.271.28Status at HEAD
Compiler: middle end and SSA optimization (Go 1.18–1.28)
Profile-guided optimization (PGO): hot-call inlining, devirtualization, profile preprocessing●●●●●●●on whenever a profile is supplied; drives inlining and both kinds of devirtualization; 1.28 also accepts Linux perf profiles
Inliner: budget, closures, and the interleaved inline/devirtualize driver●●●●●●●●●●●classic budget-80 inliner with closure, parameter-call and hot-path discounts; runs interleaved with devirtualization (inline/interleaved); functions calling recover became inlinable in 1.28
Escape analysis: call modelling, closures, func-variable callees, and solver speed●●●●●●●●●cmd/compile/internal/escape resolves calls through closures and func variables, has a heavily optimized solver, and owns go/defer wrapping; the zerocopy string→[]byte conversion is controlled by a debug flag
Allocation avoidance: stack-allocated slice backing stores and allocation-free literals●●●●●variable-size make and append use a 32-byte stack buffer when the slice does not escape; a move2heap slice pass handles slices that escape only at the end of their lifetime; runtimefreegc is off by default; bug fixing ongoing in 1.28
New inliner heuristics (GOEXPERIMENT=newinliner, inline/inlheur)●●●experiment exists, off by default, never enabled; gated by buildcfg.Experiment.NewInliner; only maintenance fixes since 1.22
Prove pass and bounds-check elimination: from poset relations to a dense limit table, topo-sorted walk, and division-aware proofs●●●●●●●●●●prove (ssa/prove.go, moved to ssacompile/prove in 1.28) keeps a per-value signed/unsigned limit table plus a relation poset; it walks blocks in topological order; divisibility and divmod rewriting run after it
Known-bits pass (bitwise constant folding)●●known bits pass in the default pipeline, run after prove and before late opt; ops are added incrementally
Memory-op optimization: memcombine, dead stores, store-to-load forwarding, alias analysis, mem2reg●●●●●●●●●memcombine, dse, dead auto elim and elim unread autos are in the default pipeline; type-based disjoint is used by CSE and DSE; mem2reg (1.28) handles single-block scalars only
Lowering of builtins to memory ops: memmove/memclr/memequal inlining and generated equality●●●●●●●●●●constant-size memmove/memclrNoHeapPointers become Move/Zero; runtime.memequal is the MemEq SSA op, expanded inline for small constant sizes; equality functions are generated per "signature" (1.27); preemptible memclr was reverted
Stack-slot merging (mergelocals)●●on by default when optimizing (since 1.23); address-use analysis tightened in 1.28
Switch lowering: jump tables, type-switch caches, and switch lookup tables●●●●●●●●●integer and string-length switches use jump tables (off under -N and retpolines); large type switches jump on type-hash bits behind per-site runtime caches; constant-returning switches become lookup tables (1.27)
Static devirtualization and shape-aware type-assertion pruning●●●●devirtualize.StaticCall uses a flow-insensitive concrete-type analysis that follows multiple assignments, assertions and conversions; impossible type assertions in shaped generic code are dropped
Loop variable per-iteration semantics (loopvar)●●●●per-iteration semantics for files at go1.22+; GOEXPERIMENT=loopvar and -d=loopvar remain for testing; cmd/compile/internal/loopvar does the rewrite
testing.B.Loop support in the compiler (bloop)●●●●●a bloop pass wraps assignments and call results inside for b.Loop() bodies in runtime.KeepAlive; inlining is no longer suppressed
Range-loop and range-over-func lowering in walk●●●●●●●slice/array range loops lower to an incremented element pointer guarded against past-the-end values; range-over-int and range-over-func are lowered; range-clear idioms become memclr
Sparse conditional constant propagation (sccp)●●●●sccp pass in the default pipeline; convergence sped up in 1.27
Loop-invariant code motion (licm)●●licm pass exists with an allow-list of liftable ops; a related "merge conditional branches" pass was disabled late in 1.27
Branch-free lowering: CondSelect-into-math and ARM64 CCMP●●●●●branchelim rewrites CondSelect into arithmetic on all arches; arm64 uses CCMP chains for logical AND conditions
Constant-folding loads from type metadata and dictionaries●●●●rewriteFixedLoad folds loads from read-only abi.Type, abi.PtrType, itab and dictionary symbols
SSA values for zero-sized and multi-field direct-interface types●●●●all zero-sized values are SSA-able; structs with one pointer field plus zero-sized fields are stored directly in interfaces
ARM64 load/store pairing (LDP/STP)●●●●SSA pair pass plus a post-genssa spill/reload fusing step
SSA package decomposition and compiler-infrastructure refactor●cmd/compile/internal/ssa split into subpackages (issue #80409), still in progress

Middle end and SSA optimizer (Go 1.0–1.9)

Reader: Claude Sonnet. Cites 1,060 of 1,161 commits in this slice; every non-noise commit is cited, plus 7 larger commits the noise filter had set aside, which a separate gap reader described. Escape-analysis details for the C era come from messages and subject lines, not diffs. About 390 small commits (per-construct dev.ssa work, early fixes) are cited in grouped items.

Release digest for this slice
  • Go 1.0 — The pre-release compiler (6g/gc in C): walk/switch/select lowering and a growing escape analysis, first inliner (2011–2012) and the old cgen code generators; mostly bug-fix commits (threads: escape analysis, inlining, order/walk).
  • Go 1.1 — Escape analysis restructured (call-graph SCC order, in→out parameter tags), inliner made practical (variadic, wrappers, escape-info stealing), rotate recognition (threads: escape analysis, inlining, order/walk).
  • Go 1.2 — Small: escape-driven stack make, redundant bounds checks dropped in inlined append and slicing (threads: escape analysis, order/walk).
  • Go 1.3 — Precise stack liveness drives order/walk: zeroed runtime temporaries and shortened temporary lifetimes (860→23 ambiguously live), uintptr ignored by escape analysis (threads: order/walk, escape analysis).
  • Go 1.4 — Escape-analysis soundness fixes motivated by stack copying (indirect-of-param), write barrier prep (threads: escape analysis).
  • Go 1.5 — The compiler converted to Go; write barriers emitted by gc; concrete==interface, _, ok := i.(T), re-slice and &T{} escape improvements (threads: write barriers, escape analysis, interface conversions, switch lowering).
  • Go 1.6 — Escape precision for variadic args and loop levels; string compare cleanup (threads: escape analysis, copies/strings).
  • Go 1.7 — SSA arrives (dev.ssa merged): IR, rulegen, CSE, nilcheckelim, prove/BCE and induction-variable BCE, regalloc, decompose, phi building, dominators; amd64 default (threads: SSA arrival, rewrite rules, CSE, nilcheck, prove, phi construction, dominators/CFG, write barriers).
  • Go 1.8 — SSA becomes default on ARM, arm64, mips, mips64, s390x, ppc64; nil checks redone; writebarrier pass in SSA; Sreedhar-Gao phis; O(n log n) CSE; type-assert/switch inlining; preemptible-loop experiment (threads: SSA arrival, nilcheck, write barriers, CSE, phis, interface conversions, switch lowering).
  • Go 1.9 — Write barrier insertion fully moved to SSA, de-virtualization, loop rotation, constant-to-interface without allocation, concurrent-backend preparation, inlining dead-code awareness (threads: write barriers, devirtualization, backend concurrency prep, inlining, rewrite rules, order/walk).

SSA backend arrival (dev.ssa): bring-up, coverage, default-on

Span: Go 1.7 → Go 1.9 · Status at go1.17: still the design at go1.17 (the SSA IR, pass pipeline and rulegen scheme introduced here is what the compiler uses); superseded only in detail by later register-ABI and typed-IR work.

Design docs & discussion

Design. The SSA backend started as cmd/internal/ssa and was moved to cmd/compile/internal/ssa. A function is a Func of Blocks holding Values; each Value has an Op, a Type, a numeric AuxInt, an Aux payload and Args. Generic ops are in gen/genericOps.go, machine ops in per-arch gen/*Ops.go; the opcode tables (opGen.go) are generated (0dca7351e9), and the rewrite functions (rewritegeneric.go, rewriteAMD64.go) are generated from .rules files by gen/rulegen. The front end (gc/ssa.go) converts the walked AST into SSA; a long list of “unimplemented” node kinds gated what could be compiled at first (083a646f63 first generated code from SSA by extracting an obj program, enough to run iterative Fibonacci).

Early on, the project was bootstrapped incrementally: 8c6abfeacb separated debug logging, work-in-progress (“Unimplemented”) and fatal errors, so SSA could be attempted on every function and the whole standard library became a compilation test; only functions named _ssa used the result. d298209b1c added GOSSAFUNC/GOSSAPKG to enable SSA code for one function or package without rebuilding, and 5844603f14 aggregated the “unimplemented” reasons (SLICEARR 14.85%, CALLINTER 13.84%, RETJMP 11.84%, EFACE 10.24% of failures at that moment) to prioritise work. 5b355a7907 replaced “compile everything, then decide” with deciding first and only sending chosen functions through SSA. 729abfa35c made the compiler tests default to SSA on amd64, after which the TODO was to remove the UnimplementedF machinery.

Two data-representation decisions shaped everything later. 2a5e6c47bc redefined sub-register integers: the high bits of a register holding a sub-word value are junk, so ops that consume sub-word args must use width-specific forms (x86 has them; indexing is the exception) and ops producing sub-word results need not extend; generic ops pretend register size equals type width. 7fc5621991 defined the representation of AuxInt as the sign-extended version of the low bits for every width, so (Const8 [0x1]) and (Const8 [0x101]) stop being distinct to CSE. Types moved out of the SSA package (85e0329fbc), ssa.Type lost Equal in favour of the cheaper back-end Compare (2563b6f9fe), and Store/Move/Zero ops later carried the stored type as Aux (211c8c9f1a).

Architectures were enabled one by one: amd64 by default in the 1.7 cycle, ARM (42181ad852, with -ssa=0 to disable), then in 1.8 arm64, mips64 and s390x (their enabling commits belong to the per-architecture backend slices) and ppc64 big-endian (eed061f89c); 7f033933ce added GOARCH=mips{,le} to gc.

Timeline.

Generic rewrite rules and rulegen

Span: Go 1.7 → Go 1.9 · Status at go1.17: still the design at go1.17 (rules in ssa/gen/*.rules, generated rewrite*.go; extended with new rule syntax and later machinery).

Continues in: Rewrite-rule generator, typed Aux/AuxInt, and rule syntax (Go 1.11 → Go 1.16)

Design. Machine-independent simplification lives in gen/generic.rules (with dec.rules for decomposition of strings, slices, interfaces, complex). A rule is (Op args) [cond] -> (result); rulegen parses the pattern, generates one Go function per top-level op (a347ab7cd1 split op rewrites into separate functions) wrapped in a for loop that restarts the match after a rewrite (0543447597), and the driver applyRewrite runs until fixed point. Pitfalls were detected mechanically: unbalanced rules (1807d54d85), rewrite loops of length >1 via Floyd’s cycle finder advancing 1.5 steps per cycle (baf2c3ec4a) and OpCopy loops (f421735bb4). Rulegen automatically copies values on the right-hand side (0fb818a424), can target a specific block (177b697ba5), logs the rule line number (514ab7c385, later behind a codegen flag f85faefa67) and emits a rule-coverage log (0e930015c1). The rules are typed: 606a11f464 detects the type earlier; 6a1153acb4 (1.8) factors out parseValue, makes op selection depend on the number of args so arch ops may share names with generic ops, and reports unresolved ambiguity. 69fe9ea43e (1.9) unified the generated-code header.

The rule set grew from basics (9b25da732b simplifications, 198fc9a9eb foldings) to constant folding and normalisation: comparisons with constants (cdc36252fe, d4a95e78fa), constant shifts (93a0b0f315, c8b148e7a5 for lsh/rsh/lsh), float constant folding (f6ceed2cab), modulo (060501dc0e, 68325b56e7 folds IsInBounds of modulo), IsInBounds with small index (9549c06ce6, bbd3ffbd83), Neg folding (964dda9bf1, 417f49a363), distributing multiplication over addition (1f6e9e36b0), AND x const as a shift (adc8d491c2), truncate-of-AND (97b2295f06), Trunc-of-Ext (5c3edc46a6), redundant extension after shift (ac74225dcc), logical identities (4d5adf1eb1), bool ops OrB/EqB/NeqB/AndB (8b92397bcd), boolean simplification (6a24b2d0c1, commutativity of boolean ops 9aeced650f), negation folded into comparison (00459f05e0), pointer rules (a7a199947a, 3294014ae1 OffPtr collapse, da1802f1df, 6ed10382f7 smarter isSamePtr), 64-bit division strength reduction (a3055af45e), floating point strength reduction x*2→x+x (63a72fd447), shift pairs to sign/zero extensions (72a071c1da), bit twiddling (00db0cbf86), canonicalising constants to one argument position and reassociating (379567aad1, which folds 4 + x - 1 and 4 & x & 1), constant pointer comparison (d68bb16b1e), Zero-after-newobject removal (b2e0e9688a, c8eaeb8cba), and store-elimination rules for values just loaded from the same location (4fc498d89a, up to 3 intermediate stores to disjoint fields). Constant math: precomputed constant square roots (8e24a98abe).

Use counts were added to Values (56e0ecc5ea) so rules can be constrained: the motivating case is not merging an op with a load that has more than one use, because duplicating a load would break race semantics (the m.state load in sync.Mutex.Lock must not be done twice). 5bfd1ef036 removed the “volatile” flag.

Timeline.

Common subexpression elimination (cse) and its scalability

Span: Go 1.7 → Go 1.9 · Status at go1.17: still the design at go1.17 (partition refinement by sorted argument classes).

Design. CSE partitions values into equivalence classes, then replaces each value by a dominating member of its class. The first form compared types via Type.Equal, with the type string forming the coarse initial partition (d9a704cd40), and did not combine Phis of different blocks (00437ebe73: an inner-loop i phi and a different block’s phi were merged, which deleted the increment and made the loop spin). Subsequent changes were driven by compile-time blowups on generated code:

Timeline.

Nil check elimination and the redesign of nil checks

Span: Go 1.7 → Go 1.9 · Status at go1.17: still the design at go1.17 (NilCheck as a memory-ordered op, eliminated by a dominator-tree walk, with a separate arch-specific late pass).

Continues in: Memory-op optimizations: memmove, Move/Zero, dead stores, nil checks (Go 1.10 → Go 1.16)

Design. nilcheckelim (2a846d2bd3) initially removed redundant nil checks (75 of 886 in the SSA-compiled 20% of the stdlib, 8.5%; an unsound more aggressive CSE would have removed 13%). Because O(n^2) behaviour showed in benchmarks (1746e711ad), 4dcf8ea1a4 rewrote it as a DFS of the dominator tree keeping a map of checked values, and f4e37c8ec5 (1.8) switched to the standard dominator tree. Knowledge of non-nil values: skipped for OpAddr/OpAddPtr (b14b8d6bc7), Phis whose args are all non-nil (e0e556620a; c694f6f3a8 repeats until stabilisation because some Phis had unprocessed args), results of newobject and of mapaccess1/2 (3c1a4c1902), and PAUTOHEAP variable access (4c4ca83120). The first design lowered the check to a TESTQ/JEQ; 31115a5c98 replaced it with faulting loads and folds the check into a following load/store when possible (binaries ~2% smaller); 4a346e7489 extended that to float loads/stores.

3134ab3c2d (1.8) is the major redesign: BlockCheck is removed; NilCheck panics on nil and returns void, is no longer a block control value, and deadcode learns not to drop it; several can occur per block; the arch-dependent elimination formerly in ssaGenValue becomes its own pass driven by new opcode flags. Bugs afterwards: nil-check info was not usable inside a block because values within a block are unordered, so 256a605faa made facts valid only from the next block (some _ = *p; f(); _ = *p patterns regress to two checks; intra-block redundancy falls to CSE), and 98061fa5f3 (1.9) re-enabled same-block removal after the ordering problem was fixed; c1892b9c4b (1.7) had earlier turned off simplification in loops.

Timeline.

Prove pass and bounds-check elimination

Span: Go 1.7 → Go 1.9 · Status at go1.17: still the base design (dominator-walk with facts) but later rewritten around a poset/limit table (the 1.19 rework) → continues as Prove pass and bounds-check elimination: from poset relations to a dense limit table, topo-sorted walk, and division-aware proofs (1.19+).

Continues in: Prove pass: unsatisfiability, partial-order facts, loop bounds-check elimination (Go 1.10 → Go 1.17)

Design docs & discussion

Design. Bounds-check removal began in the [dev.ssa] branch with bdea1d58cf: “remove proven redundant controls”, a linear-in-edges pass that walks the dominator tree and removes a branch whose controlling condition (or its negation) was already taken on the path; it removed the second check in for i := range a { a[i]++; a[i++] } and cut binaries by ~110% of what a heavier pending CL (12960) did. 34f048c9d9 tweaked it with sdom; cd798dcb88 generalised it to all booleans (so a6 || (a6 || ...) collapses); 47c9e139ae added constant comparisons propagated down the dominator tree (if x >= 7 then x > 4 is true), enabling _ = b[7] hints (issue 14900); 27ebc84716 handled 0 <= i && i < len(a); 6c6089b3fd BCE when max and limit are constants; b91cc53033 is a separate pass, “BCE for induction variables” (later loopbce): it finds loops with an upper limit (i < n, i < len(s), ranges) and removed 842 of 22954 checks in make.bash and 1957 of 42952 in all.bash; it does not yet find derived induction variables c*ind or c+ind, and “drop the NilChecks on the pointer of range” was the follow-up. 1747788c56 added a pass to print bounds checks (the -d=ssa/check_bce facility) and 0f29942489 (1.8) repurposed an old test into prove’s sliceopt test and added a missing rule so 32- and 64-bit agree. 73f92f9b04 (1.9): when x <= len(s) is known, also infer x <= cap(s), so s[x:] then s[:x] needs one check (issue 16813). dfcb853d9d moved builtin lowering later so these passes see SliceLen/SliceCap; e61db3119c simplified SliceCap when it equals SliceLen. Walk-side hints: the front end marks some indices Bounded (46e62f873a made SSA honour it for empty range loops), 7439ba32ff (1.9) respects it for OpArraySelect, and 4b0eb19a05 / 19e2922688 (1.2) were the old-backend analogues.

Timeline.

Escape analysis (esc.c → esc.go)

Span: Go 1.0 → Go 1.9 · Status at go1.17: still the flood-based esc.go design at the end of this range (go1.9) → continues as Escape analysis: call modelling, closures, func-variable callees, and solver speed (1.18+).

Paths not taken: Escape-analysis precision experiments (partially landed)

Continues in: Escape analysis: the esc.go fixes, the escape.go rewrite, and the move to package escape (Go 1.10 → Go 1.17)

Earlier history: Escape analysis and heap allocation (Go 1.0 → Go 1.0)

Design docs & discussion

Design. esc.c (later esc.go) tags each parameter with an escape note (esc:$mask, in modern terms “leaking param”). It traces assignment paths from address-taken variables and pointer-typed variables to sinks, tracking loop depth (loopdepth) so that things assigned out of a loop level escape. Go 1.0 inherited a simplified recursion (42687d6ce4, 2011-08), spurious-leak fixes (0227c45ede), DOTMETH handling (f2460a8c57), better loop depth for labels (9bf3478658) and the -mm diagnostic sink (4e78818259).

For Go 1.1, the analysis was restructured. 9fe424737b runs it per call-graph SCC in dependency order (mutually recursive functions in one batch; before, a package was one batch). 75692424d2 and 507fcf37d2 (Luuk van Dijk) introduced flow of inputs to outputs: step 0 synthesised outparams, step 1 gave them loop depth 0, step 2 emitted esc:$mask tags but still tied any param with a nonzero mask to the sink, and the next step used tags in esccall so functions that merely return their argument no longer leak it. 2667dcd113 steals escape info when inlining wrapper functions (issue 5056), 2c4b029b75 (listed under Go 1.2) uses the escape result for make([]T, const) (issue 5484), 572d984eaa/ba97d52b85 fix variable-capture-in-loops bugs.

Correctness work in 1.3/1.4 mattered because stack copying made “stack pointer in the heap” a crash rather than a harmless leak (fe3c913443): functions returning an indirect of a parameter (func f(z **int) *int { return *z }) were tagged non-leaking; the fix marks parameters whose pointed-to content reaches a result as “content escapes to result” and at call sites forces all indirections of such an argument to the heap. The message states the trade-off: a more precise version would record which result and how many indirections; the less precise, the more heap allocations. eb592d8289 stopped following uintptr passed as arguments (aligned with the GC no longer seeing pointers in uintptrs; also makes syscall args non-escaping). e5d742fcad relaxes address-of analysis, c99dce2b05/f078711b41 fix ... and slice-of-array, 775ab8eeaa handles &x inside switch x := v.(type), f20e4d5ecb &result into result, and d2204e6c0e/337fe4134f forbid/diagnose escaping args in the runtime package.

Go 1.5 loosened it where safe: 22c16b4b92 ignores re-slicing assignments (b.buf = b.buf[n:m] made b escape; two stdlib cases, zip -6.5% allocs), 1b87f01239 links STRUCTLIT to PTRLIT so &T{...} element initialisers escape only when the literal does, a21cf5b6a2 makes slice literals (before append) non-escaping and tracks pointers in slices, b19ec6842d made indirect function and method calls leak everything (they previously didn’t), 66130907d1 handles copy, 878a86a129 fixed closures captured by value (issue 10353: new(int) captured by a heap closure stayed on the stack), 6f6bcadc17 adds ODOTTYPE to escwalk, 5d18282695 factors bottom-up visiting out for reuse (used by inlining), e5060c7f75 moves the “large type is heap allocated” check into the analysis. 1.6: variadic ... args stop always escaping because the summary was already computed (f7a39a54e9, issue 12006), 745cdc3ad7 repairs range &fixedArray, e779bfa5d2 models escape across loop levels: heap allocation is forced by address publication, unknown/large size, unknown multiplicity or reachability from heap; the bug was an object reachable from a heap closure (forced by multiplicity in a loop) not being propagated, so a newly discovered heap allocation now propagates by loop level.

1.7–1.9 added tooling and precision: 2d56dee61b adds escape explanations to -m -m (“experimental”; the author notes the graph poorly handles level adjustments and that a single flooded graph would be cleaner), bea5252a13 adds an explicit “where” to EscStep, d8c815d8b5 notes escape of parts of closure-captured vars, d603c27c6b makes large pointer literals escape, f1eed92fd0 (1.8) requires flood to run to a fixed point, 923a74ce77/83c73a85db ignore contentEscapes for marking nodes as escaping, f8a3444388 removes EscScope, in 1.9 1e29cd8c2b ignores dead code, a69754e30c unnamed parameters do not escape (issue 19687), 24e94766c0 special-cases reflect header fields, fcee3777fd moves addrescapes/moveToHeap into esc.go.

Timeline.

Inlining (inl.c → inl.go)

Span: Go 1.0 → Go 1.9 · Status at go1.17: classic budget-80 inliner, still the design at go1.17 with later additions (mid-stack inlining, closures) → continues as Inliner: budget, closures, and the interleaved inline/devirtualize driver (1.18+).

Continues in: Inlining: budget, mid-stack inlining, closures, and what became inlinable (Go 1.10 → Go 1.17)

Earlier history: Inlining (Go 1.0 → Go 1.0)

Design. The inliner works on the typechecked AST. ishairy decides inlinability by walking a function body and rejecting constructs, then calls are replaced by an assignment block plus the copied body with renamed locals (Inlvar). Pre-Go1 history: first support of empty bodies and _ arguments (7e6890a670), better line numbers (86deacc0bc), “softer criteria for inlinability” (93e547a0c2, a 95-line rework), suspending safemode while typechecking inlined bodies (e0b2ce3401), and forbidding inlining of panic and recover (cae604f734).

Go 1.1 (commits from 2013-01 to 2013-04) made it practical: inlining of variadic functions (73b83a228e), temporaries created by inlining become registerisable (dfdfba14b9), elision of duplicate allocation and a self-assignment on return (a72f9f46a2, 2eafbb8878), distinct inlined labels (09a17ca1f1), enabling inlining inside generated method wrappers (386ad0ab90: interface calls through pointer-receiver wrappers over value methods, SortInt1K -33.8%, SortInt64K -36.2%), stealing escape info (2667dcd113), and disabling inlining of method values (7b8e08617e). Bugs: _ argument side effects (ee5f59ab4f), local variable (76500b14a1), T.Method calls (bcea0dd1d0).

1.7 allowed functions with switch (without break or type switch) (e41f527f4d, issue 13071), stopped penalising ODOT and friends in the cost (060038bdd0), and cleaned global state in inl.go (e6ea01685f, ed4a27a8dd). 1.8 added the -m -m “inline explainer” (e4cae432d6), removed an escape-analysis hack from inlining (a3765723c6), typed variadic inlining (5d1b53a944), and refused cgo_unsafe_args functions (9fbfe7cca0). 1.9: functions with intrinsics are inlinable (8a9dc05312), dead if branches are ignored when costing (566e72d0ce, newly inlinable includes bits.OnesCount, (*big.Float).Sign), ishairy becomes a visitor with budget and reason (e52d317d24), calls to runtime.getcaller{pc,sp} block inlining (2397cd0fbf), more non-inlinable reporting (a246f61637), literals copied so inlined positions update (1c6ef9aeed), source positions of call arguments retained (b928e2faa8), the comment fixed to state the budget is 80 nodes (619e37bdcc; the bump had come with transitive inlining in 2015).

Timeline.

Order, walk and temporary lifetimes (order.c, walk.c)

Span: Go 1.0 → Go 1.9 · Status at go1.17: order and walk stayed the pre-SSA lowering phases through the end of this range → continues as Range-loop and range-over-func lowering in walk (1.18+) for the range part.

Continues in: Compiler front of walk/order: ordering fixes and the dev.regabi IR refactor (package walk) (Go 1.10 → Go 1.17)

Design. order turns expressions into a form where evaluation order is explicit and runtime-call operands are addressable; walk lowers high-level nodes (maps, channels, strings, interfaces, append, comparisons) to runtime calls or simple nodes. Go 1.0 history shows the early compiler fixing eval-order bugs in function args (0ffacfd995 order of evaluation in function args, 272d156357 argument ordering), static initialisation (a24a26c107, 0421cf072e, 61c83b6407), s == "" → len(s) == 0 (c9293f0cc7), *f() = 1, closed arrays, struct literals, new new &Point{1,2}, and go print() (b4bb970e18). In 2012, 5efd5624cc fixed codegen reordering with &&/||, 0a55958b52 recursion on ninit in order.c.

The decisive 1.3 work (Russ Cox) tied order to precise stack liveness. e150ca9c9a requires pointers passed to the runtime to point at zeroed, never uninitialised temporaries (chanrecv, mapiterinit temporaries change from dead to alive during the call, so they must be zeroed beforehand; maps exempt). 96d90d0981 shortens temporary lifetimes in many constructs (temporaries cleaned promptly in if/for/select/&&/||/convT2E; moved select temporary generation from select.c into order.c; re-enabled the 1-entry select optimisation, issue 7672): ambiguously live temporaries in godoc went 860 → 711 (earlier CL) → 121 → 23 (this CL, of which 15 truly ambiguous), plus 18 user-declared variables; the claim is that zeroing on function entry “should now be a rare event”. 28f1868fed made GODEBUG=gcdead=1 work with liveness: ambiguously live variables are live for the whole function after entry zeroing, liveness for all parameters, and a GC fix for scanning empty strings and nil slices (it re-enabled precise slice scanning). 5225854b74 (1.4) synthesises zero values for non-assignment context after zeroed literals became a nil node.

1.5/1.6 changed the lowering: avoid x = f() turning into tmp = f(); x = tmp for simple x (18d98bc9cb: slows some things but reduces frame size and optimiser load), 7a4022ee36 fixes live variable reuse in orderstmt, a047b6bf7d (1.8) emits assignments after calls in the right order, 1cde87b312/a8f07310e3 (1.9) ensure left-to-right assignment and string range assignment order, and c682d3239e (1.9) makes order create static read-only values for constants passed to runtime conversions instead of temporaries (fmt.Println(1, 2, 3) allocated three times), later refined to use constants directly for mapaccess_fast* (5faba3057d) and byte-sized values converted to interfaces without allocation using the runtime staticbytes table (0358367576); d9270ecb3a/febafe60d4 evaluate zero-sized operands and cheapexpr. In 1.9 many walk-time things moved out of SSA construction for concurrency (typename a0d6d3855f, itabname a33903b02c, string symbols 16df2ccded).

Timeline.

Write-barrier insertion: from gc to the SSA writebarrier pass

Span: Go 1.5 → Go 1.9 · Status at go1.17: SSA writebarrier pass with store ordering, still the design at go1.17 (hybrid barrier elision refinements later).

Continues in: Write-barrier elision and the writebarrier pass (Go 1.10 → Go 1.17)

Design docs & discussion

Design. Go 1.5’s concurrent GC required write barriers, first emitted by gc during walk. d8ee1c5a2c ([dev.garbage]) added emission of pointer write barriers; e5ef657264 and ff979626fc handled parameter copies to the heap and append(slice, slice...); a7bb393628 skipped barriers for *tmp when tmp=&PAUTO; 2a73559023 emitted the typedmemmove barrier from sgen (revcomp worse, FmtFprintfEmpty -24%); 8552047a32 generated x = append(x, v) as real instructions in gen instead of walk pseudo-Go so that, when source and destination slice are the same, only the grow path rewrites x and the fast path increments the length without a barrier; 972a478ddf stops catering for the race detector; 85dd62d5dd added missing barriers on return. In 1.7, SSA reproduced these optimisations: 6b33b0e98e avoided a spill in the append fast path and a4650a2111 avoided the barrier when appending back into the same slice, then 03e216f30d re-enabled the in-place append optimisation (the earlier attempt stored newlen too early; runtime failures in 6 of 2000 runs, now none in 6000), 934c359964 reordered slice literal initialisation so the array is known to be on the stack, 8107b0012f reads the barrier flag with a 32-bit load.

In 1.8 the decision moves to SSA: f6aec889e1 (Cherry Zhang) adds a writebarrier phase: the front end emits StoreWB, MoveWB etc. conservatively, and the pass expands them into branches and runtime calls after detecting stack writes by static location (all stack-write barriers found by the issue 17330 script were eliminated except two). c39918a049 (Austin Clements) disabled several elisions invalid under the hybrid barrier (“eliding the hybrid barrier requires that both the current and new pointer be already shaded”): go tool +0.7%, compile +1.5% in size, XJSON +7.8%; the later restore of non-pointer zero assignment is 75d367e34b, ed0b232cdc finds the last StoreWB explicitly, and 92ecd78933 adds a ZeroWB case. In 1.9: c4ef597c47 redoes the pass so write barrier stores need not end blocks (values are explicitly store-ordered), 9ebf3d5100 removes all front-end barrier decisions (SSA emits plain Stores and the pass inserts barriers using Phi/Copy-propagated knowledge), 211c8c9f1a annotates Store/Move/Zero with types for it, 5bfd1ef036 drops “volatile”, 794f1ebff7 simplifies needwritebarrier, 0ee9c46cb1 adds missing barriers for reflect.{Slice,String}Header.Data, e5c9358fe2 moves the pass after dse, 94a017f3f5 allows duplicating OpVarXXX in barrier blocks, 85b2940f5c finds remaining WB ops from the end (quadratic otherwise; issue 13554 -29.9%).

Timeline.

Switch, type-switch and select lowering

Span: Go 1.0 → Go 1.8 · Status at go1.17: binary search plus later jump tables → continues as Switch lowering: jump tables, type-switch caches, and switch lookup tables (1.19+ for jump tables).

Continues in: Switch lowering, select, and type switches in walk (Go 1.10 → Go 1.15)

Design. Pre-Go1 swt.c rewrote switches (79fa5b65cb “rewrote switch fixed bug 141”), added binary search for constant cases (820f42d977), typeswitch temporaries (2450c590e9), and later “do not compile switch on interface values into a binary search” (6b72b07016). Errors for illegal switches (769919c4ee, 13e92e4d75). Go 1.5: 4bc9badd75 made the binary search for large string switches compare length first and then contents (issue 10000), 4ce06f4b5c eliminated dead code in switches. In 1.8 Josh Snyder rewrote the case expansion: explicit default and nil cases (726190daf3), a map to detect duplicate type switch cases (bd2838be77) and constant cases (e26499153e), eliminating switch case kinds (b046786528), splitting genCaseClauses (b7ac426ee3), simplifying constant case sorting (a9266eef93), 643b9ec07c tidy-up. Select got eval order fixes (1b3244e0db) and 1.3 moved select temp generation into order (96d90d0981).

Timeline.

Interface conversions, type assertions and devirtualization

Span: Go 1.5 → Go 1.9 · Status at go1.17: → continues as Static devirtualization and shape-aware type-assertion pruning (1.18+).

Continues in: Early devirtualization and static value tracing (Go 1.16 → Go 1.17)

Design docs & discussion

Design. 77a2113925 (1.5) evaluates i == c as x, ok := i.(C); ok && x == c instead of I(c) == i, avoiding an allocation (EqEfaceConcrete -73%; the 1<<16 cutoff). 25e793d7ea inlines _, ok := i.(T) (intDataSize shrinks 22%, AssertI2E2Blank -82%); 562d06fc23 (1.8) inlines _, ok = i.(T) in a more general form and 1a7fc7b3a7 the e == T comparison; 688995d1e9 (1.8) does most conversions inline: only conversions that may allocate or need an itab remain in the runtime; 1-result asserts panic out of line. 7427f2c4bd (1.7) copies a pointer-shaped value’s two words directly. In 1.9, 4e0c7c3f61 / 295307ae78 introduces de-virtualization of interface calls: an SSA rewrite rule (InterCall) fires when the interface’s itab is a known static, found after inlining a constructor like func New() Interface { return &impl{...} }, so h := sha1.New(); h.Write(buf) becomes a static call (hits “a few dozen times during make.bash, and hundreds during all.bash”); the second adds reflect.go/config.go/rewrite.go machinery to look up the itab symbol.

Timeline.

Phi construction and the SSA builder’s scalability

Span: Go 1.7 → Go 1.9 · Status at go1.17: Sreedhar-Gao phi building for large functions, still the design at go1.17.

Design. The front end builds SSA with FwdRef placeholder values resolved later. b5c5efd5de redid forward references (resolved in place) and improved phi elimination to simplify phis of phis. Large functions were quadratic (issues 15112, 15537, 14934, 16361, 8225): 6b99fb5bea (David Chase) added a sparse algorithm using nearest-ancestor queries in a dominator tree and processing in reverse post order with a cutover for small programs (a ssa/build/stats flag exposes blocks × values to tune the cutover at 1–5 million); bc1989f115 optimised lookupVarOutgoing for blocks with one predecessor (28s → 9.6s, RSS 2.28GB → 638MB); 5a6e511c61 (1.8) replaced the sparse algorithm for large functions with Sreedhar+Gao: find the dominance frontier of each variable’s defs to place phis (O(n * #variables)), then a single tree walk matches FwdRefs to the nearest def or phi, with a cutover at 500 blocks (14934: ~10s → ~4s; 16361: ~4.5s → ~3s). 433be563b6 fixed the choice, 1cbc5aa529 and 61fb2f6d63 micro-optimised insertion (#8225 25–30% faster). 4e428907c5 (1.9) avoids generating dead blocks while building (issue 19379: -36% wall time, -28% RSS) and e00e57d67c and 5272a2cdc5 fix the dependent phi insertions.

Timeline.

Dominators, loops, CFG editing and block-level passes

Span: Go 1.7 → Go 1.9 · Status at go1.17: still the design at go1.17 (dominator tree + likelyadjust + looprotate + fuse/shortcircuit/trim).

Continues in: CFG and control-flow passes: branchelim, fuse, shortcircuit, phiopt (Go 1.11 → Go 1.17)

Design. 078ba138d3 implemented the simple Lengauer-Tarjan algorithm for dominators and post-dominators, and it needed repair: DFS was not a real DFS (d32229b3b1), then 0b6332eb54 replaced the computation with the “simple” algorithm from the TOPLAS paper with minimal changes; c87a62f32b reduced allocation. The dominator tree is shared across passes (9743e4b031, e0611b1664) and CFG-dependent computations cached (75ce89c20d). 4fa050024f made CFG editing constant time: Preds and Succs carry back-indices so edges splice in O(1), fixing quadratic behaviour in deadcode and fuse. 6ed79fbd1a removed the BlockDead state (an earlier approach was 186cf1b9ba’s PlainAndDead block kind, deleting in deadcode instead of eagerly), and 9b048527db showed why eager removal breaks callers walking the blocks. d610d304f8 makes copyelim linear; b3e577b9e7 makes fuse run end to beginning because plain-block chains moved each value O(n) times (this changed order, requiring sorting in deadstore to pass toolstash-check); 4e428907c5, c67cac0703 (degenerate control blocks if a then jmp b else jmp b), 3c26c0db39 (short-circuit empty blocks), be302e6d43 (trim blocks with multiple predecessors, phi-only blocks and mergeable blocks), 5949524fc4 (phis in fuse), 79e5ef2935 (lazy true/false values in shortcircuit) shape the CFG cleanup.

Layout: bbf8c5ce2f initial likely direction, 6b3462c784 (likelyadjust: favour looping, avoid abnormal exit/panic, avoid calls, treat returns as rare; ~0.75s of a 200s make.bash), abdb2c35b6 and 073297ff68 repair the loop finder and postorder, 39ce5907ca (1.9) rotates loops so the conditional branch is at the end (removes the JMP from inner loops, looprotate pass), 638ebb04f2 moves the whole run of contiguous header blocks, 0b6fbaae6e makes guard+rotate conditional on a GOEXPERIMENT for preemptible loops. 7f1ff65c39 (1.8, David Chase) inserted scheduling checks on loop backedges with GOEXPERIMENT=preemptibleloops (or -d=ssa/insert_resched_checks/on) for GC-latency study; 9664bc1d1f fixes phi updates for nested preemptible loops.

Other passes: f91ff1a509 adds tighten (moves values closer to uses; 84437 values in make.bash), improved in 1.8 to place values at the lowest common dominator of uses without entering loops (dd24b1098a: 40% slower pass); 305a0ac123 moves constant phi args to the phi’s predecessor; e197f467d5 simplifies boolean phis; e85265e8c2 (1.8) bool→int conversion without jumps; c03ed491fe and 6b85a45edc change regalloc around loops (load live values before a loop: Fannkuch11 -12.4%; spills moved to loop exits); 815c9a7f28 uses loop information in regalloc.

Timeline.

Compile-time performance and determinism of the SSA package

Span: Go 1.7 → Go 1.9 · Status at go1.17: the allocation caches and sparse structures remain.

Design. Early SSA was cheaper to run than to write; allocation caches and algorithmic fixes were needed: preallocated small-numbered values and blocks (2f57d0fe02), backing-store buffers for Preds/Succs/Values (056c09bb88), reused sparse sets (f962f33035, c58c20f30f), three Value args preallocated (f3a29f1f81), sparse sets with smaller element size (d35a4158ab), compilation made deterministic (686fbdb3b0), consistency checks turned off by default (5305a329d8), lazy printing (9201c86b57), and in 1.9 a5e3cac895 zeroing cached values on reset and assigning IDs at use. Dave Cheney found logProgs costing 5% of time and 12.8% of allocations (cb1f2afc99).

Timeline.

Preparing the backend for concurrent compilation (Go 1.9)

Span: Go 1.9 → Go 1.9 · Status at go1.17: completed within 1.9; no later thread.

Design docs & discussion

Design. The back end must not touch shared front-end state, so things that mutated global state during SSA construction were moved earlier into walk or into setup: f9fb4579e1 disables typPtr caching in the backend (not concurrency safe), a33903b02c evaluates itabname in walk, a0d6d3855f typenames in walk, 16df2ccded string symbols in walk, beb833f124 and 245ef3a157 look up runtime symbols in initssaconfig, 2cdb7f118a moves Frontend from ssa.Config to ssa.Func, 872db79989 adds types to ssa.Types, 7efb0779be removes scratchFpMem, 4927b9a9ff makefuncdatasym_nsym, ecc6a81617 prevents modification of ONAME/OLITERAL/OTYPE in walkexpr, c4335f81a2/0b6a10ef24 call checkwidth/dowidth in the front end, 8e36575ebe stops mutating shared nodes in orderinit. Left open: nothing noted.

Timeline.

Lowering of copies, strings and slices; intrinsics

Span: Go 1.0 → Go 1.9 · Status at go1.17: → continues as Lowering of builtins to memory ops: memmove/memclr/memequal inlining and generated equality (1.18+).

Paths not taken: Alternative slice, string and interface layouts (never landed)

Continues in: Range loops, append and map idioms in walk (Go 1.10 → Go 1.13)

Design docs & discussion

Design. String/[]byte and compare lowering: no []byte copy in concatenation (205ae07cd3) or comparison (69cd91a598), stringsCompare avoided then removed (eddd7ff3cd, c73df92be6), slicebytetostringtmp intrinsified (150de948ee), short constant strings compared by unrolling len(s)==2 && s[0]=='a' && s[1]=='b' with cutoff 6 (c9fd997524; 40.8% of constant string equalities are len 0, 14.1% len 4), improved comparison with constant strings (e4a500ce14), small arrays compared with larger loads (3bdc2f3abf; ArrayEqual 7.41 → 3.17ns), walkcompare reworked to compare elements instead of addresses (0bc94a8864), df9f4f14b9/14b0af4272 earlier fixes. Range: range-clear to memclr (7eb94db44c refactor, 58e2edafb7 lower slice clears to memclrHasPointers, 8a7f0ad0b5 typedmemclr when pointers), 6f14cada11. Map ops: existence-only lookups (43e6923131), mapaccess optimisations based on algtype (423b0cc25a), ec091b6af2 and 5d6b7fcaa1 runtime fast variants. Zeroing: a833485828 no statictmp for zeroing, c8eaeb8cba/b2e0e9688a no zeroing after newobject, a51e4cc9ce zero return parameters before move-to-heap, ee69c21747 no statictmps for SSA-able composite literals, b024ed0d94 eliminate copy for static literals. Intrinsics: sqrt (764c751ae5 in 1.5 enabling OSQRT; dbd51ce99c, 8aadcc551e), math/big.mulWW/divWW (2756d56c89), atomic Xaddint64 (964639cc33), 644c16c76c, math/bits TrailingZeros, ReverseBytes, Reverse on arm64 (d5dc490519, dd9892e31b, 42e97468a1).

Timeline.

Changes in this slice that are not part of a thread are listed on the release pages: 1.0 · 1.1 · 1.2 · 1.3 · 1.4 · 1.5 · 1.6 · 1.7 · 1.8 · 1.9

Middle end and SSA optimizer (Go 1.10–1.17)

Reader: Claude Sonnet. Cites 932 of 1,046 commits in this slice; every non-noise commit is cited, plus 9 larger commits the noise filter had set aside, which a separate gap reader described. Read the design-heavy messages and about 70 full messages. The write-barrier and CFG timelines are based mostly on subject lines.

Release digest for this slice
  • Go 1.10 — Closing the SSA backend’s first year: inlining closures with captures and refusing recover callers, location-list lifetimes, memmove-to-Move, stores to unread autos eliminated, and small-range loop simplification; walk shrank (print, select, order) and write barriers were already in SSA (threads: Inlining, Memory-op optimizations, Range loops, Write barriers, DWARF).
  • Go 1.11 — The prove pass was reworked (unsatisfiability, poset, loop BCE folded in, loopbce deleted), range loops stopped producing past-the-end pointers, location lists were redesigned, is_stmt statement boundaries and stack maps at nearly every instruction landed, and mid-stack inlining prerequisites (variadics, panic, thresholds) arrived; arm64 got CondSelect (threads: Prove, Statement boundaries, DWARF, Inlining, CFG passes, Range loops).
  • Go 1.12 — Argument stack construction moved from walk to SSA, write barriers eliminated for non-heap stores, inlining cost tuned (57, single-call bodies, wrappers), ssa.html gained sources and CFGs, and bits intrinsics expanded (threads: Late call expansion, Write barriers, Inlining, Diagnostics, Arithmetic).
  • Go 1.13 — The new escape analysis (escape.go) landed and became the default; defer records moved to the stack; signed divisibility rules and prove learned extensions/bounds facts; the rulegen rewrite began and short-circuiting/&& ordering were fixed (threads: Escape, Defers, Prove, Arithmetic, Walk/order).
  • Go 1.14 — Open-coded defers and their follow-up fixes, escape analysis performance and the new leak encoding, switch walking refactor and string-switch optimization, poset rework, statement-mark preservation across passes, checkptr, and optimizer logging for IDEs (threads: Defers, Escape, Switch, Prove, Statement boundaries, Diagnostics, Rulegen).
  • Go 1.15 — Strongly typed aux/auxint with ellipsis rules across architectures, integer-range fuse+rules replacing walkinrange, mid-stack inlining through recursion cycles, precise method-value escape analysis, and typed-rule conversions (threads: Rulegen, Inlining, Escape, CFG passes).
  • Go 1.16 — Late call expansion turned on for all call kinds (expand_calls), early devirtualization via staticValue, inlining of for loops and go/defer arguments, escape analysis of known calls, and removal of untyped rules (threads: Late call expansion, Early devirtualization, Inlining, Escape).
  • Go 1.17 — dev.regabi refactors reshaped the middle end into escape, walk and devirtualize packages over ir, capture analysis moved into escape analysis, go/defer were wrapped into argument-less closures, expand_calls grew to handle register arguments, and stack-slot alignment/branch-known-from-predecessor/phiopt improvements landed (threads: walk/order IR refactor, Escape, Late call expansion and regabi, Defers, CFG passes).

Prove pass: unsatisfiability, partial-order facts, loop bounds-check elimination

Span: Go 1.10 → Go 1.17 · Status at go1.17: still the design at go1.17 (facts table + poset, induction variables found inside prove); loop BCE merged into prove in 1.11 → continues as Prove pass and bounds-check elimination: from poset relations to a dense limit table, topo-sorted walk, and division-aware proofs (1.18+)

Paths not taken: Poset-based bounds in the prove pass (superseded)

Earlier history: Prove pass and bounds-check elimination (Go 1.7 → Go 1.9)

Design docs & discussion

Design. ssa/prove.go walks the dominator tree and keeps a factsTable of relations between SSA values for each domain (signed, unsigned, and later a per-value limit). Up to 1.10 the pass asked implication questions of the table (“does a dominating fact imply this branch’s condition?”). In 1.11 Austin Clements flipped this to unsatisfiability: branch conditions are only ever asserted, and if the table becomes contradictory the branch is dead and cut (669db2cef5). That removed factsTable.get, let derived facts (len/cap relations, fence-post implications 6436270dad, unsigned limits derived from signed ones 941fc129e2) be used regardless of the shape of the branch condition, and let the pass skip provably unreachable blocks.

The second big step was Giovanni Bajo’s 1.11 series. Loop bounds-check elimination, previously a separate loopbce pass that only told prove “a lie” about len(s)-delta bounds, was moved into prove: findIndVar is reused and the induction facts are registered when the loop block is entered (7ec25d0acf), and the old pass was deleted (f49369b67c). 5c40210987 added a transitive closure of relations through a new poset data structure (ssa/poset.go): a forest of DAGs over SSA values that records <, <=, ==, !=, detects contradictions, and “proved hundreds of conditions” in make.bash; e0d37a33ab taught it x+d < w for non-constant w and that len/cap are non-negative (633 more checks removed in std+cmd), and 3c8545c5f6/67656ba71b reduced allocations by reusing posets and improving undo. Poset development continued in 1.13/1.14: constants moved into the first DAG (490646e476), constants handled more flexibly (c3a871fde1), multiple aliases per pass (18d57bc892, 7e68f81dd8), path collapsing (c70e5475e6, which Bajo says will be fundamental when poset is used for numerical limits), a debugging mode and integrity checker (19532d04bf, 0a5116493c), inequality recording changed (90888ed97b), and a fix to use only sufficient conditions for OrderedOrEqual (3c56eb4083).

From 1.13 on contributors (mostly zdjones) taught prove about the shapes that real code produces: facts flow through value-preserving sign/zero extensions (69ff0ba798), the index is learned to be >= 0 after a successful bounds check (2d41444ad0, 92 checks removed), non-negativity comes from the poset (ddef157813), facts from OpSliceMake (87e2b34f7b), induction variables bound by other induction variables (1658263bbf) and unrolled loops (d8f60eea64), isNonNegative handles more ops (ea3bfba87c, 5fac45a320), right shifts of positive ints by width-1 become zero (2cb10d42b7), and transitive slice len/cap relations in poset (5e804ba17d, 1.17). Prove also feeds later passes: shifts are marked bounded from its bounds (9eb4590ad2, stored in AuxInt by 743fd9171f) and Ctz non-zero proofs are logged (b9785fc844).

Timeline.

Escape analysis: the esc.go fixes, the escape.go rewrite, and the move to package escape

Span: Go 1.10 → Go 1.17 · Status at go1.17: the escape.go (graph/location) design, now in its own package cmd/compile/internal/escape, handling closure conversion/capture analysis → continues as Escape analysis: call modelling, closures, func-variable callees, and solver speed (1.18+)

Paths not taken: Escape-analysis precision experiments (partially landed)

Earlier history: Escape analysis (esc.c → esc.go) (Go 1.0 → Go 1.9) · Escape analysis and stack allocation (Go 1.1 → Go 1.9)

Design docs & discussion

Design. Through 1.12 escape analysis was the 2000-line AST-walking esc.go, which handled the various ways values flow with a fixed-point over EscState and encoded parameter leaks in a small bit-field. Its 1.10–1.12 history is a series of precision and correctness fixes (loop depth of range expressions dbb1d198ab, propagating loop depth to a field 5a91c83ce8, element type for OIND of slice 3042463d61, self-assignment handling ff468a43be/859cf7fc0f/499fbb1a8a/4cf33e361a, unnamed parameters be09bdf589, anonymous receivers as non-escaping 88466e93a4).

In 1.13 Matthew Dempsky added a complete reimplementation, escape.go, behind -newescape (97c4ad4327): “focuses on simplicity”, builds a data-flow graph of EscLocations with edges carrying a dereference count, and then walks from each location to find whether its address reaches a location that outlives it (the heap location sets escapes). It was enabled by default in the same release (996a687ebb, marked RELNOTE because stricter checking may break code violating the unsafe.Pointer rules) and the flag removed in 1.14 (991b0fd46c). Fixes along the way: CONVIFACE of a non-direct interface must flow its data to the heap (0349f29a55), parameter tagging refactored and then integrated before escape analysis completes (23bf6af996, b9704872d1), and diagnostics restored (063d0f11e5, 4cde749f63, 606019cb4b).

1.14 made it faster and richer: a proper work queue replaced fixed-point re-walks with the “walk from the heap last, ensure escaping objects have an edge to heap” invariants relaxed (867ea9c17f), and graph construction was optimized by marking heapLoc.escapes, skipping edges for oversized allocations and for objects whose address flows somewhere that naturally outlives them (262046716c). The leak encoding was replaced by an EscLeaks abstraction (05a805a6de, a0894ea5b5): the old scheme could record at most one dereference and only the first 4 parameters / 6 derefs; the new one is a [8]uint8 array per parameter. 1.15 added precise handling of method values (receiver flows to the closure and method receiver, not directly to the heap, 2a2423bd05) and a stricter notion of unsafe rule #4 (f049d911e9). 1.16 added staticValue to resolve calls through never-reassigned local variables and closures (c0417df156), reasons from mustHeapAlloc (095e0f48a1), and escape of OCLOSURE/OCALLPART (82c45eb681). In 1.17 (dev.regabi) the package was split out (de454eef5f), the escape state was split (67ad695416), function literals are walked in place so closure conversion can be done during escape analysis (b1747756e3), capture-by-value is recomputed there (fd43831f44), PAUTOHEAP marking was moved out of escape into SSA construction (f97983249a), and a bug about heap-allocated results flowing to parameter summaries was fixed (a429926159).

Timeline.

Inlining: budget, mid-stack inlining, closures, and what became inlinable

Span: Go 1.10 → Go 1.17 · Status at go1.17: classic AST-based inliner in cmd/compile/internal/inline with budget 80, mid-stack inlining, for loops and closures inlinable (closures-with-captures-in-functions disabled by -d=inlfuncswithclosures=0) → continues as Inliner: budget, closures, and the interleaved inline/devirtualize driver (1.18+)

Earlier history: Inlining (inl.c → inl.go) (Go 1.0 → Go 1.9) · Inlining (Go 1.1 → Go 1.9)

Design docs & discussion

Design. The inliner works on the typechecked AST (inl.go): caninl computes a “hairiness” cost with a budget (80 nodes) via hairyVisitor; mkinlcall copies the callee body and replaces parameters with inlvars, wrapping the result in an OINLCALL. Inlined call sites are recorded in an inline tree for positions and DWARF. In this era the work was about widening the set of inlinable shapes, and tuning costs.

1.10 gave inlining closures with captures (c4b65fa4cc, mapping captured vars to temporaries like parameters), non-code nodes ignored (812b34efae), go:yeswritebarrierrec functions excluded (4e64ee423c), functions that call recover excluded (b5b35be2b8, because recover matches its caller’s argument frame pointer). 1.11 was the mid-stack year: variadic functions enabled (09d4455f45), inlining parameters refactored into explicit costs with panic inlinable and -l=4 expressed as a cost (87a18c6109), a stricter threshold of 20 instead of 80 for functions over 5000 nodes (5fc70b6fac, motivated by reflect.Value’s cost being exactly 80), constant if statements treated as free after front-end deadcode learned &&/|| (2dbf15e88e), struct == cost considers component count (b638760dad), and go:norace not inlined under -race (c6e455bb11). 1.12 encouraged “single-call bodies” by making calls cheaper (13baf4b2cd), lowered call cost from 60 to 57 (624e197c71), and removed old budget hacks that stack tracing made unnecessary (555d8c455a); the generated (*T).M wrappers also became inlinable (fa913a36a2) and mid-stack inlining tracebacks were redone (69c2c56453). 1.14 inlined reflect.Value.UnsafeAddr/Pointer only when checkptr is off (e9850462aa). 1.15 allowed mid-stack inlining within a recursion cycle, tracking already inlined functions at a call site (ed7a8332c4), made intrinsic inlining cost one (956f64888d), and logged inline failures rather than successes (fced302aa1). 1.16 allowed unlabeled for loops (497ea0610e), staticValue in inlining logic (1bcf6beec5), single-return functions with unnamed results (f2c0c2b902), functions evaluated in go/defer argument lists (4fb4291388), and set Name.Defn for inlined parameters (cced777026). 1.17 rewrote helpers on the new IR (ir.EditChildren, ir.DoChildren, ir.DeepCopy replacing inlcopy), simplified closure inlining (fad9a8b528), lowered OCONVOP cost (adb467ffd2), and gated function-with-closure inlining behind -d=inlfuncswithclosures defaulting to off (8027343b63) until a bug was fixed.

Timeline.

Early devirtualization and static value tracing

Span: Go 1.16 → Go 1.17 · Status at go1.17: a small AST pass in a new devirtualize package (Func), invoked after inlining → continues as Static devirtualization and shape-aware type-assertion pruning (1.18+)

Earlier history: Interface conversions, type assertions and devirtualization (Go 1.5 → Go 1.9)

Design. After inlining, a pass scans for interface method calls where staticValue (trace through never-reassigned local variables, conversions and inlined results) proves the concrete type, and inserts an explicit type assertion so escape analysis sees the concrete method (5cc43c51c9). It rests on staticValue (shared by escape analysis and inlining, c0417df156, 1bcf6beec5, extended through OCONVNOP in f2c0c2b902). In 1.17 the code was moved to its own package (4b1d0fe66f).

Timeline.

Open-coded defers and defer lowering

Span: Go 1.13 → Go 1.17 · Status at go1.17: stack-allocated defer records (1.13), open-coded defers (1.14), and go/defer wrapped into argument-less closures (1.17)

Design docs & discussion

Design. Defers were heap-allocated records; 1.13 allocates the record on the stack when a defer executes at most once in the function body (fff4f599fe, applies to 363 of 370 defer sites in cmd/go). 1.14 added open-coded defers (be64a19d99; the main commit lies outside this unit): inline code saves the args to stack slots at defer time and sets a bit in a stack deferBits variable; exit code tests the bits in reverse order and calls the functions, FUNCDATA_OpenCodedDeferInfo tells the runtime which are active during panics, and a trailing “deferreturn” segment supports recover. Only functions whose defers are not in loops and that do not have too many defers or exits qualify. This unit contains the follow-up fixes: non-SSAable arguments in openDeferSave (cc47b0d2cd), liveness for infinite loops (1b3a1db19f), riscv64 disabled (a858d15f11) then re-enabled (2e4f490b31). In 1.17, for the register ABI, order rewrites every go and defer into an argument-less form, defer f(x,y) becomes x1,y1 := x,y; defer func(){ f(x1,y1) } (769d4b68ef), later fixed for receivers (e27f3966bb), arguments with results (06ad41642c), nil interface calls (bd6628e62d), uintptr keep-alive (53941b6150, 1a3e968b1f, 23943a6737), method-call open defers (d206ca5cac), with a debug flag to disable open-coded defers (e617b2b0dd) and register results loaded on the open-defer return path (f5efa5a313). The wrapper closure for go is forced to escape (1b0a031680).

Timeline.

Late call expansion and the SSA side of the register ABI

Span: Go 1.12 → Go 1.17 · Status at go1.17: expand_calls pass lowering aggregate args/results to OpArg*Reg/SelectN; compiler support for GOEXPERIMENT=regabi* in place

Paths not taken: Register arguments prototype (2016) (superseded)

Design docs & discussion

Design. In 1.12 Josh Snyder moved argument stack construction from walk to SSA generation (2578ac54eb): walk previously used ascompatte, reorder1 and adjustargs to turn call args into assignments to stack slots; the SSA builder now emits stores directly. In 1.16 David Chase started “late call expansion”: new generic ops StaticLECall, Dereference, SelectN, SelectNAddr (35e413c537); a new pass expand_calls.go run just before late opt (44c0586931) rewriting these into real calls and decomposing aggregates; AuxCall gained ACArgs/ACResults for static, interface and closure calls (acde81e0a9, 39da81da5e); the translation became more “value-oriented” (396688af7e); calls were turned on step by step: static (65dfe4a772), multiple results (4d7abd7ae6), address-of static (75ea9953a8), interface (adef4deeb8), closure (8c84dcfe8c), go/simple defer (4ad5dd63a7), openDeferExit (bb46b60d29), rtcall (eb67eab861), with generic rules for late-expanded calls (7bda6154ca) and memequal(x,constant,1) (f2ee58b6bb). OpArg expansion was delayed (15f01d6ae9) and 1.17 added late expansion of return values (92cb157cf3).

1.17 (Cherry Zhang, Than McIntosh, David Chase): StaticCall returns a Results value (e25040d162), register parameters on StaticCall (95ff296a11), OpArgIntReg/FloatReg retrieval (f2df1e3c34), frame offsets checked against the ABI (5ff7ec98b7, 97b32a6724), pre-spilling pointers in aggregate register args (8462169b5a), “partially live argument spilling” (23f8c203f0), too-big-to-SSA struct passing (c015f76acb) and returning medium-large results (d891ebdce1), fake .fp arg removed in favour of getcallersp/getcallerpc (fb03be9d55, a22bd3dc73, 818f6b14b4), FuncPCABIxxx intrinsics (1421516973), -abiwraplimit eliminated (feb844f1ea), map and interface-conversion functions made ABI-insensitive (9913f821e2, 49e933fc57, 6c98ecda10 disabling fast32/64 for aggregates), and memmove inlining with register args (70ed28e5f7). Statement boundaries rescued from the new ops (4df3d0e4df) and prologue positions fixed (00d42ffc89).

Timeline.

Statement boundaries (is_stmt) and source positions through the optimizer

Span: Go 1.11 → Go 1.17 · Status at go1.17: still the design (stmt marks on src.XPos lico, preserved by passes)

Design. src.lico gained an IsStmt field (619679a397): the front end marks line starts, the optimizer tries to preserve them; “not a statement” marks lines used only for profiling; a third value marks positions that may become statements if the real one is removed. A pass after SSA building assigns the first SSA node per statement and tags noise nodes (VarKill, VarDef, Phi) as never-boundary (c2c1822b12); rewrite, deadcode, cse and nilcheck move marks forward when removing a marked value. Code generation floats statement marks to the beginning of each same-line run. Cost: binary size +3.5% and compile time +3.8% over four CLs. 1.11 also adjusted positions (b9a365681f), made the first instruction a statement (eda22a06fb), and 1.13 moved marks from jumps to targets (82af9e6749), marked atColumn1 results as statements (fd788a86b6), removed marks from secondary calls (c450ace12c), fixed index-panic line numbers (e7b7f87432). 1.14 preserved marks in nilcheck (08a87938bb, 6adaf17eaa, adc4d2cc2d), empty-block trimming (91b55b4fa3), branch elimination (e06829b977), and prove (9a926911fe), and adjusted type-switch line numbers (b8fe9f7970, 01ff213a51). 1.17 rescued marks in register-ABI ops (4df3d0e4df), fixed prologue spill positions (00d42ffc89), and avoided the entry block under -N (5e4f9b077f).

Timeline.

DWARF location lists from SSA

Span: Go 1.10 → Go 1.12 · Status at go1.17: the 1.11 design (direct SSA to location list, ssa/debug.go) remained in place

Paths not taken: DWARF type generation in the compiler (dormant)

Design. The original (dev.debug) generator produced ssa.BlockDebug, then dwarf.LocationList, using regalloc RegKills to track register lifetimes. Heschi Kreinick’s 1.11 rewrite (2075a9323d) removed RegKills (which added values and empty blocks that disturbed optimization) in favour of recomputing liveness with each op’s clobber information, and goes directly from SSA to a (mostly) real location list with block/value IDs in place of PCs. It works in two phases: a CFG-order pass storing block start/end states, and a program-order pass producing the lists; lists are finalized with a base address selection entry and ID-to-PC translation, a design that dsymutil on macOS might choke on. 39eea62340 reuses allocations through the cache so enabling lists costs near the 5% target; d58d90152b (1.10) fixed lifetimes so a value starts at the next instruction after its evaluation and ends after the first instruction following its lifetime. Naming improvements: float names kept (788aa88cd0), more names for stack slots (a03480905b), name propagation into aggregates (38c725b148), slots for incoming params (fa31093ec4, 1.12), inlined-DWARF dead local fix (1ba26a33ce).

Timeline.

Switch lowering, select, and type switches in walk

Span: Go 1.10 → Go 1.15 · Status at go1.17: the 1.14 walk/switch.go design (binary search over runs of constants, string length-first) → continues as Switch lowering: jump tables, type-switch caches, and switch lookup tables (1.18+)

Earlier history: Switch, type-switch and select lowering (Go 1.0 → Go 1.8)

Design. Matthew Dempsky’s 1.14 refactor of switch walking (8f3d9855a1) separated type and expression switch typechecking (e7e2b1c2b9) and duplicate-case detection (0b739fd4df), then defined the algorithm around: preserving order for non-constant cases; binary search for runs of integer/float/string constants and concrete types in type switches; collapsing runs of consecutive integers to range comparisons; and for strings comparing lengths first. 85fc765341 switched string ordering to length-then-equality-binary-search (equality is cheaper than ordering). 2db4cc38a0 improved concrete-case type switches. In 1.11 004260afde open-coded select registration (a [n]scase array filled inline instead of selectsend/recv calls).

Timeline.

Range loops, append and map idioms in walk

Span: Go 1.10 → Go 1.13 · Status at go1.17: still the design (lowering to element pointer with late increment; memclr idiom) → continues as Range-loop and range-over-func lowering in walk (1.18+)

Earlier history: Lowering of copies, strings and slices; intrinsics (Go 1.0 → Go 1.9)

Design. In 1.10 range loops over slices/arrays for elements of size 1, 2, 4, 8 no longer keep a separate element pointer on amd64/386, using scaled index addressing (743117a85e; shrinks cmd/go by 4 KB). In 1.11, in preparation for safe points everywhere, 837ed98d63 changed the lowering to never produce a past-the-end pointer: _p += size happens at the loop top, after the condition succeeded, built on OFORUNTIL with extra “late increment” statements stored in its List; this depended on the prove OFORUNTIL support (b812eec928). Other idioms: for k := range m { delete(m,k) } becomes a runtime clear for reflexive key types (aee71dd70b); array/slice zeroing loops refactored as memclrrange (500d79c410); append(x, make([]T,y)...) becomes growslice + optional memclr (a8a60ac2a7, extended to other length types ec4e8517cd, and go115makeslicecopy later removed 5d9b66cdc6); m[k] = append(m[k], ...) avoids the second map access (c12b185a6e); m[k] op= r avoids an extra access (7395083136); len([]rune(s)) (b9a59d9f2e); []byte("const") allocates and initializes directly (ceb0c371d9); struct/array literal map keys containing string(byteslice) avoid allocation (a0f57c3fd0); non-escaping map buckets avoided for large hints (6c102e141c, 98b6c6aca6).

Timeline.

Rewrite-rule generator, typed Aux/AuxInt, and rule syntax

Span: Go 1.11 → Go 1.16 · Status at go1.17: ssa/gen/rulegen.go builds an in-memory syntax tree with typed aux fields; untyped rules removed

Earlier history: Generic rewrite rules and rulegen (Go 1.7 → Go 1.9)

Design docs & discussion

Design. Rewrite rules in ssa/gen/*.rules are compiled by rulegen.go into rewrite<ARCH>.go. 1.11 added the | alternation operator so one rule expands into several ((And(64|32|16|8) x x) -> x, b657c00243), then used it across generic and boolean rules (f6f1750a05, f6c6781302, fdee46ee70); 1.13 let | expand multiple times in one op (48d3c32ba9) and normalized whitespace (b4fbd291b3); a countRule helper gives per-function rule-trigger summaries (0047353c53) in addition to noteRule. In 1.14 the generator was rewritten to emit an intermediate syntax tree instead of text (1a53915c6b), dropped go/types for unused-variable detection (1581bb9843), and parallelized (357e8f83f8, 3c6eaa7c0d, 7d16e44d4f; rulegen runtime went from 3.4s to about 2s). 1.15 added an ellipsis syntax for op-only rewrites (8484d409ac, with diagnostics a3fc77aa7e; the conversion shrinks cmd/compile by about 2.2%), allowed named values on the RHS (d9d88dd27f), and began strongly typed aux (28157b3292): each op declares the Go type of Aux and AuxInt (e.g. int32, *obj.LSym, ValAndOff), and rules no longer cast. A long tail of CLs converted generic, AMD64, 386, s390x, arm64 and ppc64 rules (a1b802bde7, 7580937524, 2f84caebe3, dc9879e8fd, with a7e539619e moving the last generic rules), and 1.16 removed untyped rule support (79a3482d9e). Also 1.14: allow multiple SSA block control values (9c2e7e8bed) for compare-and-branch on s390x/riscv64, and a -d=ssa/check/seed randomized checker (97a268624c, 1.15).

Timeline.

Write-barrier elision and the writebarrier pass

Span: Go 1.10 → Go 1.17 · Status at go1.17: ssa/writebarrier.go inserts barriers and drops them for provably non-heap stores

Earlier history: Write-barrier insertion: from gc to the SSA writebarrier pass (Go 1.5 → Go 1.9)

Design. Write barrier insertion moved to the SSA writebarrier pass before 1.10; the frontend’s needwritebarrier was removed (e0111bb0f4). Elision rules: copies of go:notinheap pointers need none (b78b54ff83, 3a446d8652, slice clears 6454a09a97); typedslicecopy and memclrHasPointers calls are reported/marked (afbe646ab4, c8ca793176); no barrier when writing non-heap pointers to freshly allocated memory (2140975ebd: the location holds no heap pointer if it comes from runtime.newobject, and the value is a global address or read-only data), generalised to track which parts of a new object are still zero (ca36af215f). 1.14 improved removal further (30da79d958) and 1.15 stopped emitting stack maps for barrier calls (601bc41da2). 1.16 recognised newobject under the register ABI (2fa7163b06 in 1.17), and barriers are emitted in the specified ABI (89b141c06e).

Timeline.

Memory-op optimizations: memmove, Move/Zero, dead stores, nil checks

Span: Go 1.10 → Go 1.16 · Status at go1.17: generic rules plus dse/dead auto elim passes → continues as Memory-op optimization: memcombine, dead stores, store-to-load forwarding, alias analysis, mem2reg (1.18+), Lowering of builtins to memory ops: memmove/memclr/memequal inlining and generated equality

Paths not taken: Alias analysis and store-to-load forwarding (2017–19) (superseded)

Earlier history: Nil check elimination and the redesign of nil checks (Go 1.7 → Go 1.9)

Design. Constant-size runtime.memmove calls become OpMove when it is safe with respect to aliasing (f3884680fc, 1.10; arm64 0e8b7110f6; “disjoint” requirement with an OffPtr loop c1a16b7dad and amd64 enablement c292b32f33 in 1.12; 6d00e8c478 converts when arguments are provably disjoint in 1.11; 91a05b92be prevents memmove when dst==src). Other work: dead store elimination of unread autos (744ebfde04, 54f9c0416a counting nil checks as uses, adfa8b8691/1546ab5a39 keeping autos whose address reaches a phi or control value, e8666abd98 keeping call args), A→B→C Move chains incl. VarDefs (8607b2e825, restricted to disjoint stack memory a98bb7e244), generic composite-type propagation through wide Zero/Move (f31a18ded4), Zero-to-Load forwarding fix (fbfbf03b28), Move from all-zero read-only symbol becomes Zero (4a7e363288), double zeroing avoided (396833caef), out-of-bounds reads of read-only constants ignored (443990742e, dca707b2a0). Nil checks: removed after newobject (f1517ec6e5), slice pointers treated as non-nil (933e34ac99), unsafe pointer arithmetic assumed non-nil (0c7762cd18), nilcheckelim2 copying avoided (bad5abf64d), nil checks do not move across a VarDef (64c134f90f), always nil check before interface call (fa1f52c5f6), AIX nilcheck as CMP plus store to 0 (9fe9853ae5), memmove with register args (70ed28e5f7).

Timeline.

Arithmetic strength reduction, divisibility and intrinsics

Span: Go 1.10 → Go 1.17 · Status at go1.17: generic.rules and per-arch rules

Design. Strength reduction in generic rules: x*n + y*n -> (x+y)*n (a0453a180f, fb165eaffd), unsigned div/mod by 1<<63 (99fe3f8c63), signed non-negative div/mod by a power of two (0011cfbe2b; relies on len/cap being non-negative), removal of the superfluous signed shift before sign extraction (379d2dea72), a - b == 0 -> a == b (4a1baf8bd1, 1141 hits in make.bash), bit-mask folding over shifted-away bits (098208a0d9). Divisibility checks without division: signed x%2^k == 0 by masking (68819fb6d2, 44343c777c, ~55% faster on amd64) and the general modular-inverse method of Granlund–Montgomery and Hacker’s Delight 10-17 (4d9dd35806, x%c==0 by multiply, add, compare, rotate for even divisors); magic multiply for unsigned values below 1<<16 on 32-bit architectures (c1afbf69c7); bits.Div64(0, lo, y) reduced (68d4b1265e). Constant propagation through math.Float*bits (9da29b687f, 95b146e8eb), x+0 not folded for floats (c6118af558), NaN constants forbidden in SSA (2aa7c6c548, cd9fd640db, 83e288f3db), constant-fold Ctz (82253ddc7a), AND instead of SHR+SHL pairs (9ec7074a94, reasoning: large masks need a MOV not on the critical path). Intrinsics added: math.Sqrt on 386 (9ee78af806) and riscv64 (1eb66be1b9), RoundToEven on s390x (4745604bcb), Ctz8/16 and Len8/16/32 ops on amd64 (54dbab5221, 1d321ada73, c5f0104daf), bits.Mul (9eb53ab9bc), bits.RotateLeft on amd64 (5ac2476748), bits.Div on amd64 (319787a528), mulWW on ppc64x and s390x (1e8ecefcd5, ac2ceba01a), runtime/internal/sys bit funcs aliased after generation (830ee4792e), atomic And/Or on RISCV64 (42c25e65f3), FuncPCABIxxx (1421516973).

Timeline.

CFG and control-flow passes: branchelim, fuse, shortcircuit, phiopt

Span: Go 1.11 → Go 1.17 · Status at go1.17: branchelim on amd64/arm64, strengthened shortcircuit/fuse/phiopt → continues as Branch-free lowering: CondSelect-into-math and ARM64 CCMP (1.18+)

Earlier history: Dominators, loops, CFG editing and block-level passes (Go 1.7 → Go 1.9)

Design. branchelim converts simple if/else diamonds into CondSelect: introduced for arm64 CSEL with the condition code in aux (2d0172c3a7, 1.11; “go1 Gzip benchmark more than 10% faster”), tuned on amd64 where CMOV has latency 2 by limiting the number of CondSelects and counting flag recomputation (25813f9f91), run after a “plain fuse” (a55f3ee46d, elimIf 4244→4288) and not used to compute load addresses (0b79dde112). fuse was extended to join blocks (cfbf375a81), process blocks of only dead values (ddb5d0db37), invalidate the CFG when fuseIf triggers (e7b7f87432), merge range checks (e37cc29863), and handle more shapes of fuseBlockIf (051bf37833, 1.17). shortcircuit was improved (52ae04fdfc: use v.Uses == 1 instead of live-across-block values), loop-free (4ad643d208), able to handle additional phis (ade0811dc8, 64dcef3045), reimplemented using fuse (0239a5c478), guarded against infinite loops (2393d16147), disabled for intertwined phi values (771c57e68e) and rare looping CFGs (04903476fe). New in 1.17: the derivable-known-branch optimization (92d1afe989, redirect an inbound edge when the successor’s control value is known from the predecessor’s; example runtime.atoi) and a stronger phiopt (ba6bd967d2, aebc0b473e fix). Also: tighten after lowering (3a9e4440fd), phi tighten after critical (c1544ff906), deadcode before lowered CSE (260e3d0818), deadcode before nilcheck (adc4d2cc2d), always tighten and deduplicate tuple selectors (ac743dea8e), 2 control values per block (9c2e7e8bed).

Timeline.

Compiler front of walk/order: ordering fixes and the dev.regabi IR refactor (package walk)

Span: Go 1.10 → Go 1.17 · Status at go1.17: walk, escape, devirtualize, ssagen are separate packages over the new ir node types

Earlier history: Order, walk and temporary lifetimes (order.c, walk.c) (Go 1.0 → Go 1.9)

Design. Throughout, order.go (introduces temporaries, enforces evaluation order) and walk.go shrank as work moved to SSA conversion: callnew (23b476a3c8), argument stacks (2578ac54eb), print lowering simplified (8b68fafd1f, 385ca9ae9a), OCONVIFACE simplified (fe2c588b1c), strings and slices passed by value to convT2{E,I} (0e9f8a21f8; frame of an fmt.Printf caller shrinks 200→136 bytes), convT64/32 buglets (ca8540affd, bb76193a7f), and order reuses temporaries (389e942745) and types all temporaries (63e964e174). Correctness fixes record the order semantics: OASOP (34db5f0c4d), short-circuit &&/|| rewritten as r := a; if r { r = b } (4a9064ef41), select evaluation order (14305527f6), defer delete rewrite via wrapper (2c0c68d621), map assignment with panicking RHS (dd7cbf3a84). 1.14 refactored order for OAS2 (ba6aeb6ce8, e79c23822c), 1.15 moved variadic fixing from walk to order (681ba43077) and 9f4dd09bf5.

In 1.17 (dev.regabi, Russ Cox/Matthew Dempsky): walk was split out of gc (e4895ab4c0), walkexpr1 (1,164 lines) and walkstmt split by case (3f04d964ab), SSA separated from other phases (e999c17022), many ir.Node uses replaced with *ir.Name (dbf2fc8cff, 46b6e70e3b), reorder3 reduced from O(N^2) to O(N) (4c215c4fa9, merged with ascompatee in c98548e110), walkReturn simplified (5d80a590a2, d36a6bf44d, f2e6dab048), OMETHEXPR desugared during walk (135ce1c485), stack temps simplified (ba0e8a92fa, 7ce2a8383d), Node.HasCall replaced by walk.mayCall (78e5aabcdb), variable sizes computed in walk (8c5aa42c79), PAUTOHEAP, stkobj symbols, and stack objects moved to SSA/liveness (8b2efa990b, f97983249a, 14537e6e54, 422f38fb6c), functions compiled before closures (d6ad88b4db), non-concurrent compiles made deterministic (5203357eba), typecheck information exported (1129a60f1c).

Timeline.

Diagnostics for the optimizer: ssa.html, GOSSAFUNC and optimizer logging

Span: Go 1.10 → Go 1.17 · Status at go1.17: still the tooling at go1.17

Design. GOSSAFUNC produces ssa.html. 1.10 added line numbers to values and blocks (edcf2d0cd6); 1.11 added expandable columns, colors and a README for the package (5eb98b3c30, b459e00454, ec60e4a0d2, aad71d3163); 1.12 added Go source and inlined-function source columns (8c0425825c, 9e2a04d5eb), AST display (4cc027fb55), control-flow graphs via dot (3068fcfa0d), collapsible blocks (1ac84999b9), and cached env lookups (e34f660a52); 1.15 added dark mode, combined columns, block auxints (1e9665da8f, afc480bab4, a4451e1143), 1.16 package-suffix matching and directory output (bca0b44629, f4cbf3477f), 1.17 well-formed suffix matching (70deaa33eb). For IDEs, 1.14 added a framework to log optimizer (non)actions as LSP/JSON (cd53fddabb), covering nilcheck, bounds checks (46c9fd03a5), inlining (4d0ed149ff) and escape analysis (298be61f3c, explanations 06314b620d), with path expansion (a56d755f41). -d=checkptr (1.14: 80a6fedea0, dc72a2f95f, b6b984fcff, 46aa8354fa) instruments unsafe.Pointer arithmetic from walk.

Timeline.

Changes in this slice that are not part of a thread are listed on the release pages: 1.10 · 1.11 · 1.12 · 1.13 · 1.14 · 1.15 · 1.16 · 1.17

Compiler: middle end and SSA optimization (Go 1.18–1.28)

Readers: Claude Opus for the first 15 threads, then a Claude Sonnet gap pass that added the last 7 threads and the per-release items. Cites 931 of 1,216 commits; 186 small fixes are covered only by grouped items.

Profile-guided optimization (PGO): hot-call inlining, devirtualization, profile preprocessing

Span: Go 1.20 → Go 1.28 · Status at HEAD: on whenever a profile is supplied; drives inlining and both kinds of devirtualization; 1.28 also accepts Linux perf profiles

Paths not taken: PGO beyond inlining: block layout, edge profiles, function ordering (dormant)

Design docs & discussion

Design. The compiler reads a pprof CPU profile and turns it into a weighted call graph. Since 1.23 the IR-independent part lives in cmd/internal/pgo, which cmd/compile and the cmd/preprofile tool share. Each edge is a NamedCallEdge: caller, callee, and the call site’s line offset from the caller’s Function.start_line. Only the last two frames of each sample count as an edge. The serialized form is sorted by weight. cmd/compile/internal/pgoir (named pgo until 1.23) binds those names to ir.Funcs in an IRGraph. Callees outside the current package are looked up in export data. A call site is “hot” if its edge is in the top pgoinlinecdfthreshold percent (default 99) of cumulative edge weight. Hot call sites get an inlining budget of pgoinlinebudget (default 2000) instead of 80.

PGO devirtualization (devirtualize/pgo.go) works on interface method calls and, since 1.22, on calls through function values. It picks the hottest concrete callee recorded for the call site and rewrites i.Foo() into if c, ok := i.(Concrete); ok { c.Foo() } else { i.Foo() }. Inlining can then inline the direct branch. From 1.22 devirtualization and inlining run interleaved, so a devirtualized call can be inlined in the same pass. The debugging knobs are -d=pgodebug, -d=pgoinline, -d=pgodevirtualize and pgohash; pgohash bisects individual PGO decisions.

Timeline.

Inliner: budget, closures, and the interleaved inline/devirtualize driver

Span: Go 1.18 → Go 1.28 · Status at HEAD: classic budget-80 inliner with closure, parameter-call and hot-path discounts; runs interleaved with devirtualization (inline/interleaved); functions calling recover became inlinable in 1.28

Earlier history: Inlining (inl.c → inl.go) (Go 1.0 → Go 1.9) · Inlining: budget, mid-stack inlining, closures, and what became inlinable (Go 1.10 → Go 1.17)

Design docs & discussion

Design. The inliner in cmd/compile/internal/inline gives each function a cost with hairyVisitor and compares it to inlineMaxBudget (80). It then walks call sites and replaces each inlinable call with an OINLCALL body. Since 1.18 walk flattens that body (c45d0eaadb). Since 1.20 unified IR does the body copying (noder reader inlCall), and transitive inlining happens only after the AST has been edited.

The cost model has gained discounts over time:

Big functions (isBigFunc) cap callees at 20. Recursion is detected by walking the inline position stack, not by a separate check (cad4dca518).

Since 1.23 cost analysis (CanInlineFuncs) is separate from applying inlining. Since 1.24 the “interleaved” driver in inline/interleaved alternates devirtualization and inlining until a fixpoint, and honors b.Loop and go/defer restrictions.

Timeline.

Escape analysis: call modelling, closures, func-variable callees, and solver speed

Span: Go 1.18 → Go 1.28 · Status at HEAD: cmd/compile/internal/escape resolves calls through closures and func variables, has a heavily optimized solver, and owns go/defer wrapping; the zerocopy string→[]byte conversion is controlled by a debug flag

Paths not taken: Escape-analysis precision experiments (partially landed)

Earlier history: Escape analysis (esc.c → esc.go) (Go 1.0 → Go 1.9) · Escape analysis: the esc.go fixes, the escape.go rewrite, and the move to package escape (Go 1.10 → Go 1.17)

Design. Escape analysis builds a data-flow graph of locations connected by holes with dereference counts, then solves it. In the solve phase, walkAll/walkOne repeatedly walk the graph from each root and propagate the escapes, persists, mutates and calls attributes.

In 1.18 (dev.typeparams), call handling moved from order.go into escape (call.go). Escape now rewrites go/defer calls into argument-free wrapper closures, handles method-expression calls, and wraps constant arguments without allocating. In 1.22 the go/defer normalization moved to typecheck, and ir.CallExpr.GoDefer marks such calls.

In 1.22 the location attributes became a locAttr bitset. Indirect closure calls (func(){...}() stored in a variable) are tracked statically, and so are flows into *p = ... and mutating builtins. The mutates attribute lets []byte(s) reuse the string’s bytes when the result is never mutated (the zerocopy optimization).

From 1.25, the ReassignOracle lets escape find the static callee or static value of variables assigned once. In 1.27 this was generalized to FuncAssignments: when a func variable has several known callees, their parameter escape behaviours are merged through a teeHole.

Solver speed became a theme from 1.25 on, driven by typescript-go/internal/checker, a package where escape analysis dominated compile time:

Timeline.

Allocation avoidance: stack-allocated slice backing stores and allocation-free literals

Span: Go 1.21 → Go 1.28 · Status at HEAD: variable-size make and append use a 32-byte stack buffer when the slice does not escape; a move2heap slice pass handles slices that escape only at the end of their lifetime; runtimefreegc is off by default; bug fixing ongoing in 1.28

Design docs & discussion

Design. Three mechanisms keep allocations off the heap.

First, constant and zero-value interface conversions. Escape analysis propagates constants found by ReassignOracle, so a conversion any(x) with a known x reads a static read-only global instead of allocating. Zero values use the global zeroVal. A composite literal converted to an interface becomes a read-only global when all its fields are constants. Debug hash literalallochash.

Second, variable-sized make. When the result does not escape, walk emits if cap <= K { var arr [K]E; s = arr[:len:cap] } else { s = makeslice(...) }. K is 32 bytes, tunable by a debug flag, and the decision can be bisected with variablemakehash. append to a non-escaping nil slice gets a stack backing store in the same way. Lengths and capacities that are provably constant are rewritten to literals first, so the plain constant-size path applies.

Third, the slice pass (cmd/compile/internal/slice, 1.26). It tracks whether a slice variable exclusively owns its backing store. It inserts move2heap at the point where ownership ends, such as a return, so a slice built with append in a loop lives on the stack until it escapes. A related alias analysis can call runtime.growsliceNoAlias, which frees the old backing store right after growth; this is gated by GOEXPERIMENT=runtimefreegc. The prove pass learned that both arms of the resulting Phi(SliceMake, SliceMake) share the same len and cap.

Timeline.

New inliner heuristics (GOEXPERIMENT=newinliner, inline/inlheur)

Span: Go 1.22 → Go 1.28 · Status at HEAD: experiment exists, off by default, never enabled; gated by buildcfg.Experiment.NewInliner; only maintenance fixes since 1.22

Design docs & discussion

Design. Than McIntosh’s inlheur package computes function properties and stores them in export data, about 2–3% larger (323cf73091):

For each call site, the inliner builds a CallSite table with flags (in a loop, on a panic path, in an init function). It then scores the call: the callee’s size cost plus additive adjustments. At HEAD, scoring.go adjValues holds panicPathAdj +40, initFuncAdj +20, inLoopAdj −5, passConstToIfAdj −20, passConcreteToItfCallAdj −30, passInlinableFuncToIndCallAdj −45, returnFeedsInlinableFuncToIndCallAdj −40, and others.

Because a function costing more than 80 may still be inlined at a call site with a low score, CanInline relaxes its cutoff under the experiment. The 0b07bbd2be message ties this to export data; the code allows up to 160. After call sites exposed by inlining are rescored, a function whose largest possible score still exceeds the budget loses its inlinability.

Debugging uses -d=dumpinlfuncprops, -d=dumpinlcallsitescores and -d=inlscoreadj=adj:value/....

Timeline.

Prove pass and bounds-check elimination: from poset relations to a dense limit table, topo-sorted walk, and division-aware proofs

Span: Go 1.19 → Go 1.28 · Status at HEAD: prove (ssa/prove.go, moved to ssacompile/prove in 1.28) keeps a per-value signed/unsigned limit table plus a relation poset; it walks blocks in topological order; divisibility and divmod rewriting run after it

Paths not taken: Poset-based bounds in the prove pass (superseded)

Earlier history: Prove pass and bounds-check elimination (Go 1.7 → Go 1.9) · Prove pass: unsatisfiability, partial-order facts, loop bounds-check elimination (Go 1.10 → Go 1.17)

Design docs & discussion

Design. Prove walks the dominator tree. Branch conditions and value definitions add facts to a factsTable, which then removes bounds checks, nil checks, and impossible branches. It also rewrites operations whose result it can determine.

Keith Randall’s 1.24 rewrite (553443d41f) rests on one observation. Relations between pairs of SSA values may need O(n²) space, but relations between a value and constants need only O(n). So every value gets a dense limit entry, a slice indexed by value ID that holds signed min/max and unsigned umin/umax. The poset keeps only the value-to-value orderings. Once the constant logic moved into the limit table (3b96eebcbd), the poset’s constant handling was ripped out (b97971ea2e). flowLimit then computes limits from operands for each op. Jorropo added rules over two cycles: bit-length, trailing zeros, XOR, OR, Mod/Div, truncation, complement, negation, min/max, popcount, and shifts.

Later structural changes:

Timeline.

Known-bits pass (bitwise constant folding)

Span: Go 1.27 → Go 1.28 · Status at HEAD: known bits pass in the default pipeline, run after prove and before late opt; ops are added incrementally

Design docs & discussion

Design. Jorropo’s pass tracks, per SSA value, which bits are known to be zero and which are known to be one. Its main target is the defer bitfield: in a function with three defers where the middle one always runs, the branch on that bit can now be removed. It was hit at 93 unique source locations while building std.

It is deliberately separate from prove’s limits. Letting limits and known bits tighten each other would make the code complexity “multiplicative”. It runs after prove, because removing never-taken branches gives it more to work with, and before late opt, so the generic rules can clean up users of folded values.

Recursion depth is bounded by a topological order. That order now comes from an O(n) toposort shared with prove (fc44886932). Prove’s no-op And/Or removal has a known-bits counterpart in 1.28.

Timeline.

Memory-op optimization: memcombine, dead stores, store-to-load forwarding, alias analysis, mem2reg

Span: Go 1.20 → Go 1.28 · Status at HEAD: memcombine, dse, dead auto elim and elim unread autos are in the default pipeline; type-based disjoint is used by CSE and DSE; mem2reg (1.28) handles single-block scalars only

Paths not taken: Alternative slice, string and interface layouts (never landed) · Alias analysis and store-to-load forwarding (2017–19) (superseded)

Earlier history: Memory-op optimizations: memmove, Move/Zero, dead stores, nil checks (Go 1.10 → Go 1.16)

Design. SSA memory is a chain of Store/Move/Zero values threaded through a memory argument. Several passes shrink that chain.

Timeline.

Lowering of builtins to memory ops: memmove/memclr/memequal inlining and generated equality

Span: Go 1.18 → Go 1.28 · Status at HEAD: constant-size memmove/memclrNoHeapPointers become Move/Zero; runtime.memequal is the MemEq SSA op, expanded inline for small constant sizes; equality functions are generated per “signature” (1.27); preemptible memclr was reverted

Earlier history: Lowering of copies, strings and slices; intrinsics (Go 1.0 → Go 1.9) · Memory-op optimizations: memmove, Move/Zero, dead stores, nil checks (Go 1.10 → Go 1.16)

Design. Walk and the generic rules turn runtime calls with constant sizes into SSA memory ops:

Since 1.26, runtime.memequal is the MemEq op, which has no memory result, so CSE can merge repeated comparisons. It is lowered late to LoweredMemEq (dda7c8253d). Sizes 3–16 compare two overlapping chunks; sizes 17–32 compare 16 bytes plus the remainder (c7258178cd).

Struct == either inlines field compares or calls a generated function. The choice used to depend on field count and since 1.20 depends on estimated cost (6605686e3b).

In 1.27 Keith Randall changed generated equality functions to one per “signature” (30dff416e4). A signature is a summary such as M8SI (8 bytes of memory, a string, a nonempty interface). Types with the same signature share one function: 634 → 286 functions in the go binary, ~1% smaller.

Timeline.

Stack-slot merging (mergelocals)

Span: Go 1.23 → Go 1.28 · Status at HEAD: on by default when optimizing (since 1.23); address-use analysis tightened in 1.28

Design docs & discussion

Design. Than McIntosh’s liveness.MergeLocals lets AllocFrame overlap the stack slots of AUTO variables that are never live at the same time. Candidates must be larger than 3×PtrSize. A dedicated liveness pass partitions them into “can overlap” classes.

The 1.23 rework (e01b1eb289) changed the rules. A smaller variable can share a larger one’s slot, and non-pointer variables can share pointer-variable slots. Candidates are sorted by pointerness, alignment, size and name. GC shape is no longer compared, because two variables sharing a slot are never both live across a call. On the Sweet benchmarks, total bytes saved rose from 91,256 to 124,336.

Timeline.

Switch lowering: jump tables, type-switch caches, and switch lookup tables

Span: Go 1.19 → Go 1.28 · Status at HEAD: integer and string-length switches use jump tables (off under -N and retpolines); large type switches jump on type-hash bits behind per-site runtime caches; constant-returning switches become lookup tables (1.27)

Earlier history: Switch, type-switch and select lowering (Go 1.0 → Go 1.8) · Switch lowering, select, and type switches in walk (Go 1.10 → Go 1.15)

Design docs & discussion

Design. In 1.19 Keith Randall added BlockJumpTable. The 1ba96d8c09 message frames the tradeoff: a jump table is a single indirect branch, which can choke the branch target predictor, while a binary search spreads predictor state across many branches and can win when case selection is repetitive. Jump tables are therefore used only above a minimum density and case count.

String switches dispatch on the length first, so the outer switch can use a jump table (3d8cb26504, −9% on both predictable and unpredictable benchmarks). Interface→concrete type switches with many cases use a jump table on bits of the type hash (4711299661, −10.9% predictable, −17.6% unpredictable).

The bigger 1.22 change was per-site runtime caches. A type switch on an interface (39263f34a3), a type assertion to an interface (cbcf8efa5f) and convI2I (afd7c15c7f) all consult a small hash table of sampled concrete types before calling into the runtime. The table records the matched case and the resulting itab, so a repeatedly seen type avoids the runtime call. These caches use the new internal/abi runtime type mechanism (9ab5121691).

In 1.27 qmuntal’s lookup-table lowering turned switches whose cases are integer constants and whose bodies just return constants into an indexed load from a static table (2a902c8a8a). It was then extended to all constant return types, fallthroughs, and (in 1.28) assignments.

Timeline.

Static devirtualization and shape-aware type-assertion pruning

Span: Go 1.18 → Go 1.27 · Status at HEAD: devirtualize.StaticCall uses a flow-insensitive concrete-type analysis that follows multiple assignments, assertions and conversions; impossible type assertions in shaped generic code are dropped

Earlier history: Interface conversions, type assertions and devirtualization (Go 1.5 → Go 1.9) · Early devirtualization and static value tracing (Go 1.16 → Go 1.17)

Design. Static devirtualization rewrites i.M() into a direct call when the concrete type of i can be determined statically. It runs inside the interleaved inline/devirtualize loop, so the direct call can then be inlined. go/defer calls are excluded.

Until 1.26 it relied on ir.Reassigned: an interface variable assigned more than once was given up on. Mateusz Poliwczak’s rewrite (de9da0de30, #64824) tracks all assignments to local (PAUTO) interface variables and follows type assertions and interface conversions. It devirtualizes when every assignment yields the same concrete type.

A related 1.27 change by Jake Bailey works on generic shapes. A type assertion or type switch on any(x), where x has a shape type, cannot match a case whose type has a different shape, so those cases are removed from each instantiation (343fbe2971).

Timeline.

Loop variable per-iteration semantics (loopvar)

Span: Go 1.21 → Go 1.27 · Status at HEAD: per-iteration semantics for files at go1.22+; GOEXPERIMENT=loopvar and -d=loopvar remain for testing; cmd/compile/internal/loopvar does the rewrite

Design docs & discussion

Design. loopvar.ForCapture rewrites a for/range loop whose iteration variables are captured by a closure or have their address taken. For range loops it renames the variable to a temporary and prepends x := tmp_x to the body. Three-clause loops get extra copying around the post statement.

Variables that are not captured are left alone. So are loops whose escaping closures run only on a path that returns: the “return-in-loop” optimization (23e8f43cb1). The per-loop decision is stored in the DistinctVars flag on ForStmt/RangeStmt, so it survives inlining across packages with different language versions.

Debugging goes through -d=loopvar=N and loopvarhash bisection. The bisect tooling (internal/bisect) was co-developed here to find which loop a semantic change broke.

Timeline.

testing.B.Loop support in the compiler (bloop)

Span: Go 1.24 → Go 1.28 · Status at HEAD: a bloop pass wraps assignments and call results inside for b.Loop() bodies in runtime.KeepAlive; inlining is no longer suppressed

Design docs & discussion

Design. for b.Loop() { ... } should measure the loop body without the compiler optimizing it away. The first approach (1.24) disabled inlining and devirtualization inside such loops in the interleaved inliner (fe2da30cb5).

In 1.26, Junyang Shao’s cmd/compile/internal/bloop pass replaced this. It implements the keep-alive semantic designed in #61515: the left-hand-side names of assignments, and the results (or arguments, for calls with no results) of calls in the loop body, are wrapped in runtime.KeepAlive. That blocks DSE and DCE but still allows inlining (22f24f90b5, #73137).

The TODO in bloop.go asks whether a dedicated intrinsic lowering to OpVarLive would be better than reusing KeepAlive.

Timeline.

Range-loop and range-over-func lowering in walk

Span: Go 1.20 → Go 1.28 · Status at HEAD: slice/array range loops lower to an incremented element pointer guarded against past-the-end values; range-over-int and range-over-func are lowered; range-clear idioms become memclr

Earlier history: Order, walk and temporary lifetimes (order.c, walk.c) (Go 1.0 → Go 1.9) · Range loops, append and map idioms in walk (Go 1.10 → Go 1.13)

Design docs & discussion

Design. walk/range.go lowers range over a slice or array. It loads len once and increments a pointer to the element. OFORUNTIL was removed in 1.20 because conservative scanning of the top stack frame makes a past-the-end pointer harmless (661146bc0b). Keith Randall then had to keep the incremented pointer from escaping into a live past-the-end value (8477562ce5).

for i := range a { a[i] = zero } is recognized as a clear, and since 1.28 only when the target does not depend on the index (505c369188). Range-over-int (a94347a05c) reuses this lowering.

Range-over-func is rewritten by cmd/compile/internal/rangefunc, outside this bucket. The middle end has to cooperate in several places:

Timeline.

Sparse conditional constant propagation (sccp)

Span: Go 1.21 → Go 1.27 · Status at HEAD: sccp pass in the default pipeline; convergence sped up in 1.27

Design. sccp implements the classic lattice-based sparse conditional constant propagation over SSA values and reachable edges. It finds constants and dead branches that separate constant folding, propagation and dead-code elimination cannot find together. It adds generic ops/aux support in genericOps.go and a ~700-line pass.

Timeline.

Loop-invariant code motion (licm)

Span: Go 1.27 → Go 1.28 · Status at HEAD: licm pass exists with an allow-list of liftable ops; a related “merge conditional branches” pass was disabled late in 1.27

Paths not taken: Loop-invariant code motion before 2026 (landed later)

Design. ssa/licm.go (moved to ssacompile/licm.go by the 1.28 package split, 9669e08572; plus a new uses.go use-tracking helper) hoists loop-invariant computations (b := a + 15 inside a loop) to before the loop. It originated from Ilya Tocar’s 2016 CL 27656, “heavily modified”. Hoisting an op above a guarding check is unsafe for ops that fault or misbehave (bounds, nil, CPU-feature gating, zero denominator).

Timeline.

Branch-free lowering: CondSelect-into-math and ARM64 CCMP

Span: Go 1.18 → Go 1.28 · Status at HEAD: branchelim rewrites CondSelect into arithmetic on all arches; arm64 uses CCMP chains for logical AND conditions

Earlier history: CFG and control-flow passes: branchelim, fuse, shortcircuit, phiopt (Go 1.11 → Go 1.17)

Design docs & discussion

Design. branchelim turns small if/else diamonds into CondSelect. Since 1.26, generic rules then rewrite CondSelect of simple forms into arithmetic: if y { x++ } becomes MOVBLZX; ADDQ instead of LEAQ+TEST+CMOV; if y { x *= 2 } becomes a variable shift (SHLXQ). On arm64 a new SSA pass converts conditionals joined by logical AND into CMP/CCMP sequences, using a new aux type ARM64ConditionalParams (condition code, NZCV flags, optional constant).

Timeline.

Constant-folding loads from type metadata and dictionaries

Span: Go 1.21 → Go 1.27 · Status at HEAD: rewriteFixedLoad folds loads from read-only abi.Type, abi.PtrType, itab and dictionary symbols

Design docs & discussion

Design. When a load reads a field of a known type descriptor/itab/generic-dictionary symbol, the SSA rules fetch the constant from the symbol’s relocations or data instead of loading at run time. This lets generic code and type switches drop dictionary indirections, devirtualize and prune.

Timeline.

SSA values for zero-sized and multi-field direct-interface types

Span: Go 1.22 → Go 1.27 · Status at HEAD: all zero-sized values are SSA-able; structs with one pointer field plus zero-sized fields are stored directly in interfaces

Design. Structs/arrays that are pointer-shaped (one pointer-containing field, any number of zero-sized ones) can now be interface data words directly, and zero-sized values need no registers.

Timeline.

ARM64 load/store pairing (LDP/STP)

Span: Go 1.25 → Go 1.28 · Status at HEAD: SSA pair pass plus a post-genssa spill/reload fusing step

Design. An SSA pair.go pass before regalloc merges adjacent loads/stores into paired ops (saves binary size, a little icache). Spills inserted later by regalloc are fused by an ArchInfo.SSAGenFinish hook run after branch targets and frame size are final.

Timeline.

SSA package decomposition and compiler-infrastructure refactor

Span: Go 1.28 → Go 1.28 · Status at HEAD: cmd/compile/internal/ssa split into subpackages (issue #80409), still in progress

Design docs & discussion

Design. In July-August 2026 the monolithic ssa package is carved up: Op moved to an op package (ef0bd1f8df), BlockKind to block, Register to ssabase (088195b6fb), the core to ssacore (9b32c1225c), the HTML writer to ssahtml (c3b712a52b, with HTMLWriter removed from Func a91f860d7d), debug to ssadebug (a8585ce762), config to ssaconfig (72aa6db794), ssa.Compile placed behind an interface (22c106e5e2), the true core moved back as ssa and the old driver renamed ssacompile (9669e08572, d702d25226), rewrites split into ssarewrite packages (a98e6e1013), helpers split (64ebd027a7), and identifiers exported (0d74cff9d1, 8b1f275ac7, eeec749e5f). Pass framework changes: -d=ssa/phase/@keyword=value option (bef396c339), flags for the refactor (cc5a4de6bf), one pass struct reused (ed40f70205), generator split phase removed (9904c2c7c4). Walk gets a walkState carrying the current function instead of global ir.CurFunc (1824d86c9d, 02b07dbc17, 2dbb664b76, d5a3ff60b7, plus CurFunc injection into typecheck/compare/reflectdata/staticinit 066f73ad47, dc0e349ad1).

Timeline.

Changes in this slice that are not part of a thread are listed on the release pages: 1.18 · 1.19 · 1.20 · 1.21 · 1.22 · 1.23 · 1.24 · 1.25 · 1.26 · 1.27 · 1.28