Middle end and SSA optimizer
Thread map, Go 1.0 – 1.17
| Thread | 1.0 | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 | 1.7 | 1.8 | 1.9 | 1.10 | 1.11 | 1.12 | 1.13 | 1.14 | 1.15 | 1.16 | 1.17 | Status 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
| Thread | 1.18 | 1.19 | 1.20 | 1.21 | 1.22 | 1.23 | 1.24 | 1.25 | 1.26 | 1.27 | 1.28 | Status 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
cgencode 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),
uintptrignored 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 doc New SSA Backend for the Go Compiler listed in Go: A Documentary
- talk Generating Better Machine Code with SSA. GopherCon 2017. Jul 24, 2017. listed in Go: A Documentary
- talk All About The Go Compiler. Jul 20, 2017. listed in Go: A Documentary
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.
- Go 1.7 — Done: The whole dev.ssa history is listed under 1.7. 2015-05: global variables (
23df95b9b5), code generation from SSA (083a646f63), reorganised opcode tables (b3137966db), dead-store pass (8d32360bdd), string ops (f7f604e284), block rewriting rules (a9cec30fdc), removal of thecgenpass (7bdecbf840). Summer 2015:Auxintfield (8f22b5292f), per-Value line numbers (81ccf508aa), nilcheckelim (2a846d2bd3), call opcodes (290d8fc14a), Lengauer-Tarjan dominators (078ba138d3), constant bools, loops,OANDAND/OOROR(e81671115c), width/sign-specific opcodes (67fdb0de86),Constsplit intoConst8..64(9cb332efd4), the HTML printer (35fb514596), per-pass timing (24dcede1c0), initial likely-direction (bbf8c5ce2f), the first global register allocator replacing the per-block one (0b46b42943), first floating point (997a9f32b0), division and high-multiply (a45f2d8f28,67cbd5b51d), decompose pass (9f954db170), complex arithmetic (3a9d0ac3c8), explicit exit blocks vs implicit ones (f5c53e0deb). Autumn: append (9d22c101f5), type assertions (269baa981e), struct/array literals (d3886906b1), stack-based pointer arithmetic withoutuintptr(582baae22a), arguments start spilled (02f4d0a130), better register allocation (75102afce7), panic deduplication (74e568f43a). Dec 2015–Feb 2016: compilation trigger (5b355a7907), structs SSA-able (a734bbc953), phi optimisation (b5c5efd5de), empty-block short-circuiting (3c26c0db39),&&/||optimisation (d8a65672f8),PPARAMOUTSSA-able (6a8a9da572), constant cache (194c79c163), inlinable bool ops (8b92397bcd). The remaining 1.7-cycle work on master is in the other threads. Left open:5b355a7907leaves “get rid of all the UnimplementedF stuff” as a TODO; the in-treessa/TODOfile kept the list of unfinished work (9787ba43ee,7730880f7c,ad5ceafa2cupdate it). - Go 1.8 — Done: ARM default-on (
42181ad852), ARM Defer/RetJmp (d108bc0e73), 64-bit integer decomposition on ARM (8756d9253f), ARM floating point (59e11d7827), ARM symbolic constants (e1a2ea88d0),SSAReg(d0fa6c2f9e), default types for extension ops (68dc102ed1), stability of generated code (95427d2549), ppc64 BE (eed061f89c), mips (7f033933ce), tuple-select CSE (3d5cf72ca9),GOSSAFUNCrepaired (3eb16c059d),GOSSAHASHto set aDebugTestflag onssa.Configfor bisecting miscompiles (897c0ebf61), CFG-dependent caches (75ce89c20d). Left open:882dd1c3ba“enable rewritings that are blocked by old backends”, i.e. rules still gated by the legacy backends on the remaining arches. - Go 1.9 — Done: the old-backend leftovers are shrinking:
46085c4b36culls deadobjcode;5c90e1cf8adropsFunc.StaticData; positions becomesrc.XPos(fc456c7f7b,eaca0e0529);a5e3cac895splits state amongssa.Func/Cache/Config. See the concurrency-preparation thread for the rest. Left open: nothing noted.
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.
- Go 1.7 — Done: block-rewriting rules (
a9cec30fdc), rulegen sanity checks and logging as above, first folding rules (9b25da732b,198fc9a9eb,fd458ba499), constant-comparison rules (cdc36252fe,d4a95e78fa), constant-shift rules (93a0b0f315),Negfolding (964dda9bf1), strength-reduction of 64-bit constant divides copied fromcgen_div(a3055af45e: TimeFormat from ~40% slower to ~10% slower than the old backend), rewrite of Phis (d0d04d2d6c), opt run twice after lowering builtins later (dfcb853d9d, which also makesSliceLen/SliceCapvisible to bounds-check passes and saves ~45K in tool binaries),Phi→Or8/And8(8b20fd000d,caef4496fc), Trunc/AND, mod and bounds folds (97b2295f06,060501dc0e,68325b56e7),unsafe.Pointerconversion cleanup (a3180d8b1d). Left open:882dd1c3ba1.8 in 1.8 shows arch rules still blocked by old backends. - Go 1.8 — Done: rulegen refactor (
6a1153acb4), more constant folds (f0bab31660), Trunc-of-Ext (5c3edc46a6), extension after shift (ac74225dcc), negation into comparison (00459f05e0), constant square roots (8e24a98abe),Zeroafternewobjectremoved (b2e0e9688a), unblocking rules (882dd1c3ba), better tuple CSE (3d5cf72ca9). Left open: nothing noted. - Go 1.9 — Done:
walkmulremoved in favour of generic rules (47d2a4dafa), reassociating constant folding (379567aad1), shift-pair-to-extension (72a071c1da), float strength reduction (63a72fd447),ZeroExt(Const)(12c58bbf81),Neg(Const)(417f49a363), store elimination (4fc498d89a), BCE rules for AND-bounded indexes (4fc45ae879), boolean rules (6a24b2d0c1,9aeced650f), bit-twiddling (00db0cbf86), 32-bitAddPtr(Const)(4b3a55ec02),IsNonNil(ConstNil)(6ad2d6aa92), fixes togeneric.rules(457ac38e7a). Left open: nothing noted.
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:
- initial partitions built by recursively comparing values and their arguments (
b96189d1a0, ~3–5% on compilebench) and cached CSE keys (317226e61c,8c954d5780,851ceebceb,bc07922843,c3db6c95b6“double speed”); - the dominator queries inside CSE were N^2 (
2a29576562) and then replaced by a shared dominator tree (e0611b1664) and by sorting each class by dominance so that dominating values come first, removing a linear scan per substitution (3ea7cfabbb; issue 15112 from 1m26s to 52s); - the refinement itself was O(n^2) (pick one value, split into matching/non-matching) until
30088ac9a3replaced it by sorting a class by the equivalence classes of its arguments and splitting at changes, O(n log n) and no longer needingcmpDepth; issue 15112 from 282s to 20s, issue 16361 from 8s to 3s.e6051de035first loweredcmpDepthfrom 4 to 1 (issue 17127: 8 minutes to 1 minute) before that rewrite. Value semantics: commutative ops are normalised (88c1ef5b45, fixed by6317f92f6ein 1.9), -0.0 and 0.0 are different (ce4317266c, andadba6c4fdftreats a -0.0 literal as 0.0 at SSA build), tuple selectors are copied/typed uniformly (3d5cf72ca9,08dca4c649), constant strings can be CSE’d after reusing aux symbols (e22ba7f0fb), and a constant cache (62% hit rate over the stdlib) avoids creating duplicates (194c79c163, extended by39214275d6to nil/iface/slice/“” and by3dcfce8d19toOffPtr [c] SP, which alone had been ~30% of all CSE’d values).
Timeline.
- Go 1.7 — Done: type-refined CSE (
d9a704cd40), the phi bug (00437ebe73), key/alloc improvements (317226e61c,851ceebceb,8c954d5780,6a96a2fe5a), N^2 dominator fix (2a29576562), zero-arg CSE pass (94f0245114), commutative ops (88c1ef5b45), faster initial partitions (b96189d1a0), shared dominator tree (e0611b1664), dominance sort (3ea7cfabbb,77d374940e). Left open:3ea7cfabbbfixes a pathological case but CSE was still super-linear (e6051de0351.8 in 1.8). - Go 1.8 — Done: O(n log n) refinement (
30088ac9a3), reduced comparison depth (e6051de035), tuple selector CSE (3d5cf72ca9), allowing new SSA values to appear during CSE (a34fddf46c). Left open: nothing noted. - Go 1.9 — Done: constant-string CSE (
e22ba7f0fb), commutative-op fix (6317f92f6e), fewer allocations (f791b288d1: Aux map populated once across the two CSE runs and skipped for non-CSE-able ops), tuple-select ignoring types (08dca4c649),OffPtr SPin the constant cache (3dcfce8d19). Left open: nothing noted.
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.
- Go 1.7 — Done: the pass (
2a846d2bd3), benchmarks (1746e711ad), DFS rewrite (4dcf8ea1a4), non-nil sources (b14b8d6bc7,e0e556620a,3c1a4c1902), faulting-load checks (31115a5c98,4a346e7489), optimiseOCHECKNILuse (e22ae879d3), opt-time simplification (fb2f99d5fd), no simplification in loops (c1892b9c4b), nilcheck for interface go/defer calls (f3689d1382). Left open: intra-loop use of facts was disabled. - Go 1.8 — Done: redo (
3134ab3c2d),newobjectremovalb2e0e9688a/4d07d3e29cre-enabled, dominator tree (f4e37c8ec5),256a605faa, PAUTOHEAP (4c4ca83120). Left open:256a605faaexplicitly loses intra-block elimination. - Go 1.9 — Done: same-block elimination back (
98061fa5f3),IsNonNil(ConstNil)(6ad2d6aa92), more Phi non-nil (c694f6f3a8). Left open: nothing noted.
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)
- background Bounds Checking Elimination
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.
- Go 1.7 — Done:
bdea1d58cf,34f048c9d9,b91cc53033,cd798dcb88,47c9e139ae,27ebc84716,6c6089b3fd,1747788c56,9549c06ce6(IsInBounds with small index),bbd3ffbd83,68325b56e7. Left open: derived induction variables; pointer-increment form of range loops. - Go 1.8 — Done:
0f29942489(sliceopt test and rule),93bca632d9(preserve type ininrange),6eede325ab(duplicate-case detection for integer ranges, front-end). Left open: nothing noted. - Go 1.9 — Done:
73f92f9b04,4fc45ae879(AND-bounded indexes),7439ba32ff,0b6fbaae6e(loop rotate/guard made conditional because it “degraded bounds check removal” in preemptible-loop mode). Left open: the preemptible-loops experiment still trades off BCE.
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)
- background Go Escape Analysis Flaws listed in Go: A Documentary
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.
- Go 1.0 — Done:
42687d6ce4(simplified recursion),0227c45ede,f2460a8c57,9bf3478658,4e78818259,94ff311d1b(scalar struct fields no false positives). Left open: the whole package was one batch and escaping params all tied to the sink (as 1.1 addressed). - Go 1.1 — Done:
9fe424737b(call-graph SCC order),75692424d2/507fcf37d2(in→out tags),5583060c4c(addresses escaping through closures called in place),2667dcd113, bug fixesba97d52b85,54af752865,71282131a1,572d984eaa,20c7e41555,1dcf658f6d. Left open: tags record only whether content flows to results. - Go 1.2 — Done:
2c4b029b75(make([]T, const)uses the escape result),148fac79a3ordering fix. Left open: nothing noted. - Go 1.3 — Done:
eb592d8289,e5d742fcad,e0a55a6c98(for-init misanalysis),c99dce2b05,f078711b41. Left open: the indirect-return imprecision found in 1.4. - Go 1.4 — Done:
fe3c913443(the indirect-of-param fix),775ab8eeaa,f20e4d5ecb,0f2cde8bdc,d2204e6c0e,337fe4134f. Left open: per-result mapping and indirection counts (message). - Go 1.5 — Done:
22c16b4b92,1b87f01239,a21cf5b6a2,b19ec6842d,66130907d1,878a86a129,5d18282695,e5060c7f75,bc44b818a6(toughened against bugs),6f6bcadc17. Left open: nothing noted. - Go 1.6 — Done:
f7a39a54e9,745cdc3ad7,e779bfa5d2. Left open: nothing noted. - Go 1.7 — Done:
2d56dee61b,d8c815d8b5,d603c27c6b,fb2af2b35b(don’t walk field-name syntax),7e88826a69(look at function decls instead of OTFUNC). Left open: data structure for reasons “might be extendable to allow a cleanup of the underlying algorithms”. - Go 1.8 — Done:
f1eed92fd0,bea5252a13,f8a3444388,923a74ce77,83c73a85db. Left open: nothing noted. - Go 1.9 — Done:
1e29cd8c2b,a69754e30c,24e94766c0,fcee3777fd,6f83b75be2(lexical scope of escaped variables). Left open: nothing noted.
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.
- Go 1.0 — Done:
7e6890a670,86deacc0bc,93e547a0c2,81728cf06d,e0b2ce3401,cae604f734. Left open: nothing noted. - Go 1.1 — Done:
73b83a228e,dfdfba14b9,a72f9f46a2,2eafbb8878,09a17ca1f1,386ad0ab90,2667dcd113,7b8e08617e,ee5f59ab4f,76500b14a1,bcea0dd1d0. Left open: method values uninlined. - Go 1.4 — Done:
eb54079264(liveness for address-taken variables in inlined functions). Left open: nothing noted. - Go 1.7 — Done:
e41f527f4d,060038bdd0,e6ea01685f,ed4a27a8dd. Left open:break/type switch. - Go 1.8 — Done:
e4cae432d6,a3765723c6,5d1b53a944,9fbfe7cca0,0bc94a8864(inlined==compares elements, not addresses),dc5f9311be(Name.Inlvar removed). Left open: nothing noted. - Go 1.9 — Done:
8a9dc05312,566e72d0ce,e52d317d24,2397cd0fbf,a246f61637,1c6ef9aeed,b928e2faa8,619e37bdcc. Left open: ishairy visitor: “will make it easy to add more state” — later budget/mid-stack work.
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.
- Go 1.0 — Done: 2008–2009: fix-ups to the first walker for maps (
dee07c884e,90846cd447), channels, select (949ab5c7ff,bd397d2f9b),convert(52861558f4), eval order in args (0ffacfd995),&T{}as allocation (beee6915f8),new new(dabdfa6cce), range over channels (54aa835b44), slice conversion (65d397f747); 2010–2011:s == ""aslen(s) == 0(c9293f0cc7), zero unnamed returns when the function has defer (d9254d00f9), newlenspec optimisation (81c3e8cabc), complex/=(4e5a59591b),introduce temp = nod+tempname(9854fd2a0e),go print(b4bb970e18); 2012: order of&&/||codegen (5efd5624cc), ninit recursion in order.c (0a55958b52), append inline preserving args (47b6197a01), delete of old map delete (9069721b0e). Left open: nothing noted. - Go 1.1 — Done: parallel assignment in range (
51072eb1fb), second value in range skipped when blank (7936ab58f7),x*8→x<<3(31072e41f4), rotate recognition for^(947a3ddf87) anda.b[0]<<1|a.b[0]>>31(4de6687554: asm shrinks to a singleROLL), struct padding not hashed or compared (2ad57b4583), map-index eval order in multiple assignment (5ea52a4d91), interface-comparison double evaluation (804a43ca76), eval order in select (1b3244e0db). Left open: nothing noted. - Go 1.2 — Done: inlined
appendloses a useless bounds check (4b0eb19a05), redundant slice bound check removed (19e2922688),panicsliceunlikely (7c8be15b8a), constants not leaked to backends (d7c99cdf9f). Left open: nothing noted. - Go 1.3 — Done:
e150ca9c9a,96d90d0981,28f1868fed,8b6ef69e23(no past-the-end pointer in range loops),6f6a9445c9(no vararg channel calls),15d294991f,1357f548b0. Left open: 23 ambiguously live temporaries. - Go 1.4 — Done:
9aefdc8028,5225854b74,e024ed5ca4,454d1b0e8b. Left open: nothing noted. - Go 1.5 — Done:
18d98bc9cb,205ae07cd3/69cd91a598(no[]bytecopy in string concatenation or comparison),77a2113925,25e793d7ea,4ce06f4b5c. See other threads. Left open: nothing noted.
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)
- tracking issue #17583 cmd/compile: insert write barriers in SSA referenced by 4 of this thread's commits
- motivating issue #17503 runtime: eliminate stack rescanning referenced by 1 of this thread's commits
- design doc Proposal: Eliminate STW stack re-scanning Austin Clements, Rick Hudson, 2016-10-21 · linked from #17503
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.
- Go 1.5 — Done:
d8ee1c5a2c,e5ef657264,ff979626fc,a7bb393628,2a73559023,8552047a32,9c9e36b340,972a478ddf,9aefdc80281.4 (fat writebarrier w/o temp). Left open: inline check for typedmemmove barriers (“sets us up to generate an inline check”). - Go 1.6 — Done:
85dd62d5dd. Left open: nothing noted. - Go 1.7 — Done:
aebf6611df,3e54ca9a46(omit barrier on global function),15ed37d7b7(enforce nowritebarrier in SSA),42da35c699,6b33b0e98e,a4650a2111,03e216f30d,934c359964,8107b0012f. Left open: barrier decisions still in the front end. - Go 1.8 — Done:
f6aec889e1,c39918a049,d0e92f61e5/68331750da(removed unnecessary barriers for APPEND/stack writes),75d367e34b,ed0b232cdc,92ecd78933,fdc4a964d2. Left open: optimisations disabled by the hybrid barrier. - Go 1.9 — Done:
c4ef597c47,9ebf3d5100,211c8c9f1a,5bfd1ef036,794f1ebff7,0ee9c46cb1,e5c9358fe2,94a017f3f5,85b2940f5c. Left open: nothing noted.
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.
- Go 1.0 — Done:
820f42d977,79fa5b65cb,2450c590e9,6b72b07016,6f14cada11. Left open: nothing noted. - Go 1.5 — Done:
4bc9badd75,4ce06f4b5c. Left open: nothing noted. - Go 1.7 — Done:
e41f527f4dmakes switches inlinable. Left open: nothing noted. - Go 1.8 — Done:
726190daf3,bd2838be77,b046786528,b7ac426ee3,e26499153e,a9266eef93,643b9ec07c,6eede325ab. Left open: nothing noted.
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)
- motivating issue #19361 cmd/compile: Devirtualize calls when concrete type behind interface is statically known referenced by 2 of this thread's commits
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.
- Go 1.5 — Done:
77a2113925,25e793d7ea,fd85a6c640(fast-pathed conversion condition fix). Left open: nothing noted. - Go 1.7 — Done:
7427f2c4bd,5c5e8d4105. Left open: nothing noted. - Go 1.8 — Done:
562d06fc23,1a7fc7b3a7,688995d1e9,672e579444. Left open: nothing noted. - Go 1.9 — Done:
4e0c7c3f61,295307ae78,c682d3239e,0358367576,d9270ecb3a. Left open: only direct itab-known calls are handled.
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.
- Go 1.7 — Done:
b5c5efd5de,6b99fb5bea,bc1989f115,8b20fd000d. Left open: old algorithm remains for small functions. - Go 1.8 — Done:
5a6e511c61,433be563b6,1cbc5aa5291.9. Left open: “we might end up introducing a bunch of dead phis”; TODO to place phis only where reads may use them. - Go 1.9 — Done:
61fb2f6d63,4e428907c5,e00e57d67c,5272a2cdc5,9664bc1d1f,a027466e4b. Left open: nothing noted.
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.
- Go 1.7 — Done:
078ba138d3,d32229b3b1,0b6332eb54,4fa050024f,6ed79fbd1a,9743e4b031,d610d304f8,f91ff1a509,c67cac0703,3c26c0db39,6b3462c784,c03ed491fe,6b85a45edc,815c9a7f28,305a0ac123,e197f467d5,bbf8c5ce2f,186cf1b9ba. Left open: nothing noted. - Go 1.8 — Done:
dd24b1098a,be302e6d43,7f1ff65c39,75ce89c20d,e85265e8c2,62ba15a492(more non-returning runtime calls). Left open: bool2int forif b {return 1}; return 0(“left for future work”, message ofe85265e8c2). - Go 1.9 — Done:
39ce5907ca,638ebb04f2,abdb2c35b6,073297ff68,b3e577b9e7,16e430e1ef,0b6fbaae6e,9664bc1d1f. Left open: rotation conditional on GOEXPERIMENT (BCE regressions).
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.
- Go 1.7 — Done: the items above plus pass stats (
a3f72956f1),24dcede1c0. Left open: nothing noted. - Go 1.8 — Done:
e6051de035,30088ac9a3,fe27291c00. Left open: nothing noted. - Go 1.9 — Done:
a5e3cac895,f791b288d1,4ee934ad27(export data -10%),ccaa8e3c6f. Left open: nothing noted.
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.
- tracking issue #15756 cmd/compile: parallelize compilation referenced by 11 of this thread's commits
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.
- Go 1.9 — Done: all of the above. Left open: nothing noted.
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)
- proposal #18616 math/bits: an integer bit twiddling library referenced by 3 of this thread's commits
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.
- Go 1.5 — Done:
43e6923131,205ae07cd3,69cd91a598,764c751ae5. Left open: nothing noted. - Go 1.6 — Done:
eddd7ff3cd,c73df92be6,7eb94db44c,423b0cc25a. Left open: nothing noted. - Go 1.7 — Done:
995fb0319e(stringtoslicebytetmp fix),b024ed0d94. Left open: nothing noted. - Go 1.8 — Done:
c9fd997524,150de948ee,58e2edafb7,8a7f0ad0b5,8e24a98abe,8aadcc551e,2756d56c89,964639cc33,644c16c76c,9a243303b8(omit temporary panicdivide calls). Left open: nothing noted. - Go 1.9 — Done:
a833485828,c8eaeb8cba,a51e4cc9ce,ee69c21747,3bdc2f3abf,e4a500ce14,dbd51ce99c,d5dc490519,dd9892e31b,42e97468a1,ec091b6af2,5d6b7fcaa1. Left open: nothing noted.
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
recovercallers, 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,
loopbcedeleted), range loops stopped producing past-the-end pointers, location lists were redesigned,is_stmtstatement 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 viastaticValue, inlining offorloops 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,walkanddevirtualizepackages overir, 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)
- background Bounds Checking Elimination
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.
- Go 1.10 — Done: use right shifts to prove bounds so more checks vanish (
561b147e4b); loop finder made aware of irreducible loops (9e21e9c5cb, sets a function-level flag so the loop rotater is not confused). Left open: walk still emitted the bounds-check boundedness hints (commit says it will rely on these rules “when we eliminate boundedness checks from walk.go”). - Go 1.11 — Done: unsatisfiability design (
669db2cef5), simpler limit logic and unsigned limits derived from signed (2e9cf5f66e,941fc129e2), fence-post implications (6436270dad), fail fast on unsat and short-circuit self-facts (385d936fb2,d54902ece9), relations between constants (8d8f620f68), len/cap relations infactsTable.update(4816eface1), OFORUNTIL inductive facts (b812eec928, needed by the new range lowering), loop BCE inside prove and removal of loopbce (7ec25d0acf,f49369b67c), fullOpIsInBounds/OpIsSliceInBoundssupport (ac43de3ae5), unsigned relations while branching (29162ec9a7), poset transitive closure (5c40210987), negative-increment loops (6d379add0f),len(s)-delta(e0d37a33ab), shift simplification (9eb4590ad2). Left open: poset only used for relations, not limits; fence-post for the unsigned domain was wrong (fixed in 1.12). - Go 1.12 — Done: fix fence-post implications for the unsigned domain (
09ea3c08e8); negative upper bound check forIsSliceInBounds(ea6259d5e9); overflow fix when deriving fromx+d >= w(2e217fa726); avoid implicit bounds checks after explicit append checks (9f66b41bee). Left open: nothing noted. - Go 1.13 — Done: poset used for non-negatives (
ddef157813), repeated zero-constant checks short-cut (576442b27b), index >= 0 learned from bounds checks (2d41444ad0), induction detection for unrolled loops (d8f60eea64),int64(uint64>>x) >= 0(66f5d4e035), slice bounds checks reversed so the RHS ofIsSliceInBoundsis non-negative (83a33d3855), range analysis of small signed ints fixed (f44404ebbf). Left open: extension handling (done in 1.14). - Go 1.14 — Done: sign/zero extensions (
69ff0ba798), poset naming/consts/aliases/path collapsing (b16463c912,490646e476,c70e5475e6),OpSliceMakefacts (87e2b34f7b), indvars bound by other indvars (1658263bbf), poset sufficient-condition fix (3c56eb4083), prove attempts to preserve statement marks when it removes code (9a926911fe). Left open: nothing noted. - Go 1.15 — Done: zero right shift of positive ints by bits-1 (
2cb10d42b7);isNonNegativeuses bit patterns only (5fac45a320); local induction facts only when an edge exists between if block and phi block (7f86080476, completed by the indirect-connection checka6755fc0de1.16 in 1.16). Left open: nothing noted. - Go 1.16 — Done: indirect if-to-phi connection check (
a6755fc0de); unsigned comparisons to 0 optimized (e30fbe3757). Left open: nothing noted. - Go 1.17 — Done: transitive slice len/cap relations in poset (
5e804ba17d); strengthened branch-known-from-predecessor handling lives in the CFG thread (92d1afe989). Left open: poset remains the only relation store; the later per-value limit table is the 1.18+ redesign.
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)
- tracking issue #23109 cmd/compile: rewrite escape analysis referenced by 3 of this thread's commits
- tracking issue #33981 cmd/compile: improve escape analysis fidelity referenced by 4 of this thread's commits
- background Go Escape Analysis Flaws listed in Go: A Documentary
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.
- Go 1.10 — Done: loop depth of range expression fix (
dbb1d198ab); Left open: esc.go design (AST walk, small leak encoding). - Go 1.11 — Done:
go:uintptrescapesfor unnamed parameters (beeab6ac02), loop depth to field (5a91c83ce8), element type forOINDof slices (3042463d61), explanation of escape reasons (a12c1f26e4,360c19157a), anonymous receivers non-escaping (88466e93a4). Left open: nothing noted. - Go 1.12 — Done: self-assignment handling unified in
esc.go(ff468a43be,859cf7fc0f,499fbb1a8a),mayAffectMemoryfixes (4cf33e361a,2d82465d18), sink-node positions (67ac554d79), unnamed parameters (be09bdf589). Left open: esc.go’s encoding limits, fixed by the rewrite. - Go 1.13 — Done: user-visible new escape analysis implementation added (
97c4ad4327) and enabled by default (996a687ebb); CONVIFACE data flows to heap (0349f29a55); parameter tagging refactor (23bf6af9961.14);-newescaperespected in genwrapper (a152dd0438). Left open: -m=2 diagnostics and performance work (done in 1.14). - Go 1.14 — Done:
-newescapeflag removed (991b0fd46c); work-queue walking and graph optimizations (867ea9c17f,262046716c);EscLeaksand new leak encoding (05a805a6de,a0894ea5b5); parameter tagging integration (b9704872d1); loop depth preserved when evaluating blocks (1406ece446);-m=2restored (063d0f11e5,4cde749f63,07513d208a); JSON escape logging for the optimizer log (298be61f3c); checkptr escapes (b6b984fcff,46aa8354fa);n.Noescape()split into separate uses (2197321db1). Left open: nothing noted. - Go 1.15 — Done: method values analyzed precisely (
2a2423bd05),Escape.tagHolerefactor (1811533695), walkAll pre-allocation (e43239aabe), stricter safety rule #4 (f049d911e9). Left open: nothing noted. - Go 1.16 — Done: known-call analysis via
staticValue(c0417df156),mustHeapAllocreturns a reason (095e0f48a1) and handles OCLOSURE/OCALLPART (82c45eb681), escape reason for MAKESLICE with no cap fixed (e572218d12), method-value and variadic rewrites moved before escape (681ba430771.15,9f4dd09bf51.15). Left open: nothing noted. - Go 1.17 — Done: package
escapesplit out (de454eef5f), escape state split (67ad695416), reorganized to walk closures in place (b1747756e3), capture analysis reimplemented (fd43831f44,ab523fc510), decoupled fromName.Vargen(f57f484053), stopped analysingNameOffsetExpr.Name_(88956fc4b1), heap-allocated results fix (a429926159), closure escape insidetooHairyscan (51e1819a8d,48badc5fa8),gowrapper closures always escape (1b0a031680). Left open: closures with captures are not yet inlinable by default (see inlining thread).
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)
- proposal #19348 cmd/compile: enable mid-stack inlining referenced by 2 of this thread's commits
- design doc Proposal: Mid-stack inlining in the Go compiler David Lazar, Austin Clements, 2017-03-10 · linked from #19348
- talk talk: Mid-stack inlining in the Go compiler (external) linked from #19348
- motivating issue #15561 cmd/compile: support inlining non-escaping closures referenced by 3 of this thread's commits
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.
- Go 1.10 — Done: closures with captures inlined (
c4b65fa4cc); better inliner diagnostics (475df0ebcc); simplifiedmkinlcall1(b80029ccf4); non-code nodes ignored (812b34efae);go:yeswritebarrierrecblocks inlining (4e64ee423c); broken accounting code removed (13bf4ada80); functions calling recover not inlined (b5b35be2b8); inline fixes for reassignments (483e298daa). Left open:-l=4mid-stack inlining unfinished (recoverwas the blocker). - Go 1.11 — Done: variadic functions (
09d4455f45), parameter refactor +panicinlinable (87a18c6109), larger-function threshold (5fc70b6fac), constantif(d7cd61ceaa),go:norace(c6e455bb11), struct == cost (b638760dad). Left open:-l=4“not the default” because it makes the compiler itself bigger and slower, and is untested with the debugger/DWARF. - Go 1.12 — Done: single-call bodies encouraged (
13baf4b2cd), call cost 57 (624e197c71), budget hacks removed (555d8c455a), autogenerated wrappers inlined (fa913a36a2), mid-stack tracebacks (69c2c56453). Left open: the mid-stack-inlining flag and its default are outside this unit. - Go 1.13 — Done: unnecessary inline marks removed (
c46ebec322);visitBottomUpchanged to pre-order (ef2806e4ab,e40dffe55a); intrinsic inlining shortcut after the getcaller check (376ce8c880). Left open: nothing noted. - Go 1.14 — Done: inline-related optimizer logging (
4d0ed149ff). Left open: nothing noted. - Go 1.15 — Done: cycle-of-recursion mid-stack inlining (
ed7a8332c4), intrinsic cost one (956f64888d), cost printed with-m=2(ee46f135a9), inline failure logged (fced302aa1). Left open: nothing noted. - Go 1.16 — Done: for loops (
497ea0610e), staticValue (1bcf6beec5), improved single-return inlining (f2c0c2b902), go/defer argument inlining (4fb4291388),Defnfor inlinee params (cced777026), miscompile fixes (35693d037f,7191f1136b,89b44b4e2b). Left open: labeledforloops, per commit message (“future CL”). - Go 1.17 — Done: closure inlining simplified (
fad9a8b528), inline results declared early for empty returns (6c3bcda866), OCONVOP cost reduced (adb467ffd2),tooHairyscans closure bodies (51e1819a8d), do not substitute OGOTO in closures (95dde3f029), no inltree for closure inside inlined body (6d2ef2ef2a), imported closure types fix (eb433ed5a2), workarounds for closures with type switches (9f601690da), functions with closures not inlined by default (8027343b63). Left open: bug #44370 behind the disabled default.
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.
- Go 1.16 — Done: early devirtualization of interface method calls (
5cc43c51c9), graceful failure and promoted-method fixes (aa4f48b751,e62adb1c0b), reassignment checks (7191f1136b,063a91c0ab,89b44b4e2b). Left open: only locals initialised once are handled (staticValue’s reassign analysis). - Go 1.17 — Done: new
devirtualizationpackage (4b1d0fe66f). Left open: nothing noted.
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 doc Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case Dan Scales, Keith Randall, and Austin Clements, 2019-09-23 · linked from #34481
- proposal #34481 cmd/compile, runtime: a design for low-cost defers via inline code referenced by 1 of this thread's commits
- motivating issue #14939 runtime: defer is slow referenced by 2 of this thread's commits
- motivating issue #6980 cmd/compile: allocate some defers in stack frames referenced by 1 of this thread's commits
- talk Implementing Faster Defers. Nov 11, 2020 listed in Go: A Documentary
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.
- Go 1.13 — Done: stack-allocated defer records (
fff4f599fe). Left open: defers still cost a runtime call per execution. - Go 1.14 — Done: open-coded defer fixes (
cc47b0d2cd,1b3a1db19f), riscv64 workaround (a858d15f11,2e4f490b31), nil deferred function panics at invocation on Wasm/AIX (c4817f5d4f). Left open: riscv64 was broken for a time. - Go 1.16 — Done: late call expansion handles simple defer and
callDeferStack(4ad5dd63a7,68bc950583,bb46b60d29),go/deferargument fixes (bdb480fd62,76a615b20a,deaf29a8a8). Left open: nothing noted. - Go 1.17 — Done: argument-less wrapping for go/defer (
769d4b68ef) and its follow-up fixes above, debug flag (e617b2b0dd), register results on return path (f5efa5a313). Left open: thegowrapper must not escape in the runtime (1b0a031680).
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)
- tracking issue #40724 cmd/compile: switch to a register-based calling convention for Go functions referenced by 14 of this thread's commits
- design doc Proposal: Register-based Go calling convention Austin Clements, 2020-08-10 · linked from #40724
- proposal #18597 proposal: cmd/compile: define register-based calling convention linked from #40724
- discussion Bug in new amd64 ABI golang-dev · linked from #40724
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.
- Go 1.12 — Done: argument construction moved to SSA (
2578ac54eb). Left open: calls still fully expanded at SSA build. - Go 1.16 — Done: the late call expansion series above, begun in
35e413c537and44c0586931, with4d7abd7ae6,ad8447bed9,ccf89bef43(store call args in the call block),c3fe874f25(avoid generating CSEs; maintain debug names). Left open: “does not work yet for SSA-able aggregates” initially (4d7abd7ae6), later fixed byad8447bed9. - Go 1.17 — Done: register-ABI plumbing listed in the Design paragraph, plus
GOEXPERIMENT=regabiargsincoming/outgoing fixes (3304b2235a,1f29e69bad), expandCalls name preservation and deduplication (33d99905da,0921211403,daee726a0b,e5bc4f2a77),OpArgdedup (5d6581d747,64ca7e2cb3). Left open: regabi not default for all arches (experiment-gated); fast map functions for aggregates (TODO in6c98ecda10).
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.
- Go 1.10 — Done: line numbers in ssa.html (
edcf2d0cd6), explicit NoXPos for entry-block constants (f22cf7131a). Left open: no statement/non-statement distinction. - Go 1.11 — Done:
IsStmtin lico (619679a397); stmt boundary pass (c2c1822b12); position adjustments (b9a365681f,eda22a06fb). Left open: Delve could not yet use the marks (“worsens optimized-debugging experience”). - Go 1.13 — Done: move marks from jumps to targets (
82af9e6749), OFOR body pos (7afd58d458), line tables indexed by source file (6081a9f7e6). Left open: better heuristic skipping not-statement values. - Go 1.14 — Done: preservation in nilcheck, branchelim, fuse and prove (see Design), bogus line for empty infinite loops (
9bba63bbbe,0e02cfb369), empty block marked preemptible (a037582eff). Left open: nothing noted. - Go 1.17 — Done: register-ABI ops (
4df3d0e4df), prologue (00d42ffc89),-Nentry block (5e4f9b077f). Left open: nothing noted.
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.
- Go 1.10 — Done: keep float names (
788aa88cd0), more stack-slot names (a03480905b), name propagation (38c725b148), lifetime adjustment (d58d90152b), ssa dump files alphabetized (75f0ad705f). Left open: RegKill-based pipeline. - Go 1.11 — Done: reimplementation (
2075a9323d), memory use (39eea62340), dead locals in inline DWARF (1ba26a33ce). Left open: macOS base-address entries (commit says “TBD”). - Go 1.12 — Done: params get slots (
fa31093ec4). Left open: nothing noted. - Go 1.17 — Done: debug analysis block/function end sentinels (
afa58ddf5a), regabi name mapping for small in-params (f62739b861,0921211403). Left open: only single-register params mapped.
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.
- Go 1.10 — Done: fixes for invalid switch case panics (
24ca86f308,39e523792e), value switches (bb45bc27b5). Left open: nothing noted. - Go 1.11 — Done: open-coded select send/recv/default (
004260afde); untyped bool conversion in walkCases (a375a6b363); avoid runtime call forswitch string(byteslice)(566e3e074c). Left open: nothing noted. - Go 1.14 — Done: major refactoring (
8f3d9855a1), string switch optimization (85fc765341),binarySearchOIF construction (1b2c794896), implicit break position (6139019efa), no statement marks inside the switch tree (01ff213a51), block-indirection removed in type switches (ab00f89c27). Left open: jump tables not yet present. - Go 1.15 — Done: better concrete type-switch code (
2db4cc38a0), consistent positions (f5439d4c68,02ab2c040e). Left open: nothing noted.
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.
- Go 1.10 — Done: range simplification (
743117a85e), single blank var handling (137e4a6c63), large-hint map bucket (6c102e141c). Left open: nothing noted. - Go 1.11 — Done: no past-the-end pointer (
837ed98d63), map-clear idiom (aee71dd70b), memclr refactor (500d79c410), slice extension (a8a60ac2a7), mapassign-append (c12b185a6e),m[k] op= r(7395083136), preemptibleloops typechecking (48f990b4a5). Left open: preemptibleloops “doesn’t quite” work. - Go 1.12 — Done:
[]byte("...")(ceb0c371d9), struct/array key literals (a0f57c3fd0), copyRet allocs (5dbc67a99a). Left open: nothing noted. - Go 1.13 — Done: map initializers evaluated incrementally (
ee59c06acb),callnewported to SSA (23b476a3c8) and no newobject for zero-sized types (c1050a8e54). Left open: nothing noted.
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)
- motivating issue #30810 cmd/compile: rewrite rulegen to not generate plaintext Go code directly referenced by 1 of this thread's commits
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.
- Go 1.11 — Done:
|operator (b657c00243), rulegen diagnostic (31ef3846a7),useAvg/useHmulSSA config options (a9ba3e30ac),Op{SP,SB}checks (cb44c8debb). Left open: whitespace alignment cleanups (stated inb657c00243). - Go 1.12 — Done: randomized value order for testing (
2b4f24a2d2),v.Pos.linewarnRule (25b84c0155). Left open: nothing noted. - Go 1.13 — Done: multi-
|expansion (48d3c32ba9),countRule(0047353c53), deadcode pass storage inssa.Cache(c9ccdf1f8c). Left open: nothing noted. - Go 1.14 — Done: rulegen rewrite (
1a53915c6b), multiple control values (9c2e7e8bed), go/types removal (1581bb9843), parallelization. Left open: nothing noted. - Go 1.15 — Done: ellipsis rules (
8484d409ac,a3fc77aa7e), typed aux (28157b3292), conversion of generic/AMD64/s390x/arm64 rules,-d=ssa/check/seed(97a268624c), rule source-line flag (24fa159a2e). Left open: remaining architectures to convert. - Go 1.16 — Done: untyped rules removed (
79a3482d9e), reassociation rules (d556c251a1),ecb79e8afaregenerated missing condition. Left open: nothing noted. - Go 1.17 — Done: no-op rewrite funcs smaller (
bd8b3fe5be). Left open: nothing noted.
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.
- Go 1.10 — Done: needwritebarrier removed (
e0111bb0f4), counter instead of loop (99c757adb5), notinheap elision (b78b54ff83,3a446d8652), typedslicecopy reporting (afbe646ab4). Left open: nothing noted. - Go 1.12 — Done: non-heap pointer elision (
2140975ebd), memclrHasPointers (c8ca793176), slice-clear notinheap (6454a09a97). Left open: only freshly-allocated objects. - Go 1.13 — Done: tracking zeroed object parts (
ca36af215f), volatile values copied before barrier call (f23c601bf9), crash on barrier insertion fixed (316ac4f330). Left open: nothing noted. - Go 1.14 — Done: improved removal (
30da79d958). Left open: nothing noted. - Go 1.15 — Done: no stack maps for barrier calls (
601bc41da2), readonly sym checks simplified (71e0cd815d). Left open: nothing noted. - Go 1.17 — Done: newobject in register ABI (
2fa7163b06), ABI-specific barriers (89b141c06e), pointer store types preserved in expandCalls (ca3aefc4a9,5579ee169f). Left open: nothing noted.
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.
- Go 1.10 — Done: unread-auto elimination (
744ebfde04), memmove with known size (f3884680fc), nil check after newobject (f1517ec6e5),nilcheckelim2copies (bad5abf64d), nil check before interface call (fa1f52c5f6). Left open: aliasing restrictions on memmove. - Go 1.11 — Done: arm64 memmove (
0e8b7110f6), disjoint memmove -> Move (6d00e8c478), composite optimizations (f31a18ded4), address-reaches-phi autos (adfa8b8691,1546ab5a39), Zero-to-Load fix (fbfbf03b28). Left open: “return value unnecessarily cleared” needed dead store analysis (f31a18ded4). - Go 1.12 — Done: amd64 disjoint memmove (
c292b32f33), Move chain with VarDef (8607b2e825,a98bb7e244), nil-check use in dead-auto (54f9c0416a), AIX nilcheck (9fe9853ae5). Left open: nothing noted. - Go 1.13 — Done: nil-check not moved across VarDef (
64c134f90f), slice pointers non-nil (933e34ac99). Left open: nothing noted. - Go 1.15 — Done: ro-sym Move to Zero (
4a7e363288), double zeroing (396833caef), Move/Zero typed aux (32467e677f,4799955004), negative size guard (bc98e35b53). Left open: nothing noted. - Go 1.16 — Done:
85bb4294c0alias detection (walk), memequal(x,const,1) (f2ee58b6bb), keep call args. Left open: nothing noted.
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.
- Go 1.10 — Done:
a0453a180f,fb165eaffd,99fe3f8c63,0011cfbe2b, float constant propagation (9da29b687f,95b146e8eb), ppc64 Ctz (53bbddd527), soft-float routines for soft-float targets (6be1c09e19). Left open: lower-bound info on SSA values (“may trigger on more cases in the future”). - Go 1.11 — Done: Sqrt 386,
4a1baf8bd1,098208a0d9, Ctz/Len ops,a9ba3e30acconfig options, math/bits usage (5654114d04,7ac756f74b). Left open: Ctz/Len only on amd64. - Go 1.12 — Done:
bits.Mul,bits.Div, RotateLeft,c6118af558,379d2dea72. Left open: Div intrinsic amd64-only. - Go 1.13 — Done: signed divisibility (
68819fb6d2,4d9dd35806,44343c777c),61945fc502(no panicshift for masked shifts),68d4b1265e. Left open: nothing noted. - Go 1.14 — Done: AND over shifts (
9ec7074a94), Neg/Com rules (9675f81928),b8cbcacabepointer comparisons,bb7890b85aNot absorbed into Neq/Eq. Left open: nothing noted. - Go 1.15 — Done: integer-in-range checks restored in SSA via fuse + rules (
e37cc29863) and walkinrange removed (24343cb886); NaN restrictions; reassociation typed rules;14ad23d1f5zero extensions after 32-bit shifts avoided. Left open: nothing noted. - Go 1.16 — Done: reassociation
(op (op y C) x|C)(d556c251a1), 32-bit magic multiply (c1afbf69c7), unsigned comparisons to 0 (e30fbe3757). Left open: nothing noted. - Go 1.17 — Done: intrinsic fixes (
402f177efc,42c25e65f3),FuncPCABIxxx. Left open: nothing noted.
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.
- Go 1.11 — Done: arm64 CondSelect (
2d0172c3a7), amd64 cost model (25813f9f91), tighten after lowering (3a9e4440fd), branch likeliness reset (4313d7767d,81786649c5), common fuse code (cfbf375a81). Left open: cmov cost on amd64 handled heuristically. - Go 1.12 — Done: fuse before branchelim (
a55f3ee46d), CMOV load addresses (0b79dde112). Left open: nothing noted. - Go 1.13 — Done: shortcircuit (
52ae04fdfc1.14, 1.14), fuseIf (ddb5d0db37,e7b7f87432), phi tighten (c1544ff906). Left open: nothing noted. - Go 1.14 — Done: shortcircuit infinite loops (
2393d16147),7673884a7f(do not fuse branches with side effects). Left open: nothing noted. - Go 1.15 — Done: integer ranges (
e37cc29863), shortcircuit phis (ade0811dc8,64dcef3045,0239a5c478,4ad643d208). Left open: nothing noted. - Go 1.16 — Done: shortcircuit and phi flags removed (
ea37a65af6). Left open: nothing noted. - Go 1.17 — Done:
051bf37833,92d1afe989,ba6bd967d2, shortcircuit guards (771c57e68e,04903476fe). Left open: nothing noted.
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.
- Go 1.10 — Done: walkprint simplified (
8b68fafd1f,a14a8a3eb9), OASOP order (34db5f0c4d), hiter type in order (9c3f268558), println fixes (02deb77f6d,4cff104771), panic frame info (7c8a9615c0). Left open: nothing noted. - Go 1.11 — Done: order.go refactored into methods (
26708439ec),defer deletefix (2c0c68d621), finishcompare series (30673769ed,5b071bfa88). Left open: nothing noted. - Go 1.12 — Done: arg stack in SSA (
2578ac54eb), order temporaries reuse (389e942745,63e964e174),fe2c588b1c. Left open: nothing noted. - Go 1.13 — Done: short-circuit ordering (
4a9064ef41),callnewto SSA (23b476a3c8), maplit incrementally (ee59c06acb), sinit methods (ee2836048c). Left open: nothing noted. - Go 1.14 — Done: OAS2 in order (
ba6aeb6ce8,e79c23822c), walkcompare cleanups (ecc2d61798,e76b9e8908). Left open: nothing noted. - Go 1.15 — Done: variadic call desugaring in order (
681ba43077,9f4dd09bf5), eqstring/eqinterface factoring (daae72e88e,f4e13b83aa),19ed0d993cstaticuint64s. Left open: nothing noted. - Go 1.16 — Done: OANDNOT lowered in SSA (
a19cf510af), selection fixes,aliasedimproved (85bb4294c0). Left open: nothing noted. - Go 1.17 — Done: package split and refactors above. Left open: nothing noted.
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.
- Go 1.10 — Done: line numbers (
edcf2d0cd6), dump grouping (75f0ad705f). Left open: nothing noted. - Go 1.11 — Done: README, columns, colors. Left open: nothing noted.
- Go 1.12 — Done: sources, CFGs, AST. Left open: nothing noted.
- Go 1.14 — Done: LSP/JSON logging (
cd53fddabb,298be61f3c,4d0ed149ff,46c9fd03a5), checkptr. Left open: nothing noted. - Go 1.15 — Done: dark mode, combined columns, block auxint, HTMLWriter refactor (
1e9665da8f,afc480bab4,a4451e1143,6736b2fdb2). Left open: nothing noted. - Go 1.16 — Done: GOSSAFUNC matching and directory output (
bca0b44629,f4cbf3477f,234e23d763). Left open: nothing noted. - Go 1.17 — Done: package-suffix match (
70deaa33eb). Left open: nothing noted.
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 doc Proposal: profile-guided optimization Cherry Mui, Austin Clements, Michael Pratt, 2022-09-12 · listed in Go: A Documentary
- proposal #55022 cmd/compile: profile-guided optimization referenced by 7 of this thread's commits
- design doc Proposal: Design and Implementation of Profile-Guided Optimization (PGO) for Go Raj Barik, Jin Lin, 2022-09-12 · listed in Go: A Documentary
- proposal #55025 cmd/compile: design and implementation of Profile-Guided Optimization (PGO) listed in Go: A Documentary
- tracking issue #62463 cmd/compile: PGO opportunities umbrella issue linked from #55022
- talk Michael Pratt, Boost application performance with profile guided optimization, GopherConUK 2024 listed in Go: A Documentary
- background Profile-guided optimization listed in Go: A Documentary
- background Chen, et al. AutoFDO: Automatic Feedback-Directed Optimization for Warehouse-Scale Applications. 2016. listed in Go: A Documentary
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.
- Go 1.20 — Done: user-visible Raj Barik’s initial implementation:
pgo/graph.goandirgraph.gobuild the profile graph, and the inliner uses it (99862cd57d, #55022). Within the same cycle the heuristics were reworked:- Inlinability is decided by edge weight rather than node weight (
fb4f7fdb26). - The hot threshold is a CDF over edge weights, not a fixed percentage (
b07e845e76). - Calls are matched by line offset from
start_line(bdd1e283a9, with a fix in0409314db3). - Only the last two frames form an edge (
6e0e492e12). - Defaults became 99% CDF and a 2000 budget, and the debug flags were renamed
pgo*(1b03568ae1). - Supporting work: global state, label handling and per-location maps were removed for speed (
ec0b540293,204be97d24,d73885588a,1a4cc091a9,a72e133422,7e88b866f9,f187c6b08e,d9cc39b25c,3f1bcc58b3); cross-package inlining was fixed (ada9385a5f); empty profiles are accepted (efe541d4e5); debug output and the missing-start_linehint were improved (e5d2814576,64519baf38).
- Inlinability is decided by edge weight rather than node weight (
- Go 1.21 — Done: user-visible
8c445b7c9fadds PGO devirtualization of interface calls, based on Uber’s CL 484838. Follow-ups:3637132233handles methods from other packages when the current package references the concrete type.fd03e6ebc9checks the method name.38e2376f35adjusts the diagnostics.
- Hot calls are no longer inlined into “big” functions. The PGO budget had bypassed the callee cap of 20 that applies to big functions (
c1ac63e973). - Symbol matching escapes package paths (
0fd6ae548f), and profile open and parse errors are reported (afbcf2138c). - Graph fixes:
7180a09745,5e4e2acc8e,a4b66b14e2. - The pprof graph moved to
pgo/internal/graphand dead code was deleted (d740b365b7,7484dd30fd). Node weights leftIRNode(4b6a542048), the post-inlining graph dump was removed (cce67690b8), and the “WORK IN PROGRESS” label was dropped (446ad131f6). -d=pgoinlinebecame-d=pgodebug(7c2cd0bbe2).- The compiler itself is now built with PGO (
45b74d20a0). - Functions from norace packages are not inlined in race mode, an issue PGO exposed (
87e69c1812).
8c445b7c9fmessage lists its limitations: only interface calls, only the single hottest callee, and no lookup of callees missing from export data. - Go 1.22 — Done: user-visible
- Indirect callees and functions can be looked up from export data (
bfb8924653,42bd21be1c). - Function-value devirtualization (
fb6ff1e4ca, #61577) can be disabled with a flag (1ab9df4849). hairyVisitorandmkinlcallnow share their inlinability logic, so the hot-budget decisions agree (b56645a87b).pgohashenables bisection (3fb86fb864).- The hottest edge is selected stably, breaking ties by name (
4b17b36b3f,2d428cfd85). runtime.memhash_varlenis never devirtualized (3fd5c357a3).- Devirtualization uses
CallExpr.GoDefer(ddb38c3f54). Readability refactors:14c347f5ce,da13da1fce. - Devirtualization and inlining are interleaved so devirtualized calls get inlined (
4a90cdb03d, #52193).
fb6ff1e4canotes that closures needing a context are not supported as targets. - Indirect callees and functions can be looked up from export data (
- Go 1.23 — Done:
cmd/preprofilepre-parses profiles into a compact text format. The first versionb7ae16e04bwas reverted ina807295d43and reimplemented ine069587c8d.- The compiler checks the header without reading the whole file (
9819d8265f). - The shared parsing moved into
cmd/internal/pgo, with entries sorted by weight so deserialization needs no sort (63deaf00ea). The message reports PGO parsing falling from 13.7% to 1.3% of compile CPU. pgowas renamedpgoir(0c5612092d).- Call-site offsets are now assigned correctly (
185f31bf30). - Export-data lookup gained a debug log (
d037bba19b), method lookup bails on interface types (508e761978), and devirtualization gained a post-lookup cleanup hook (ca5ba146da).
- Go 1.25 — Done: a zero-weight edge is never chosen as the hottest callee (
13b1261175). Left open: nothing noted. - Go 1.27 — Done:
- A profile whose graph is empty after scaling now produces a parse error (
a1150b5017). - The PGO profile is threaded through the inliner and the body reader, so imported generic instantiations see it. Without it,
newinlinercould clearfn.Inland crash (e296659b12).
- A profile whose graph is empty after scaling now produces a parse error (
- Go 1.28 — Done: user-visible
FromPProfaccepts CPU profiles from external tools such as Linux perf viaperf_to_profile. When the value names differ from Go’s (for examplecycles_event/count), it takes the CPU-like sample column (3bd19c1c39, #64489). Left open: nothing noted.
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 doc Go 1.22 inlining overhaul listed in Go: A Documentary
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:
encoding/binaryloads, stores and appends are cheap (17c513e722,629c0b3a6e).- Taking the address of a field at offset zero costs 0 (
638c9aad5f). - A call to a function-typed parameter costs 17 instead of 57 (
a464ffda3e). Calls to closure variables are discounted (ad7f736d8f). - A function that contains a closure gets double the budget (
170436c045), and a closure called only once gets a very high budget (d524c954b1). abi.NoEscapeis cheap (b6efbd4efc).
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.
- Go 1.18 — Done:
- dev.typeparams refactors simplified variadic calls and
mkinlcall(6da1661371,417955d151,1ec056244e). Walk flattensOINLCALL(c45d0eaadb). - user-visible Functions containing
ORANGE(a3bb28e5ff) and labeledfor/switch(74acbaf94a) became inlinable, with fixes inb8da7e4c4candc6e82e5808. encoding/binarycalls are treated as cheap (17c513e722).- Closure positions (
08e2519ded,465b402808), a TODO in inl.go (d92101f452). - Shape-typed generic bodies are not inlined when shape and non-shape arguments mismatch (
6ba68a0581).
- dev.typeparams refactors simplified variadic calls and
- Go 1.19 — Done:
- user-visible
selectbecame inlinable (80a7504a13). - The shape-argument restrictions were refined (
cb458c05a8,19156a5474,f2b1cde544).
- user-visible
- Go 1.20 — Done:
- With unified IR on, inlining fails gracefully (
f2851c67fd) and more inlining is allowed, lifting the shape restrictions (52016be3f4). - Transitive inlining is deferred until the AST is edited (
f983a9340d). - Cost discounts for
encoding/binaryappends and zero-offset field addresses (629c0b3a6e,638c9aad5f). - A latent
CalleeEffectsissue was fixed (25d74f324d). - Coverage-counter updates do not count against the budget (
4a459cbbad,317f2a7df6).
- With unified IR on, inlining fails gracefully (
- Go 1.21 — Done:
-m=2flags “large” functions (733ba92187), andisBigFuncrecognizes unified IR codegen (6f6276ce36).- Operations inside SCCs are reordered so callees are analyzed before callers: +47 inlinable functions in the runtime (
f5c7416511, followed by6b2ad9ef50and63edd418b6). - user-visible Functions that construct closures became inlinable, +57/−10 inlinable functions in std (
445e520d49). This landed after several revert rounds caused by closure-name and position issues. - Hidden closures that become unreferenced after inlining are dead-coded (
f1caf1aa1c, reworked inea69de9b92).
- Go 1.22 — Done:
checkptrarguments may be inlined (28a8896d57).- A small tweak for range-func panics (
2380862c7c). ir.CallExpr.GoDefermarksgo/defercalls, which are now normalized in typecheck (0709f1bb00).
- Go 1.23 — Done:
- Cost analysis was separated from applying inlining (
b158ca9ae3). - Runtime functions are not inlined under
-d=checkptr(f0de94ff12). - A fixpoint refactor (
a245aced26,28e0052ee7) was reverted in9159d71a37(#66261).
- Cost analysis was separated from applying inlining (
- Go 1.24 — Done:
- Calls to
PPARAMfunctions are favored (a464ffda3e), and calls to closure variables are discounted (ad7f736d8f). - user-visible Closure inlining is strongly favored: double budget (
170436c045), and a very high budget for closures called once (d524c954b1). - Interleaving was refactored into a fixpoint loop (
4aa1c02dae). abi.NoEscapecounts as a cheap call (b6efbd4efc). “Cheap” runtime calls are detected bygo.runtime(c2b580a474).- A robust “contains closure” test was added (
889abb17e1).
- Calls to
- Go 1.25 — Done:
- Infinite recursion and out-of-memory when inlining closures were fixed (
34073a736a,4f45b2b7e0). - The recursion test is now based on inline positions (
cad4dca518), which made older checks unnecessary (1cf6b50263, +0.5% cmd/compile text). ir.ContainsClosurewas added (a08984bc8f), andCallExpr.NoInlinewas removed (3033ef0016).
- Infinite recursion and out-of-memory when inlining closures were fixed (
- Go 1.26 — Done:
- Superfluous re-slicing
s[:]is ignored in cost (a67977da5e). - Functions using
runtime.deferrangefuncare not inlined (cd668d744f).
- Superfluous re-slicing
- Go 1.27 — Done: walk replaced
mapaccess1*calls withmapaccess2*(62d08234b7). Left open: this was reverted outside this bucket and redone in 1.28. - Go 1.28 — Done:
- user-visible Functions that call
recoverbecame inlinable (0977a589b4). mapaccess2is inlined intomapaccess1ininternal/runtime/mapswith inliner support (defa1f0155).
- user-visible Functions that call
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:
walkOnestopped touchingir.Nodes in its inner loop. Build time fell from 91.8s with go1.24.0 to 65s (04a9b16f3d).- Work is reordered to visit locations likely to reach the heap first. Build time fell from 91.8s to 17.6s, about 5× (
74304cda29). - Packages with many closures got faster (
f506ad2644). - In 1.28,
walkOnereuses one queue: −28.8% bytes allocated (ab4ad11bad). Roots with equivalent state are coalesced into one multi-source walk: TypeScriptGoChecker −48.5% in compilebench (372c766cdd). For this, the predecessor paths used for diagnostics became immutable snapshots (e78076d579).
Timeline.
- Go 1.18 — Done: dev.typeparams moved call handling out of order into escape:
- Call logic was moved from
order.gointo escape (0a0e3a3dea), escape analysis of calls was refactored (99732b9070), and the package was split into multiple files (3e6219c6a9). - The special escape-analysis tags were removed (
077100dfcd). - Method-expression calls are analyzed (
eb691fdd62), and method calls are rewritten into method expressions (2493c72742, then during typecheck in0cf71f7f92). - Constant
go/deferarguments are wrapped without allocating (a72a499c24). Diagnostics are skipped for wrappers (df00abc61b).walkGoDeferwas simplified (e59a19cceb). CallExpr.PreserveClosureand theSetClosureCalled(false)hacks were removed (1a445dab66,70f4ab6565). An outdated TODO was dropped (73162a54c2).- Defers lost variadic calls and
_defer.siz, and open-coded defer metadata was simplified (5b350505da,74b0b2772a,c0a86c10f1,b20747334a). ORECOVERwas desugared toORECOVERFP(574ec1c645). “Hidden parameters” were formalized (5dac279fbd).- Method-value wrappers moved to walk (
f99b3fe2ab,fe73f28dc5), andPartialCallTypewas renamedMethodValueType(4676c3675e). OCALLMETHis caught in the backend (aee209c044,180c338c68,27e3b797bb).
- Call logic was moved from
- Go 1.19 — Done: values with alignment greater than
PtrSizeare forced to the heap (162b88265e). Under-asan, making conversions tounsafe.Pointerescape (5fd0ed7aaf,c379c3d58d) was reverted in599d5395eb. Left open: nothing noted. - Go 1.20 — Done:
OITABandOCHECKNILare supported (0c2f87f23d). Left open: nothing noted. - Go 1.21 — Done: fixed memory corruption from escape analysis of
unsafe.SliceData/StringData(198074abd7) and genericgo/defercalls (93f10b8829).-m=2output was fixed for recursive functions with closures (1e12c63aac). Left open: nothing noted. - Go 1.22 — Done:
- Indirect closure calls are optimized, along with tracking of
*p=and mutating builtins (ff47dd1d66). blankLocbecame transient,transientwas flipped to!persists, and the attributes became a bitset (570763e0ec,4e336b8e1e,1038fc207b). Leftovergo/defernormalization code was removed (b805e18fbf).- user-visible Zero-copy string→[]byte conversion when the bytes are never mutated (
c8adb3004f) was reverted the next day (b75ac7a8d5), then restored behind the-d=zerocopydebug flag (925d2fb36c, #2205). ir.StaticValueimproved andir.StaticCalleeNamewas extracted (3be2176d92).ORECOVERis desugared in typecheck (52c3e8c735). Fixed escape analysis of stringmin/max(34416d7f6f).
- Indirect closure calls are optimized, along with tracking of
- Go 1.24 — Done: fixed escape analysis for rangefunc bodies, which the “-range
” closure naming had broken ( db40d1a4c4).hash/maphash.Comparable[string]no longer escapes its argument (50a8b3a30e). Left open: nothing noted. - Go 1.25 — Done:
- The solver speedups
04a9b16f3d,74304cda29andf506ad2644. - Escape uses an
ir.ReassignOracle(e89791983a). unique.Makemodels the data flow of non-string pointers (5e6a868b28), and SSA eliminates the string copy in calls tounique.Make(27ff0f249c).-m=2logs became more accessible (8bf816ae68,c1aad08901).- The literal and constant allocation avoidance described in the next thread.
- The solver speedups
- Go 1.27 — Done: user-visible fewer heap allocations.
- Singly-assigned func variables are treated as static callees, which helps recursive closures (
f4bfb1a9c6). - Callees are represented as a slice (
9be7615aa2), and multiply-assigned func variables merge their callees viateeHole(5af294bac7). - Call analysis uses
ReassignOracle.StaticValue(1bcfdf2df2).
- Singly-assigned func variables are treated as static callees, which helps recursive closures (
- Go 1.28 — Done: the queue reuse (
ab4ad11bad), immutable diagnostic paths (e78076d579) and walk coalescing (372c766cdd) described above. Left open: nothing noted.
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 doc Directly freeing user memory to reduce GC work cited in this thread's commits
- proposal #74299 runtime, cmd/compile: add runtime.freegc and runtime.freegcTracked to reduce GC work (GOEXPERIMENT=runtimefreegc) referenced by 2 of this thread's commits
- tracking issue #71359 cmd/compile: avoid allocs by better tracking of literals for interface conversions and make referenced by 8 of this thread's commits
- motivating issue #8618 runtime: don't allocate for non-escaping conversions to interface{} referenced by 2 of this thread's commits
- discussion a safe free of memory proposal, runtime.FreeMemory() golang-nuts · linked from #74299
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.
- Go 1.21 — Done: an empty
makesliceis rewritten to azerobasepointer (fc814056aa,0d8d181bd5). Left open: nothing noted. - Go 1.25 — Done: user-visible fewer allocations.
- Constants are propagated into interface conversions (
ed24bb4e60,326e5e1b7a). Zero values use the globalzeroVal(c3bb27bbc7). Composite literals convert to interfaces without allocating (f4de2ecffb). - Follow-ups preserve side effects when rewriting with literals (
988a20c8c5), keep the optimization on under coverage (479b51ee1f), addliteralallochash(e46d586edd), and fix line numbering (66536242fc). - Variable-sized
makesliceis stack-allocated, pointer-free elements only (7a427143b6). Pointerful elements were then allowed (2d050e91a3), and static len/cap are resolved (ba3f568988). appendbacking stores are stack-allocated, cmd/go <0.1% bigger (ce88e341b9).variablemakehashand the threshold flag were added; they found the culprit in #73199 (2c9689ab0e). The optimization is off under-N(21acfdc4ef).- Stack buffers are over-aligned to match heap alignment (
c8bf388bad). - In a related backend change, scalar heap allocations are aggregated (
5fc596ebe7).
- Constants are propagated into interface conversions (
- Go 1.26 — Done:
appendbacking stores are stack-allocated even when the result escapes, throughmove2heap(32f5aadd2f).move2heapis no longer placed inside loops deeper than the declaration (3c6bf6fbf3).len([]T{})is propagated intomake(84db201ae1).- Alias analysis plus
runtime.growsliceNoAlias(4879151d1d) sit behindGOEXPERIMENT=runtimefreegc, which was set off by default for 1.26 (5a347b775e).
4879151d1dnotes that the final slice memory is never freed and is left to other work;runtimefreegcstays off. - Go 1.27 — Done:
- A range-over-slice transition is marked in the backing-store analysis (
6e04f9f830, #79909). - Prove handles
Phi(SliceMake, SliceMake)so bounds checks still go away after the stack/heap split (2a1d605e7f, #77375).
- A range-over-slice transition is marked in the backing-store analysis (
- Go 1.28 — Done:
- A slice whose element interior escapes through
&s[i].fis no longer stack-allocated (2b9e6788d4, #81612). - Non-escaping appends are not given a
move2heap(5391dd543a, #82126). make’s length-before-capacity evaluation order is preserved when rewriting with literals (d98516a9d2).makeslicecopywas extended beyond simple non-address-taken variables (98ef86b779).
- A slice whose element interior escapes through
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 doc Go 1.22 inlining overhaul linked from #61502
- tracking issue #61502 cmd/compile: overhaul inliner referenced by 9 of this thread's commits
Design. Than McIntosh’s inlheur package computes function properties and stores them in export data, about 2–3% larger (323cf73091):
- function flags such as “never returns” and “panic path”;
- per-parameter flags such as “feeds an if” or “feeds an indirect or interface call”;
- result flags such as “always returns the same constant” or “returns an inlinable func”.
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.
- Go 1.22 — Done: [user-visible, experiment only]
- The experiment was added (
fbf9076ee8). - Function properties and their framework (
03d457a221,b888ec4ac3), written to export data (323cf73091). - Analysis of “no return” (
e844d72421), results (3f0f76734e) and params (906a073b0b). - The call-site table (
19ca2337c5) and scoring (746f7e1744). The message says the adjustment values are arbitrary and “will almost certainly need tuning”. - Trace flags (
dab9c502c4,a9cfbec17b). Budget computation was refactored (6eb31c1a00), and the func-level never-returns flag is used (eac1f3e461). - Inlining is decided by score, with the relaxed
CanInlinebudget (0b07bbd2be). - Rescoring by result use (
00073c25f6), ConstExpr removal (0725410df5,4651c0c6d9,a86ef0409b), the “returns inlinable func” heuristic (f56c293319), and call-result scoring (9e90a15ba4). - Inlinability is revised by the largest possible score (
04d64a3b36). - The package-level table was removed, analysis was regionalized, and call sites exposed by inlines are scored (
74350dd603,993ca35bd4,a0df23888f). -d=inlscoreadj(8d41d053e0).- Analyzers,
ir.StaticValueuse,AnalyzeFuncand scoring were refactored (d21afcdf00,bbcd85528c,3f04f959d2,322cd4e807). - Fixes and speedups:
3255fca993,5239c91351,cfb281754e,15fa7a84b8,d3d5173759,9d3b3416a4. - Enablement is checked via
internal/buildcfg(5b6d3dea87).
746f7e1744says the score values need tuning; the experiment stayed off. - The experiment was added (
- Go 1.27 — Done: PGO plumbing fixed a crash where
revisitInlinabilityclearedfn.Inlfor PGO-hot functions undernewinliner(e296659b12). Left open: nothing noted. - Go 1.28 — Done: a nil
curfuncis passed during analysis (c0c6468861). Left open: the experiment is still off by default.
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)
- background Bounds Checking Elimination
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:
- 1.26 walks the blocks in a topological order, so facts are learned in a single correct pass (
53be78630a, #68857). This removed severalflowLimitworkarounds. - 1.26 moved the division optimizations out of the generic rules into new
divisibleanddivmodpasses that run after prove, followed by a “middle opt” generic pass (9bbda7c99d). Prove now sees the originalx/candx%coperations, and the rewrites still happen beforedecompose. - Loop BCE (
loopbce.go) finds induction variables. Since 1.22 it inverts loops to count down to zero where possible (bac4e2f241). - 1.27 fixed an overflow check that could remove a needed bounds check (
f5b77a7e2f, CVE-2026-27143). - 1.28 fixed “time traveling” proofs: facts about a value’s arguments were applied to the whole block, before that value had executed (
a595f29385).
Timeline.
- Go 1.19 — Done:
- Results of
math/bitsoperations are known nonnegative (3bd8c78575). - Constants discovered by prove are folded: about 70k hits building
go, binary 0.04% smaller (e1e056fa6a). And(1efe38750a). Upper bounds<= maxintfixed (31b8c23c57).- The induction variable detector was reworked for correct overflow handling (
2acd3646fc, #53653, #53663).
2acd3646fcsays induction variable detection “is still not quite right”. - Results of
- Go 1.20 — Done:
- The relation between
x+deltaandx(d2e0587f77), with 8/16-bit deltas (2addbf3f2a). - Bounds for strided-loop induction variables were tightened (
af7f067e0d). - Signed relations for constant integer phis: −1.4% total size (
d96eb826cb). - Bitwise
OR(3680b5e9c4), reverted for compile time (a6ddb15f8f1.21) and relanded in 1.21. - An index out-of-bounds check in
walkIndexwas removed (98f5152368).
ORsupport was pending a compile-time fix. - The relation between
- Go 1.21 — Done:
- More bounds information from logic operators (
9be533a8ee). - loopbce handles 8-, 16- and 32-bit induction variables (
bb5ff5342d). - Unsigned division, modulus and right shift (
35755d772f).ORrelanded (63a08e61bd). - Slice-to-array-pointer casts are known non-nil (
0d9eb8bea2), while a slice’s pointer is not assumed non-nil (61bc17f04e).
- More bounds information from logic operators (
- Go 1.22 — Done: loops are rewritten to count down so only the iterator stays live. This covers
for i := 0; i < n; i++, index-freerangeover a slice, and range-over-int (bac4e2f241, #61629).findIndVarno longer matches disjoint loop headers (8b4e1259d0). Left open: nothing noted. - Go 1.23 — Done: integer comparisons with numeric bounds improved (
bbd0bc22ea). Bug workarounds were removed from loop inversion (ff35c382eb). Left open: nothing noted. - Go 1.24 — Done: the limit-table rewrite.
- Dense limit table (
553443d41f) and the constant rewrite (3b96eebcbd), fixing #66826, #66692 and #48213. Its TODO said “remove constant logic from poset code”; that was done inb97971ea2e. - Constant ranges propagate through multiplies and shifts (
a4a130f6d0). Boolean and pointer relations (5925cd3d15). - The domain relation table was reorganized and the jump-table case simplified (
f32ec41df5,aba16d17c5). - Simplification: HelloSize −0.83%, CmdGoSize −1.53% text (
9b4268c3df). checkbceruns afterfuseLate(864aa86448).- Jorropo’s limit derivations:
bits.Len*(0c7523ff59) and its constant folding (090f03fd2f);bits.TrailingZeros*(f49fe2955d).- XOR (
49621cc311), OR (57df33814a). - Modu, Divu, and Div/Mod with positive arguments (
194fa2eb6c,e23ebec90d,68c431e89f). - Trunc, Complement, Negation (
4f2c0e5d08,2f3165973f,820f58a27f).
- Fixes: off-by-one (
f259e4c916), unsat checked before local facts (3de175f383), cleanups (b538e953ee,9a4fe7e14a). - min/max phis (
4dcbb00be2). Limits for newly created values (cb163ff60b,5123f38e05).
flowLimitworkarounds for #68857 (min/max, ModU, And) remained until the 1.26 topo-sort. - Dense limit table (
- Go 1.25 — Done:
isGenericIntConst(bdc2d856a8).- Bool-to-uint8 limits (
b7c9cdd53c). Duplicate unsat reports are suppressed (00635de759). - min/max inside
flowLimit(d2842229fc).OnesCountlimits (c00647b49b,99411d7847). - Bit size derived from type size (
644b984027). - Bounds on signed
x % 2^k(3baf53aec6).
- Go 1.26 — Done:
- Transitive len/cap properties (
a3295df873) were reverted (d0a64f7969, #74974). Transitive proofs for safe unsigned and signed adds and subs landed instead (e5f202bb60,1a72920f09,d574856482,f32cf8e4b0). - Loop-return domination is checked in both controls (
8cd85e602a). StringLenrules (f703dc5bef,62cd044a79).- Maximum len/cap from element size (
9fcb87c352). - Bounds checks eliminated for slices of the same length (
519ae514ab). - Double-offset
IsInBounds(1caa95acfa), slice len/cap arithmetic (d91148c7a8, fixed inf84f8d86be), chunked indexing with reslicing (20c9377e47). - Slicemask elided for
len==c>0(ec70d19023,1bca4c1673). - Topo-sort walk (
53be78630a), with the #68857 workarounds removed (a50de4bda7,2ce5aab79e). - The divisible/divmod passes and “middle opt” (
9bbda7c99d). Unsigned rounding-up divide (77dc138030). Multiplies proved to be by 0 or 1 becomeCondSelect(24af441437). - Subtract idioms (
c12c337099). Rsh becomes RshU for positive arguments, with unsigned relations propagated (ec92bc6d63,3820f94c1d). Right shift modelled precisely (235b4e729d). - Unsigned-division control-flow fix (
dc42565a20). Overflow fix (f87aaec53d). - Unsigned power-of-two detection (
f3582fc80e,bd94ae8903,7b67b68a0d,26da1199eb,72147ffa75,84b070bfb1). - Cleanups (
a0f33b2887,b8468d8c4e,bc15963813,e64023dcbf). Brute-force limit tests improved limit addition (03fcb33c0e,37ce4adcd4).
9bbda7c99dmessage debates the naming of the opt passes (“Perhaps instead they should be generic, opt, and late opt?”). - Transitive len/cap properties (
- Go 1.27 — Done:
- Multiple induction variables per block (
8afbae3e51), including induction-variable pairs such asi, j = i+1, j-1(faeffecf86, #45078). - Ctz and PopCount modelling (
a4fda8d32a,f35fb95503,417d5e6627).noLimitcleanup (8f739162e6,82ef9f5b21). - A stronger subtraction bound using non-equality (
14a4cb13e3). - No-op
And/Orare removed via prove (33504a8696,e7b0a53f31), with the last #68857 workaround dropped (2df8c060e8). - Phi of two
SliceMakes (2a1d605e7f). - Loopbce: overflow logic fixed (
f5b77a7e2f, CVE-2026-27143, #78333). Loops that would overflow are not inverted (5edb5d729d). Minimum-integer step handled (e7c75c3ae8). Count-down code cleaned up (831c489f9c).
- Multiple induction variables per block (
- Go 1.28 — Done:
- Constant-delta additions (
8ee6c488c2). The induction-variable minimum is visible in its block (6547138e2b). - Signed↔︎unsigned limit propagation (
13a7b71d85). Unused poset unsigned machinery removed (0c0dacdf0b). - Time-traveling proofs fixed (
a595f29385). Slices of the same length are added later (f85791f6df). (x+y)-x == yand related identities (4d9381e946).Const64for 64-bit slicemasks (395f4f5e3f).IsInBoundsRsh bit width (d9884eed65).convertIntWithBitsize(12a1a35032).- Direct jump-table branches are not scanned (
9383561a67).
- Constant-delta additions (
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
- tracking issue #78633 cmd/compile: do bitwise constant folding referenced by 11 of this thread's commits
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.
- Go 1.27 — Done:
- The pass itself (
7a8dcab743). - Support for booleans and Eq (
b9e1876c11), Zext (1ad012aa6b), CvtBoolToUint8 (d75902b195), Sext (8963c303b4), Neq (9c0a8a2b46), Lsh/RshU/Rsh (977041b065,767140eff2,e133fb1569,c72ba16e07), Trunc (2bb808bfc2). - Call stack use is limited (
be9da6ce60). Shift iteration is faster (880ef11ecf). Debug printing improved (e73e73470e).
7a8dcab743leaves merging with prove’s limits for “someone … in the future”. - The pass itself (
- Go 1.28 — Done:
- Dead values are optimized when debugging (
548c25ea38). - No-op
And/Orare removed (4f21e8a963,8213feb422). - The toposort is shared with prove (
4b45efba20,fc44886932). On tsgo,fc44886932measured 491.3s versus 492.7s on master.
- Dead values are optimized when debugging (
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.
- memcombine (1.21,
cedf5008a8). A separate generic pass, run after late fuse, merges adjacent narrow loads and stores (for examplebinary.LittleEndian.Uint64written byte by byte) into wide ones. Before it existed, this needed thousands of per-arch rewrite rules. It handles constant stores, stores of consecutive loads, different-size stores, and bool constants. On architectures without fast unaligned access it requires alignment. - DSE (
deadstore.go). Removes stores shadowed by later stores to the same location. It learned to handle:- constant pointer offsets;
- equivalent
LocalAddrs; InlMarkas a non-read;- multiple shadowed ranges per location (
d474f1fd21), which elides zeroing inb[0]=1; ...; Moves from read-only memory and deadMoves;CondSelect.
- Store-to-load forwarding. Generic rules that let a
Loadread the stored value, or read through aMoveto its source. Since 1.27 this applies to field reads from a just-copied struct, sos.h.Value().typno longer forces a full copy (38ac6577cb, #77720). - Alias analysis.
disjointgained one type-based rule, “a non-pointer type can’t alias a pointer” (11f7ea8ce0). Since 1.28 it uses the types recorded inStoreops. CSE can match loads across disjoint stores by keeping a table of memory definitions (c34b99a5e2). - mem2reg (1.28). A port of LLVM’s PromoteMemoryToRegister. It promotes address-taken stack scalars whose address is used only by loads and stores into SSA values.
- Stack-slot cleanup.
dead auto elimbecame more aggressive, stores to unread parameters are removed, andelim unread autosruns after lowering.
Timeline.
- Go 1.20 — Done: more constant stores combined on amd64 (
09932f95f5), plus store combining of values (e7307034cc). These were per-arch rules, the predecessor of the memcombine pass. Left open: each architecture needed its own rules, which the 1.21 pass replaced. - Go 1.21 — Done: the separate memcombine pass (
cedf5008a8). It was made robust to reassociated expressions (6042a062dc). Left open: nothing noted. - Go 1.22 — Done:
- Memcombine of stores whose values come from consecutive loads (
e713d6f939). - memcombine state is reset between blocks (
08cdfd06ed), and big-endian multi-byte elements were fixed (bda1ef13f8). - DSE handles constant pointer offsets (
43b57b8516, #63657).
- Memcombine of stores whose values come from consecutive loads (
- Go 1.23 — Done: bool constants are included in memcombine (
802473cfda). DSE treats equivalentLocalAddrs as equal (57026007c8). Left open:802473cfdapoints to “trickier cases … see TODO at memcombine.go:503”. - Go 1.24 — Done:
- An
InlMarkis not a read for DSE, which avoids double zeroing (b0f7be3cfa). - Zeroing right after
newobjectis removed even when builtin decomposition runs late (b2cdaf7346). - CSE removes redundant
OpLocalAddrs so DSE and memcombine see more (c4e6ab9750, #70300).
- An
- Go 1.25 — Done:
OpLocalAddris not a load in DSE (0825475599).- Type-based alias analysis (
11f7ea8ce0, #70488). - memcombine handles different-size stores (
113b25774e), with an offset fix (d6c29c7156). - Store-to-load forwarding works with copy-compatible types (
16a6b71f18).
- Go 1.26 — Done: DSE tracks multiple shadowed ranges (
d474f1fd21). Dead auto elimination became more aggressive (c7ccbddf22). Stores to unread parameters are removed (38b26f29f1, also on dev.simd as2b50ffe172). Left open: nothing noted. - Go 1.27 — Done:
- CSE of loads across disjoint stores (
c34b99a5e2), recognizingOpVarDef/OpZeroas memory definitions (13cab13f78). Memory-access overlap detection fixed (3985ca0b62). - DSE learned about
Moves (da6a4cd70a, ~<0.1% code) and usesAuxIntfor theMovesize (6559f48782). - memcombine uses
HasPointersto match the write-barrier check (deaf3e6789). - Small
Loads are forwarded throughMove(38ac6577cb).
0offset case did not match the forwarding rule, fixed in 1.28. - CSE of loads across disjoint stores (
- Go 1.28 — Done:
disjointusesStoretypes (5e18e24123).- DSE handles
CondSelect(00ba484362). - Whole-copy loads are forwarded through
Move, including offset 0 (bfc0f5497b). - riscv64 memcombine requires alignment (
58e8638c80). - The
mem2regpass (59b9e2dc57); promoted variables loseaddrtaken(18d746bc25).
59b9e2dc57says it handles only variables whose loads and stores sit in one basic block; “a later CL extends the analysis across blocks with phi insertion”. Some edge cases are “not handled”.
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:
memmovebecomesMove.memclrNoHeapPointersbecomesZero(1fc585dc2f), which DSE can then remove.memequalwith small constant sizes becomes loads and compares.
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.
- Go 1.18 — Done:
memequal(x, const, sz)is inlined for 2/4/8 bytes, firing ~100 times in the compiler (810b08b8ec). Left open: nothing noted. - Go 1.19 — Done:
memmovewith a known size is inlined on loong64 (a6c95e75d9). Left open: nothing noted. - Go 1.20 — Done: the
memmoveoptimization no longer trusts the destination pointer’s base type (e283473ebb). Struct-compare inlining uses a cost heuristic (6605686e3b). Left open: nothing noted. - Go 1.21 — Done:
- Type equality functions are generated during walk (
864b54e2c3). The unusedeqForwas removed (89c3f7c119). - Known-size
memclrNoHeapPointersbecomesOpZero(1fc585dc2f), with PPC64 support (ebe49f98c8). Negative-size memclr is not inlined (07559ceb72). clearon slices with zero-size elements was fixed (0b65b02ba5).memequalwas removed from more string compares (6b165577fe).
- Type equality functions are generated during walk (
- Go 1.22 — Done:
s==sis folded to true for strings, which matters for NaN tests in generic code (d0964e172b, #60777). Left open: nothing noted. - Go 1.23 — Done:
- Redundant
cmpstringcalls are CSE’d (1a0b86375f, rule fixed in9f9dd2bfd8). - Zeroing for
append(s, make([]T, n)...)avoids a past-the-end pointer (93e3696b5d).
1a0b86375fnotes a redundant</<=test remains “because the compiler doesn’t know that cmpstring only ever returns -1,0,1”. - Redundant
- Go 1.24 — Done: constant-size memclr is inlined on loong64 (
91d07ac71c). Race instrumentation is added whenwalkCompareemitsmemequal(09eefb3a4d). Left open: nothing noted. - Go 1.26 — Done: the
MemEqintrinsic, initially on arm64 (dda7c8253d).memequalis expanded for lengths 3, 5, 6, 7 (a5fa5ea51c). Left open:dda7c8253dsaysLoweredMemEq“may be lowered differently for more constant size cases in future (for other targets …)”. - Go 1.27 — Done:
- Small constant-size
MemEq(c7258178cd). - Per-signature equality functions (
30dff416e4). - More
OpMoveinstead ofmemmoveon loong64 (93b78326c3). - A preemptible
memclrNoHeapPointersPreemptiblefor large pointer-free clears (50126a8e44, andadf9c27874for nosplit callers) was reverted (5f5f4ccdb3,0a750df0b2) because it exposed unsafe uses ofnotInHeapSlice.
30dff416e4says==code generation outside equality functions is unchanged (“Maybe a TODO”). Preemptible memclr awaits thenotInHeapSlicefix. - Small constant-size
- Go 1.28 — Done: constant-size equality with 32-bit
Const32lengths (8aec6766e9, #63131). Left open: nothing noted.
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
- proposal #62737 cmd/compile: merge temporaries and named variables by GC shape referenced by 5 of this thread's commits
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.
- Go 1.23 — Done:
- user-visible Initial merging (
89f7805c2e) was reverted (0bf6071066) and relanded default-off (875332b8a2). - Address-taken candidates were added (
82e929e4f8), and region formation was enhanced (e01b1eb289). - The flag was flipped on by default (
ecfddf4841).
89f7805c2enotes that the merge-locals liveness pass would ideally fold into the stack-map liveness pass, but “this may be difficult”. - user-visible Initial merging (
- Go 1.28 — Done: only address uses known not to propagate the address are accepted; any other use aborts merging for that local (
06bc6592c3, #80127). Left open: the message calls this conservative, with tracking “uses of uses” as a possible refinement.
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)
- motivating issue #5496 cmd/compile: compile dense, pure-integer "switch" into jump table referenced by 2 of this thread's commits
- motivating issue #34381 cmd/compile: use hashing for switch statements referenced by 2 of this thread's commits
- discussion Jump Tables in 6a golang-nuts · linked from #5496
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.
- Go 1.19 — Done:
- Jump tables (
1ba96d8c09); arm64 support (540f8c2b50). - String switches use a jump table on lengths (
3d8cb26504), with prove changes so the length test is not repeated. - Jump tables are off under
-N(78bea702cd). The table symbol is local (c5da4fb7ac).
go119UseJumpTableskill switch remained until 1.21. - Jump tables (
- Go 1.20 — Done: a better split for string binary search (
69aed4712d). Jump tables are off when spectre retpolines are on (1eb0465fa5). Left open: nothing noted. - Go 1.21 — Done: the
go119UseJumpTablesflag was removed (4e3abee245). Better code for switches folded to a constant early (74b52d9519). Left open: nothing noted. - Go 1.22 — Done:
- Jump tables for large type switches (
4711299661). - Interface type-switch improvements (
28f4ea16a2) and the type-switch cache (39263f34a3). - Caches in front of type assertions (
cbcf8efa5f) andconvI2I(afd7c15c7f). - The type hash comes from the itab field (
e0948d825d), using the new runtime type mechanism (9ab5121691). - SCCP propagation into jump tables was fixed (
788a227759), and so was constant-folding an out-of-range index (f6509cf5cd).
- Jump tables for large type switches (
- Go 1.23 — Done: the jump table symbol is static (
2908352980). Left open: nothing noted. - Go 1.24 — Done: a type switch with a single runtime-known type and a case variable is optimized (
711552e98a). Left open: nothing noted. - Go 1.26 — Done: propagation of an out-of-range jump-table index was fixed (
b6b8b2fe6e). loong64 jump tables (af6999e60d). Left open: nothing noted. - Go 1.27 — Done: user-visible faster switches.
- Lookup tables for constant-returning integer switches (
2a902c8a8a), extended to all constant return types (6797caf71a) and fallthroughs (1f5c165a81). - A missed case in binary-search-for-switch was fixed (
3f3387fab8). - Under retpoline, binary search is used for SIMD non-constant immediates (
d5ebe8100d).
- Lookup tables for constant-returning integer switches (
- Go 1.28 — Done:
- Lookup tables support assignments (
36042fe95b). uint64indices are truncated for imm8 jump tables (b590fd1075).- Prove skips direct jump-table branches (
9383561a67). Switch statement positions fixed (2a2f42823b). - Jump tables on riscv64 and mips (
8e35b7dc52,c8337c9bb2,84ee19cf1a).
- Lookup tables support assignments (
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.
- Go 1.18 — Done: fixed AST generation in devirtualization on dev.typeparams (
942bcc2d4f). Left open: nothing noted. - Go 1.20 — Done: fixed a unified IR devirtualization bug (
d6294e00f0).defer/gocalls are not devirtualized (c82304b712).OCALLMETHis desugared toOCALLFUNCwithin devirtualization (4f8bc6224b). Left open: nothing noted. - Go 1.26 — Done: user-visible more calls devirtualized.
- The concrete-type analysis (
de9da0de30, #69521, #64824). - Fixes for
OCONVNOP(5a9ef44bc0), non-PAUTO assignments (4dbf1a5a4c), positions (0ddb5ed465) and typos (b9d86baae3).
- The concrete-type analysis (
- Go 1.27 — Done: types are compared by pointer identity (
09031d907c), with better debug logs (4d1f503396). Impossible type assertions are pruned in generic functions (343fbe2971). Left open: nothing noted.
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
- proposal #60078 spec: less error-prone loop variable scoping listed in Go: A Documentary
- design doc Proposal: Less Error-Prone Loop Variable Scoping golang/proposal
- tracking issue #57969 cmd/compile: add GOEXPERIMENT=loopvar referenced by 1 of this thread's commits
- discussion redefining for loop variable semantics listed in Go: A Documentary
- motivating issue #20733 proposal: spec: redefine range loop variables in each iteration listed in Go: A Documentary
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.
- Go 1.21 — Done: [user-visible, experiment]
GOEXPERIMENT=loopvar,-d=loopvarand-d=loopvarhashwere added (c20d959163).- A flag on FOR/RANGE preserves the semantics across inlining (
5a7793b7b8). - The return-in-loop optimization was restored correctly (
23e8f43cb1). - “loop-transformed” logopt (
dace96b9a1), logging tweaks (59d19ba797,1524bd1c78), and bisect report improvements (b3d1cce3eb,48f8c5c037).
5a7793b7b8says the change would later be “triggered … by the specified version for compilation”. - Go 1.22 — Done: user-visible the new semantics apply to files compiled at Go 1.22+, with version-dependent handling of the experiment and debug flags (
fe1daf2e43). Left open: nothing noted. - Go 1.25 — Done: an ICE in the transformation was fixed (
a0dc7bf084). Left open: nothing noted. - Go 1.27 — Done: version detection with line directives was fixed (
b408256be7). The compiler’s ownx := xcapture workarounds were removed (8da2be3cd0). Left open: nothing noted.
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
- proposal #61515 testing: add testing.B.Loop for iteration referenced by 3 of this thread's commits
- tracking issue #73137 testing: implement b.Loop keepalive without preventing inlining, which can lead to heap allocs referenced by 3 of this thread's commits
- discussion Go 1.24 BLooper golang-nuts · linked from #73137
- proposal #61179 proposal: testing: add Keep, to force evaluation in benchmarks linked from #61515
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.
- Go 1.24 — Done: user-visible variables are kept alive in
b.Looploops by disabling inlining and devirtualization there (fe2da30cb5). A one-time ramp-up was added, andb.Loopitself is inlined (558f5372fc). Left open: the approach was revisited under #73137, and the 1.26 pass replaced it. - Go 1.25 — Done: a stopped timer in
B.Loopis detected (5918101d67). TheB.Loopdocumentation was clarified per #73137 (ab2926291b). Left open:ab2926291bhints that “the exact approach might change in the future”. - Go 1.26 — Done: user-visible the bloop KeepAlive pass (
22f24f90b5). Only addressable nodes are kept alive (509ddf3868), the name lookup was fixed (3fd9cb1895), and unnecessary interface conversions are avoided (2ac1f9cbc3). Left open: thebloop.goTODO aboutOpVarLive. - Go 1.27 — Done: extraneous heap allocations from autotmp scope are avoided (
bae9486d07). Blank nodes are kept alive (9ce4f3876e). Left open: nothing noted. - Go 1.28 — Done: switch case expressions are not rewritten (
412d6e161f). Compiler#-names are removed from keepalives (903c4c08b0). Uses ofir.CurFuncwere replaced (611b566506). Left open: nothing noted.
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)
- proposal #61405 spec: add range over int, range over func linked from #61717
- tracking issue #61717 cmd/compile: add new range behind GOEXPERIMENT=range referenced by 1 of this thread's commits
- discussion user-defined iteration using range over func values listed in Go: A Documentary
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:
- room for rangefunc defers (
390763aed8); - a live closure pointer and a named
#yield%dbody closure for debuggers (44079f39eb,f27a40ce5f); - escape-analysis naming (
db40d1a4c4); - “bad iterator” panic status (
d311cc95dc); - variadic signatures and labels in nested closures (
670038a5a5,f665ff8bc1).
Timeline.
- Go 1.20 — Done:
OFORUNTILis no longer used (661146bc0b).- Range desugaring was refactored for unified IR, with RType fields for range assignments (
0a503cf43a,d667be8831). - Careful pointer incrementing (
8477562ce5).
- Go 1.22 — Done: user-visible range over integer, initially behind
GOEXPERIMENT=range(a94347a05c). Room for rangefunc defers (390763aed8). Left open: nothing noted. - Go 1.23 — Done: the closure pointer stays live for range-body closures (
44079f39eb). The body closure is named#yield%d(f27a40ce5f). Rangefunc constants are deduplicated intointernal/abi(997b6969fd). Left open:f27a40ce5fsays the name “may not actually be necessary”, pending confirmation with Delve. - Go 1.24 — Done: the “bad iterator” panic status was changed (
d311cc95dc). The saved.closureptris omitted in optimized code (a9ad0ff6ba). Left open: nothing noted. - Go 1.25 — Done: side effects of range over an array are not evaluated when there are none (
8af32240c6). A cast was added in the final-value computation (3452d80da3). Left open: nothing noted. - Go 1.27 — Done: slice/array range loops simplified on loong64 (
134035855c). Variadic signature preserved (670038a5a5), label shadowing fixed (f665ff8bc1), and the ranged-over length is not re-evaluated (7395fa1fd8). Left open: nothing noted. - Go 1.28 — Done: range-clear is avoided when the target depends on the index (
505c369188). An ICE on range over an unsigned constant ≥ 2^63 was fixed (0564ffd5f2, #82234). Left open: nothing noted.
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.
- Go 1.21 — Done: first landing of the pass (
fa50248ce6, Yi Yang, #59399). Left open: it was withdrawn and had to be resubmitted. - Go 1.22 — Done: resubmission (
4ee1d542ed) after the first version broke a dominance relationship caught by ssacheck. Left open: nothing noted. - Go 1.26 — Done: sccp debug messages gained position info (
9a77aa4f08). - Go 1.27 — Done: faster convergence (
033b11f257): no duplicate uses on the revisit worklist, no queueing of uses of values already at Bottom, early exit for Bottom values; ~9% less sccp time over make.bash with identical results. Left open: nothing noted.
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.
- Go 1.27 — Done: pass added (
f9f351b130, 346 lines + tests). Hoisting was made opt-in: only ops on an explicit allow-list may be lifted, instead of a deny-list (f93424692d, fixing #78997). In the same cyclezcsewas taught not to lift arch-feature-gated instructions (7f7ce373ed1.28, 1.28 cycle). A separate new “merge conditional branches” pass was disabled as “late in the cycle, try again for 1.28” (31525e9db0, #80102). Left open: the merge-branches pass is off. - Go 1.28 — Done: arm64 regalloc preloads multi-instruction constants (MOVD+MOVK) before loops when a register is free (
addee8c213), since regalloc never preloaded rematerializable values. Left open: nothing noted.
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)
- motivating issue #71268 cmd/compile: opt: generate conditional comparisons for || and && conditions referenced by 2 of this thread's commits
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.
- Go 1.18 — Done:
branchelimenabled on ppc64 (259735f97a). - Go 1.20 — Done: CMOV disallowed when an argument is pointer arithmetic, which could be unsafe to speculate (
c8057d8569). - Go 1.26 — Done: CondSelect→math for +1 and doublings/halvings (
fcd28070fe,ce05ad448f);cmov(x,x,cond)→x(94645d2413); branchelim supported arches moved intoConfig(2d33a456c6); CCMP aux type and generation (10ac80de77); fused NaN checks with other comparisons (97fd6bdecc); bool→int→bool roundtrip removal (73d7635fae). - Go 1.27 — Done: CCMP pass for ARM64 AND conditions (
fbe55325dd, 5.8k lines mostly generated rules); CondSelect math extended to OR/XOR and contrary forms and non-1 constants (de7f006df2,d9fbe4c90d,0a36b58888,68ee544e87), arm64 inline register shifts (a93560b70a), enabled on all arches with PPC64 keeping ISEL when the constant is not 1 (455d4f41fb). Left open: PPC64 other constants unoptimized. - Go 1.28 — Done: pow2 Or/Xor enabled on amd64 (
3c2bdfff47), always for powers of two (3e6ad2cd0a); fix: do not rewrite IF to CMOV if argument instructions cannot run early (2bc055441f).
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
- motivating issue #75203 cmd/compile: intrinsify reflect.TypeFor referenced by 2 of this thread's commits
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.
- Go 1.21 — Done: fold loads from constant dictionaries and types (
635839a17a,bd3f44e4ff, #59591);ir.Funcexposed to SSA (598cf5e6ac). - Go 1.25 — Done: load constants from itabs (
43e6525986); avoidifaceeqcall when the interface is known direct (072eea9b3b). - Go 1.26 — Done:
abi.PtrType.Elemloads (df29038486, symbol marked usedd767064170); fixed-load logic consolidated (b915e14490);abi.Type.{Size_,PtrBytes,Kind_}loads, with counts in std/cmd such as 75 for Kind_ (5b218461f9); nil checks on.dictremoved (97da068774). - Go 1.27 — Done: fix to sign-extend AuxInt in
rewriteFixedLoad(a78df5aa0a).
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.
- Go 1.22 — Done: zero-sized values dropped from call expansion (
1493dd3cfc); decompose shortcuts apply forPtrShaped(eb832afb23). - Go 1.23 — Done: 0-sized value simplification in copyelim (
6f5d77454e). - Go 1.26 — Done: multi-field structs stored directly in interfaces (
cd55f86b8d,03ed43988f);StructSelectof interface data fields and moreStructMakeargs allowed (bcd25c79aa,72e8237cc1). - Go 1.27 — Done: pointer-shaped types SSA-able despite many 0-sized fields (
6435bf46c1); unwrapping of direct interfaces inStructMake/ArrayMake1(1aa534dbb8); general zero-sized handling and all zero-sized SSA-able (b48b2002fe,0886e65b11); ICE/bounds-check fixes (b16360470c,6f19c3b459). Left open: nothing noted.
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.
- Go 1.25 — Done: pair loads and stores on arm64 (
20d7c57422); write-barrier code shaped for pairing (187fd2698d). - Go 1.26 — Done: stp merging for non-sequential cases (
4c63d798cb). - Go 1.27 — Done: pair a load with a load in a subsequent block (
c8df1410d5); more stp merging (996b985008). - Go 1.28 — Done: NEON loads/stores paired (
3e6eb83f95); regalloc spill/reload STR/LDR fused into STP/LDP in a finalSSAGenFinishstep (aa5fc60a63). Left open: nothing noted.
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
- tracking issue #80409 cmd/compile/internal/ssa: split into subpackages referenced by 14 of this thread's commits
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.
- Go 1.28 — Done: all of the above (~45 commits). Also post-regalloc strict SSA is re-established with inserted phis, allowing dead-code elimination after regalloc and trimming 11,937 dead restores over std (
4cff6b2647, motivated by #74365);IterDomFrontierPlusextracted from the Sreedhar-Gao phi walk (d46cdb752b,98ea765140,a0fa72258f); per-value debug data and CSE memory args in HTML dumps (b60817a581,2cf686292a); compile-speed items: value slice forFunc.Names(e7d8668a6a), no heapLocalSlot(d3c517908d),sparseSetinliveValues(38630629d9). Left open: refactor ongoing.
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