Skip to content

Chapter 17 · SSA-Based Scalar Optimizations

Part 4 · Optimization · about 3 weeks · Previous: Ch 16 · Next: Ch 18

The problem

You are given a function in SSA form: LLVM IR after mem2reg, or the chapter's compact SSA notation. You want an equivalent function that computes less, obtained with scalar optimizations, the ones that look at individual values rather than loops or memory layouts. Five questions organize the chapter. Which values are constants, and which ranges or bits of a value are known (constant propagation, LVI, known bits)? Which computations and branches cannot affect the output (dead-code elimination)? Which computations recompute a value that is already available on some or all paths (value numbering, partial-redundancy elimination)? Which branches and blocks are left over after the others have run (CFG simplification, jump threading)? And how should all of these be ordered, when each can enable the others (phase ordering, combined analyses, equality saturation)? The inputs come from Ch 16 (SSA), Ch 15 (dominator and post-dominator trees, control dependence) and Ch 14 (lattices and fixed points). In LLVM 23 these are the passes sccp/ipsccp, correlated-propagation, bdce, adce, dse, early-cse, gvn, newgvn, gvn-hoist, simplifycfg and jump-threading, which make up much of the function simplification pipeline of default<O2>. In pebblec your four passes run on the LLVM IR that Pebble emits, before the loop optimizations of Ch 18.

What you will be able to do

  • Trace SCCP by hand (both worklists, every lattice change and executable edge) and prove that it finds every constant that constant propagation and dead-code elimination find together, in any order.
  • Compute known bits, demanded bits and constant ranges for small functions, and say what LazyValueInfo and CorrelatedValuePropagation do with them.
  • Mark live instructions for aggressive DCE using control dependence, and explain why ADCE may delete an empty loop only when the function must make progress.
  • Compute the congruence partition of Alpern, Wegman and Zadeck by hand, and compare it with dominator-based hashing on functions where the optimistic and pessimistic answers differ.
  • Solve the four lazy code motion bit-vector systems (anticipability, availability, later, insert/delete) and apply them to a TAC program.
  • Implement pebble-sccp, pebble-adce, pebble-gvn and pebble-simplifycfg as opt plugin passes, check them against LLVM, and measure simple CP vs SCCP and hash-based vs partition value numbering in the comparison lab.
  • Explain the phase-ordering problem with a measured example. Prove that a combined analysis is at least as precise as any interleaving of separate ones. Build a small equality-saturation engine (★).
  • Find where LLVM 23, GCC 15 and Cranelift implement each technique, and read the relevant code.

Prerequisites: Ch 14 (lattices, monotone frameworks, available expressions and very busy expressions, sparse analysis), Ch 15 (dominator trees, post-dominance with a virtual exit, control dependence), Ch 16 (SSA and mem2reg), Ch 9 (LLVM IR), and Lesson 8.3 (DAGs and hash-consing, the idea behind value numbering). Ch 13 (local value numbering) is referenced but not required.

Notation

Shared notation follows the house notation: §1 (sets, functions, logic), §2 (orders and lattices), §3 (graphs and CFGs), §4 (dominance) and §7 (dataflow, SSA, IR). Orientation: as in Ch 14, lattice values rise: \(\bot\) means "no evidence yet" (optimistic: unreachable, undefined, congruent to everything), \(\top\) means "overdefined". So SCCP starts every value at \(\bot\) and every edge non-executable, and the AWZ partition starts with one class and only splits. In this chapter:

Symbol Meaning
\(F\) an SSA function; \(N\), \(E\) its blocks and CFG edges, \(n = \lvert N \rvert\), \(e = \lvert E \rvert\)
\(I\), \(U\) number of instructions, number of SSA uses (def-use edges)
\(\bot\), \(c\), \(\top\) the constant lattice of Lesson 17.1: no evidence, the constant \(c\), overdefined
\(X\) the set of executable edges; a block is executable if an edge of \(X\) enters it (Definition 17.1.7)
\(V(v)\) the lattice value of SSA value \(v\) (Definition 17.1.7)
\([a, b)\), \(\mathrm{CR}(v)\) a wrapped half-open interval; LLVM's ConstantRange of \(v\) (Lesson 17.2)
\(\mathrm{KB}(v) = (Z, O)\) known-zero and known-one bit masks (Definition 17.2.4)
\(\mathrm{DB}(v)\) demanded bits of \(v\) (Definition 17.2.6)
\(\mathrm{CD}(Y)\), \(\mathrm{ipdom}(Y)\) control dependences, immediate post-dominator (Ch 15, Lesson 15.4)
\(\mathit{Live}\) the instructions ADCE marks useful (Algorithm 17.3.5)
\(\mathrm{VN}(v)\) the value number of \(v\)
\(v \equiv w\) \(v\) and \(w\) are congruent (Definition 17.5.1)
\(P = \{C_1, \dots, C_k\}\) a partition of the SSA values into classes
\(\mathrm{ANTIN}\), \(\mathrm{ANTOUT}\), \(\mathrm{AVIN}\), \(\mathrm{AVOUT}\) anticipability and availability per block (Lesson 17.6)
\(\mathrm{EARLIEST}(i, j)\), \(\mathrm{LATER}(i, j)\), \(\mathrm{LATERIN}(j)\) lazy code motion's edge and entry sets (Definition 17.6.5)
\(\mathrm{INSERT}(i, j)\), \(\mathrm{DELETE}(j)\) where LCM inserts and deletes computations
\(\mathrm{DEEXPR}\), \(\mathrm{UEEXPR}\), \(\mathrm{EXPRKILL}\) downward exposed, upward exposed and killed expressions of a block (EaC3's names)
\(\mathrm{lfp}\, G\) least fixed point of a monotone \(G\)
\(\mathrm{find}(c)\), \(\mathrm{terms}(c)\) union-find root of an e-class; the terms it represents (Definition 17.8.2)
\(\ell \to r\), \(\sigma\) a rewrite rule; a substitution from pattern variables to e-classes (Definition 17.8.3)

Numbered statements are N.k.m (chapter, lesson, counter), as in NOTATION.md §9.

Technique map

Family Techniques (origin) Lesson
Constant propagation Kildall's dense CP (Kildall 1973), sparse simple CP on SSA (Reif & Lewis 1977; Wegman & Zadeck 1991), dense conditional CP (Wegbreit 1975), SCCP (Wegman & Zadeck 1991), interprocedural SCCP (preview of Ch 20) 17.1
Range and bit analyses LazyValueInfo and CorrelatedValuePropagation (LLVM), ConstantRange (wrapped intervals), known bits (ValueTracking), demanded bits and BDCE (LLVM), GCC's Ranger/VRP 17.2
Dead-code elimination Mark–sweep DCE, aggressive DCE with control dependence (Cytron, Ferrante, Rosen, Wegman & Zadeck 1991), dead-store elimination (MemorySSA-based DSE in LLVM) 17.3
Dominator-scoped redundancy EarlyCSE (LLVM), dominator-based value numbering DVNT (Briggs, Cooper & Simpson 1997), LLVM's GVN with equality propagation 17.4
Partition and optimistic GVN Congruence partitioning (Alpern, Wegman & Zadeck 1988) with Hopcroft splitting, optimistic RPO/SCC value numbering (Simpson 1996; Cooper & Simpson), NewGVN (Gargi 2002; LLVM), predicated VN, GVN-hoist and GVN-sink 17.5
Partial-redundancy elimination Morel–Renvoise (1979), lazy code motion (Knoop, Rüthing & Steffen 1992; Drechsler & Stadel 1993 edge form), SSAPRE (Chow, Chan, Kennedy, Liu, Lo & Tu 1997; Kennedy et al. 1999), GVN-PRE (VanDrunen & Hosking 2004), LLVM load PRE 17.6
CFG simplification SimplifyCFG and Cooper–Torczon's Clean, switch-to-lookup-table, jump threading (Mueller & Whalley 1995; LLVM with LVI; GCC's backward threader) 17.7
Phase ordering and equality saturation Fixed pipelines, combined analyses (Click & Cooper 1995), equality saturation (Tate, Stepp, Tatlock & Lerner 2009) with rebuilding (egg, Willsey et al. 2021), acyclic e-graphs (Cranelift) 17.8
flowchart LR
  K[Kildall CP 1973] -->|SSA def-use chains| SSC[Sparse simple CP]
  W75[Wegbreit 1975<br/>conditional CP] -->|sparse| SCCP[SCCP 1991]
  SSC -->|executable edges| SCCP
  SCCP -->|ranges per edge| LVI[LVI / CVP, Ranger]
  SCCP -->|bit lattice| KB[Known / demanded bits]
  MS[Mark-sweep DCE] -->|control dependence| ADCE[ADCE 1991]
  LVN[Local VN, Ch 13] -->|dominator scopes| DVNT[DVNT / EarlyCSE / GVN]
  DVNT -->|optimistic partition| AWZ[AWZ 1988]
  AWZ -->|hashing + folding| RPO[RPO / SCC VN]
  RPO -->|constants + reachability| NGVN[NewGVN]
  MR[Morel-Renvoise 1979] -->|edge placement, optimal| LCM[Lazy code motion 1992]
  LCM -->|on SSA| SSAPRE[SSAPRE 1997]
  LCM -->|value numbers| GVNPRE[GVN-PRE 2004]
  SCCP -.->|leaves constant branches| SCFG[SimplifyCFG]
  LVI -->|edge facts| JT[Jump threading]
  SCCP -->|combined with VN| CC[Click-Cooper 1995]
  AWZ --> CC
  CC -->|non-destructive rewriting| EQS[Equality saturation 2009]
  EQS -->|rebuilding| EGG[egg 2021]
  EQS -->|acyclic, eager| AEG[Cranelift aegraph]

Who uses what

System Technique Notes
LLVM 23 SCCP and IPSCCP (ValueLatticeElement with ranges), LVI + CVP, known bits / demanded bits + BDCE, ADCE, MemorySSA DSE, EarlyCSE, GVN (DVNT-style + load PRE), NewGVN (optional), GVN-hoist/sink (optional), SimplifyCFG (8× at -O2), jump threading lessons 17.1–17.7; llvm/lib/Transforms/Scalar/ and llvm/lib/Transforms/Utils/SimplifyCFG.cpp
GCC 15 CCP (5× in the pipeline), Ranger-based VRP and threading, dce/cddce (control-dependence DCE), dse, FRE and PRE on RPO value numbering (tree-ssa-sccvn.cc, tree-ssa-pre.cc), RTL lazy code motion (lcm.cc, gcse.cc) 17.1, 17.3, 17.5, 17.6
Open64 SSAPRE (osprey/be/opt/opt_ssa*, opt_eant.cxx, opt_efinalize.cxx) 17.6
Cranelift (Wasmtime 37) aegraph mid-end: eager ISLE rewriting, GVN by hashconsing, LICM and rematerialization during elaboration 17.8
egg 0.11 equality saturation with rebuilding, e-class analyses, extraction 17.8
MLIR -sccp (sparse dataflow framework), -cse (dominator-scoped), canonicalization patterns 17.1
Pebble pebble-sccp, pebble-adce, pebble-gvn (DVNT), pebble-simplifycfg exercises

Comparison

The fixed columns follow docs/authoring/DEPTH_CONTRACT.md §3 item 8; every lesson repeats its own rows. \(n\) blocks, \(e\) edges, \(I\) instructions, \(U\) uses, \(E\) def-use edges (Lesson 17.5), \(N\) values, \(h\) the lattice height.

Constant propagation (lesson 17.1)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Kildall dense CP Constants valid on all paths, every edge assumed executable; equals SSC on SSA \(O(\lvert\mathit{Vars}\rvert^2 n e)\) · slow on large functions A map per program point; works without SSA Low (one generic solver) Non-SSA IRs (GCC RTL cprop), teaching
Sparse simple CP (SSC) Same constants as Kildall (Theorem 17.1.11); optimistic at phis \(O(U + I)\) · linear One lattice value per SSA value Low Folding along def-use chains; the base of SCCP
SCCP Strictly more than CP + DCE iterated (Theorem 17.1.12); proves blocks unreachable \(O(U + e + I)\) · linear, ~2–3 visits per instruction Constants plus executable edges; drives branch folding Medium (two worklists, edge bookkeeping) LLVM sccp/ipsccp, GCC CCP, MLIR -sccp

Range and bit analyses (lesson 17.2)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
LazyValueInfo + CVP Per-block, per-edge ranges (branch facts); pessimistic on cycles demand-driven, cached per (value, block); capped at 500 steps Ranges on request; rewrites divisions, flags, compares High (cache invalidation, edge constraints) CVP, jump threading (LLVM); Ranger/VRP (GCC)
ConstantRange arithmetic Wrapped intervals; sound transformers; one interval per value (union loses) \(O(1)\) per operation Ranges printed as range(...), !range Medium (every opcode, signed/unsigned views) SCCP's lattice, LVI, InstCombine, attributes
Known bits Per-bit facts; incomparable with ranges; best transformer for add query \(O(2^d)\) with depth cap \(d\) Zero/one masks Low per rule, many rules InstCombine, alignment, SelectionDAG
Demanded bits + BDCE Backward; removes computations of undemanded bits \(O(w \cdot U)\) Masks per value Low BDCE, InstCombine's SimplifyDemandedBits, vectorizer width

Dead-code elimination (lesson 17.3)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Mark–sweep DCE Removes dead cycles; keeps every branch \(O(I + U)\) · linear CFG unchanged Low Cheap cleanup; LLVM adce without control flow, GCC dce
Aggressive DCE Also removes dead branches and dead loops (Theorem 17.3.11) \(O(I + U + \lvert\mathrm{CD}\rvert)\) · near-linear with IDF Rewrites branches to post-dominators Medium (post-dominators, control dependence, termination policy) LLVM adce, GCC cddce, pebble-adce
Dead-store elimination Removes overwritten / never-read stores; enables ADCE \(O(I \cdot s)\) with MemorySSA scan limits Deletes memory operations High (alias analysis, partial overwrites) LLVM dse, GCC dse

Dominator-scoped redundancy (lesson 17.4)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
EarlyCSE Dominated redundancies of pure ops, loads (generations or MemorySSA), calls; simplification \(O(I + U)\) · the cheapest global CSE Replaces with dominating values Low–medium Cleanup between LLVM passes (2× in default<O2>)
Dominator-based VN (DVNT) Dominated redundancies + meaningless/redundant phis; pessimistic at back edges; misses siblings \(O(I + U)\) · one walk Value numbers + replacements Low pebble-gvn; GCC dom; teaching
LLVM GVN DVNT-like numbering + equality propagation + load elimination + PRE \(O(I + U)\) per iteration + memdep · the most expensive of the three Replacements, PRE insertions High LLVM gvn at -O2

Partition and optimistic GVN (lesson 17.5)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
AWZ partitioning Maximal congruence (Theorem 17.5.10); ⊇ hash-based (17.5.12); no algebra, no constants \(O(E \log N)\) Hopcroft; rounds × \(O(E)\) with signatures Classes, not replacements (needs a leader choice) Medium (partition refinement) Theory, the lab; basis of optimistic VN
Optimistic RPO / SCC VN Same partition, plus folding and identities during hashing 2–3 linear iterations · fast Value numbers usable directly Medium GCC FRE/PRE (do_rpo_vn)
NewGVN (Gargi) Optimistic congruence + constants + unreachable code + predicates + memory sparse, touched instructions · slower than GVN Replacements; unreachable blocks marked High LLVM newgvn (not default)
GVN-hoist / GVN-sink Moves equal computations to a common dominator / successor; code size per value number · cheap with limits Fewer copies, same number of evaluations Medium LLVM gvn-hoist, gvn-sink; GCC code hoisting

Partial-redundancy elimination (lesson 17.6)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Morel–Renvoise Correct and safe; misses critical-edge cases; not optimal (Proposition 17.6.15) bidirectional bit vectors · slow to converge Block-end insertions Medium Historical; the problem statement
Lazy code motion Computationally and lifetime optimal among safe placements (Theorems 17.6.13–17.6.14); lexical 4 rapid bit-vector problems · fast Edge insertions, deletions, temporaries Medium GCC RTL PRE (lcm.cc), textbooks, the ★ lab
SSAPRE Same optimality per expression, on SSA; sparse per expression, near-linear Φ-based placement, SSA temporaries High (six steps) SGI/Open64; the SSA book
GVN-PRE Value-based: finds redundancies with different names, via phi-translation set operations per block over value numbers Insertions of new expressions (e.g. x_6 * 2) High GCC tree PRE
LLVM load PRE Loads only; single missing predecessor; safety checks per load, memdep-bound Hoisted loads + phis Medium (inside GVN) LLVM gvn

CFG simplification (lesson 17.7)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
CFG simplification Canonical CFG: no constant branches, no unreachable blocks, no mergeable chains rounds × linear · cheap, run often Fewer blocks and edges; selects Low for the core rules, high for LLVM's full set After every CFG-changing pass (LLVM 8× at -O2)
Switch-to-lookup-table Dense constant switches only \(O(k \log k + \mathit{size})\) A constant table + range check Medium (profitability, holes, types) Late SimplifyCFG in LLVM
Jump threading Removes branches whose outcome is known per edge (with LVI: ranges, predicates) LVI queries + bounded duplication Duplicated blocks Medium–high (SSA repair, loop headers) LLVM jump-threading 2× at -O2; GCC threaders

Phase ordering and equality saturation (lesson 17.8)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Phase ordering (fixed pipeline) Limited by the order: misses facts that need two analyses at once (Proposition 17.8.7) sum of the passes · predictable Each pass's output is inspectable (-print-changed) Low per pass, high to tune the pipeline LLVM default<O2>, GCC passes.def
Combined analyses At least every phase ordering of the combined analyses (Theorem 17.8.6), sometimes strictly more \(O(I \cdot h)\) · comparable to one GVN One fixed point; hard to debug when it does not converge High (one transfer function that knows every fact) LLVM NewGVN, GCC RPO VN / FRE
Equality saturation Every rewrite order at once, if saturated (Theorem 17.8.9) unbounded; needs limits · slow on AC rules Extraction chooses by a cost model; equivalences can be explained (egg's proofs) Medium (e-graph, e-matching, extraction); rules are declarative Research, rewrite-heavy domains (tensor graphs, floating-point accuracy with Herbie), superoptimizers
Acyclic e-graphs (aegraph) Eager single application of each rule; no saturation guarantee near one pass · production JIT speed Rules in ISLE, verifiable one by one Medium–high (elaboration, bounds) Cranelift's mid-end in Wasmtime

Comparison-lab results (reproduce with build/<preset>/bin/ch17-compare --table labs/ch17-scalar/corpus/*.ll on a solution build): on the 12 corpus functions (48 blocks, 114 candidate instructions), simple CP finds 7 constants and SCCP 12, and SCCP proves 5 blocks unreachable. Hash-based (dominator-scoped) value numbering finds 15 redundant instructions and the AWZ partition finds 26. The running example run alone gives 0 vs 3 constants and 0 vs 3 congruences. The ★ LCM lab reduces the running TAC program from 24 to 17 dynamic evaluations (labs/ch17-lcm/inputs/running.tac).

Route through this chapter

Step What Techniques How it is exercised
1 Lesson 17.1 Kildall CP, sparse CP, SCCP drill sccp-trace; quiz; flashcards
2 Lesson 17.2 LVI, CVP, ConstantRange, known bits, demanded bits quiz; flashcards
3 Lesson 17.3 mark–sweep DCE, ADCE, DSE drill adce-marking; quiz
4 Lesson 17.4 EarlyCSE, DVNT, LLVM GVN quiz; flashcards
5 Lesson 17.5 AWZ, optimistic RPO VN, NewGVN, GVN-hoist/sink drill vn-partition; quiz
6 Lesson 17.6 Morel–Renvoise, LCM, SSAPRE, GVN-PRE, load PRE drill lcm-sets; quiz
7 Lesson 17.7 SimplifyCFG, switch tables, jump threading quiz; flashcards
8 Lesson 17.8 phase ordering, combined analyses, equality saturation, aegraphs quiz; flashcards
9 Exercises E1–E4 Pebble implements SCCP, ADCE, DVNT and SimplifyCFG-lite ./course test 17
10 Comparison lab labs/ch17-scalar/ simple CP vs SCCP; hash VN vs AWZ partition lab tests + ch17-compare --table
11 ★ labs labs/ch17-lcm/, labs/ch17-egraph/ lazy code motion on TAC; equality saturation ch17.lab tests
12 Theory test all ./course quiz 17 (≥ 80 % to finish)

Practice and check

./course drill sccp-trace --difficulty easy      # warm up; --solution shows every step
./course drill vn-partition                      # AWZ rounds and the final partition
./course drill lcm-sets                          # the LCM sets of a small CFG
./course drill adce-marking                      # which instructions and branches ADCE keeps
./course flash 17                                # daily, a few minutes
./course quiz 17                                 # after the lessons
./course test 17                                 # after the exercises and the lab
./course status                                  # done = quiz ≥ 80 % and tests pass

References

The chapter's annotated bibliography (papers, textbook sections, pinned source files and documentation) is in references.md. Start with: [WZ91] (SCCP, and the dense/sparse × conditional/unconditional map of Lesson 17.1), [AWZ88] (congruence partitioning), [KRS94] (lazy code motion, with its optimality proofs), [CC95] (why analyses should be combined), and [WNW+21] (egg and equality saturation). [SSAB] and [EaC3] are the textbook companions.