Skip to content

Chapter 15 · Control-Flow Analysis: Dominance & Loops

Part 3 · Analysis Foundations & SSA · about 2–3 weeks · Previous: Ch 14 · Next: Ch 16

The problem

You are given a control-flow graph (CFG) \(G = (N, E, r)\): the basic blocks of one function, an edge for every possible jump, and one entry block without predecessors. You want its shape, in the form every later optimization consumes. Which blocks must run before which (dominance: \(d \mathrel{\mathrm{dom}} n\) if every path from \(r\) to \(n\) contains \(d\))? Where do paths from different definitions meet (dominance frontiers, and iterated frontiers for a set of blocks)? Which blocks run on every path to the exit (post-dominance), and which branches decide whether a block runs (control dependence)? Where are the loops, how do they nest, and are they well behaved (natural loops, loop nesting forests, reducibility)? Finally, how do you put loops into a canonical shape that loop optimizations can rely on (preheaders, single latches, dedicated exits, loop-closed SSA)? The outputs are a dominator tree with O(1) queries, a post-dominator tree with a virtual exit, frontier sets, control-dependence sets, and a loop forest. In LLVM they are the analyses DominatorTree, PostDominatorTree, DominanceFrontier/IDFCalculator, LoopInfo and CycleInfo, recomputed or incrementally updated between passes. In pebblec they run after lowering to LLVM IR and before SSA construction (Ch 16), loop optimizations (Ch 18) and dead-code elimination (Ch 17).

What you will be able to do

  • Trace iterative dominators, Cooper–Harvey–Kennedy (every intersect finger walk) and Lengauer–Tarjan (every EVAL, LINK and bucket) by hand on a 10-node CFG, and get the same dominator tree three ways.
  • Prove the dominator tree theorem and explain why CHK needs reverse postorder, why a semidominator is not always the immediate dominator, and why the post-dominator tree needs a virtual exit.
  • Compute dominance frontiers bottom-up (Cytron) and with runners (CHK), the iterated frontier of a set of blocks (worklist and DJ graph), and control dependences from the post-dominator tree.
  • Find back edges, natural loops and their nesting; decide reducibility with T1/T2; build Havlak's forest for an irreducible CFG and say what LLVM's LoopInfo and CycleInfo each report.
  • Implement all of the above behind the nine-function contract pebble/include/pebble/Analysis/Dominance.h (your code in pebble/lib/Analysis/Dominance/src/, the shared analysis library later chapters reuse), check it against LLVM on thousands of random CFGs, and run it inside opt as print<pebble-domtree> and friends.
  • Choose between CHK, Lengauer–Tarjan and Semi-NCA for a given compiler with measured numbers, and explain the quadratic worst cases of CHK and Semi-NCA.
  • Find where LLVM 23 builds dominator trees, updates them incrementally, places phis with IDFCalculator, discovers loops, and enforces loop-simplify form and LCSSA.

Prerequisites: Ch 8 (CFGs, basic blocks, DFS preorder/postorder and reverse postorder), Ch 12 (new-pass-manager analyses, plugins, lit + FileCheck) and Ch 14 (monotone frameworks, round-robin iteration in RPO, lattice height).

Notation

Shared notation follows the house notation: §1 (sets, functions, logic), §3 (graphs and CFGs) and §4 (dominance and loops). Orientation: dominator sets form a lattice ordered by \(\subseteq\) in which the answer is the greatest fixed point: the iteration starts at \(\top = N_r\) and sets only shrink (the dual of the may-analyses of Ch 14). In this chapter:

Symbol Meaning
\(G = (N, E, r)\) a flowgraph: nodes (basic blocks), edges, entry \(r\) without predecessors (Definition 15.1.1)
\(n = \lvert N \rvert\), \(m = \lvert E \rvert\) numbers of nodes and edges (lessons use \(m\) for edges; NOTATION.md's \(e\) is the same)
\(N_r\) the nodes reachable from \(r\)
\(\mathrm{preds}(n)\), \(\mathrm{succs}(n)\) predecessors, successors, in listed order
\(u \to v\), \(u \leadsto v\) an edge; a path (possibly empty)
\(d \mathrel{\mathrm{dom}} n\), \(d \mathrel{\mathrm{sdom}} n\) dominance, strict dominance (Definition 15.1.2)
\(\mathrm{Dom}(n)\) the set of dominators of \(n\)
\(\mathrm{idom}(n)\) immediate dominator (Definition 15.1.5)
\(\mathcal{D}\), \(\mathrm{level}(x)\), \(\mathrm{depth}(x)\) the dominator tree and the depth of \(x\) in it (Theorem 15.1.6)
\(\mathrm{NCA}(a, b)\) nearest common ancestor in a tree (in \(\mathcal{D}\) unless stated)
\(\mathrm{in}(x)\), \(\mathrm{out}(x)\) DFS entry/exit numbers of the dominator tree (Corollary 15.1.7)
\(T\), \(\mathrm{parent}(w)\), \(u \preceq_T v\), \(u \prec_T v\) DFS spanning tree, tree parent, ancestor-or-self, proper ancestor (Definition 15.1.9)
\(\mathrm{pre}(v)\), \(\mathrm{post}(v)\), \(\mathrm{rpo}(v)\) DFS preorder, postorder and reverse-postorder numbers
\(\mathrm{sdom}(w)\) the semidominator function of Lengauer–Tarjan (Definition 15.1.13), not the relation \(\mathrel{\mathrm{sdom}}\)
\(d(G)\) loop connectedness: the most retreating edges on any cycle-free path
\(h\) height of the dominator (or post-dominator) tree
\(\alpha(m, n)\) inverse Ackermann function
\(\mathrm{DF}(X)\), \(\mathrm{DF}(S)\), \(\mathrm{DF}^{+}(S)\) dominance frontier, of a set, iterated (Definitions 15.3.1–15.3.2)
\(\mathrm{DF}_{\mathrm{local}}\), \(\mathrm{DF}_{\mathrm{up}}\) Cytron's local and up frontiers (Definition 15.3.3)
\(J(S)\), \(J^{+}(S)\) join set and iterated join set (Definition 15.3.9)
\(\hat{x}\), \(R\), \(G^{R}_{\hat{x}}\) virtual exit, post-dominator roots, augmented reverse graph (Definition 15.4.1)
\(p \mathrel{\mathrm{pdom}} n\), \(\mathrm{ipdom}(n)\) post-dominance, immediate post-dominator (Definition 15.4.2)
\(\mathrm{CD}(Y)\), \(\mathrm{PDF}(Y)\) control dependences of \(Y\), post-dominance frontier (Definitions 15.4.4–15.4.5)
\(\mathrm{body}(h)\), \(\mathrm{loop}(t \to h)\) natural loop of a header, of one back edge (Definition 15.5.4)
T1, T2 Hecht–Ullman reduction rules (Definition 15.6.1)
\(I(h)\), \(I(G)\) interval with header \(h\), derived graph (Definition 15.6.5)

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

Technique map

Family Techniques (origin) Lesson
Dominator algorithms Iterative dataflow with bit vectors (Prosser 1959; Lowry & Medlock 1969; Allen & Cocke 1972), Cooper–Harvey–Kennedy (2001), Lengauer–Tarjan with simple and sophisticated linking (1979), Semi-NCA (Georgiadis 2005) 15.1
Incremental dominators Edge insertion by depth-based search (Georgiadis, Italiano, Laura & Santaroni 2016, after Ramalingam & Reps 1994), edge deletion by subtree rebuild (Georgiadis et al. 2016), batched updates with DomTreeUpdater (LLVM, 2017–2018) 15.2
Dominance frontiers Cytron's bottom-up DF (Cytron, Ferrante, Rosen, Wegman & Zadeck 1991), CHK's per-join runners (Cooper, Harvey & Kennedy 2001), iterated DF by worklist (Cytron et al. 1991), DJ graphs and linear-time DF⁺ (Sreedhar & Gao 1995) 15.3
Post-dominance and control dependence Post-dominator trees on the reverse CFG with a virtual exit (Lowry & Medlock 1969; LLVM root selection), control dependence (Ferrante, Ottenstein & Warren 1987; Cytron et al. 1991) 15.4
Loop nesting forests DFS edge classification (Tarjan 1972), natural loops and LoopInfo (Lowry & Medlock 1969; Allen 1970), Tarjan's loop nesting (Tarjan 1974), Havlak's forest for irreducible loops (Havlak 1997; Ramalingam 1999), Steensgaard's forest (Steensgaard 1993) 15.5
Reducibility T1/T2 reduction (Hecht & Ullman 1972), interval derived sequences (Allen 1970; Allen & Cocke 1976), DFS back-edge characterization (Hecht & Ullman 1974; Tarjan 1974), node splitting (Cocke & Miller 1969; Janssen & Corporaal 1997) 15.6
Canonical loop forms Loop-simplify form: preheader, single latch, dedicated exits (Lowry & Medlock 1969; LLVM LoopSimplify), loop-closed SSA (GCC tree-SSA loop optimizer; LLVM LCSSA) 15.7
flowchart LR
  IT[Iterative Dom sets<br/>Prosser 1959, Lowry-Medlock 1969] -->|same fixed point, sets as tree paths| CHK[Cooper-Harvey-Kennedy 2001]
  IT -->|semidominators, near-linear| LT[Lengauer-Tarjan 1979<br/>simple / sophisticated LINK]
  LT -->|keep semidominators, NCA instead of buckets| SNCA[Semi-NCA<br/>Georgiadis 2005]
  SNCA -->|local rebuilds| INC[Incremental: DBS insert,<br/>subtree delete 2016]
  RR[Ramalingam-Reps 1994] -->|insertion lemma| INC
  CHK -->|runners| CHKDF[CHK frontiers]
  DT[Dominator tree] --> CYDF[Cytron DF 1991] -->|worklist| IDF[Iterated DF]
  DT --> DJ[DJ graph<br/>Sreedhar-Gao 1995] -->|linear DF+| IDF
  DT -->|reverse CFG + virtual exit| PDT[Post-dominators] -->|reverse DF| CD[Control dependence<br/>FOW 1987]
  DT -->|head dominates tail| NL[Natural loops]
  DFS[DFS edge kinds<br/>Tarjan 1972] --> TL[Tarjan loop nesting 1974] -->|irreducible entries| HV[Havlak 1997<br/>Ramalingam 1999]
  DFS --> RED[Reducibility tests]
  T12[T1/T2 1972] --- RED
  INT[Intervals 1970] --- RED
  RED -->|if irreducible| NS[Node splitting]
  SCC[SCCs] --> ST[Steensgaard 1993]
  NL --> LS[Loop-simplify form] --> LCSSA[LCSSA]

Who uses what

System Technique Notes
LLVM 23 Semi-NCA (full build), DBS insertion and subtree-rebuild deletion (updates), DomTreeUpdater batching; Cytron DF; Sreedhar–Gao DF⁺ (IDFCalculator); natural loops (LoopInfo) plus a Havlak-style cycle forest (CycleInfo); LoopSimplify, LCSSA llvm/include/llvm/Support/GenericDomTreeConstruction.h; lessons 15.1–15.7
GCC 15 Lengauer–Tarjan with path compression and balancing (dominance.cc); CHK to fix up small block sets after CFG changes; CHK runners for DF; worklist DF⁺; natural loops (flow_loops_find), SCC-based irreducible-region marking; loop-closed SSA 15.1, 15.3, 15.5
Go 1.25 (cmd/compile) Lengauer–Tarjan, simple LINK/EVAL (dominatorsLTOrig); CHK kept as dominatorsSimple; Sreedhar–Gao phi placement for functions with ≥ 500 blocks (ssagen/phi.go); dominator-based loop nest with irreducible detection (loopnestfor) 15.1, 15.3, 15.5
rustc 1.90 Semi-NCA following Georgiadis's thesis (rustc_data_structures::graph::dominators) 15.1
Cranelift (Wasmtime 36) Semi-NCA dominator tree (CHK kept as the verification baseline); natural loops from back edges (LoopAnalysis) 15.1, 15.5
HotSpot C2 (JDK 25) Lengauer–Tarjan with the sophisticated LINK (domgraph.cpp); loop tree with explicit irreducible-loop flags (loopnode.cpp) 15.1, 15.5
WebKit (B3, DFG) Semi-NCA below 20 000 blocks, Lengauer–Tarjan above; naive iterative dominators as a self-check (WTF/Dominators.h) 15.1
V8 TurboFan Loop-aware "special RPO" that keeps loops contiguous; dominators by common-dominator walks (scheduler.cc) 15.5
Swift 6, MLIR Reuse LLVM's DominatorTreeBase templates for SIL and for MLIR regions 15.1

Comparison

The fixed columns follow docs/authoring/DEPTH_CONTRACT.md §3 item 8; every lesson repeats its own rows. n = nodes, m = edges, h = dominator-tree height, d = loop connectedness (the most back edges on any acyclic path), α = inverse Ackermann.

Dominator algorithms (lesson 15.1)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use Irreducible CFGs
Iterative bit vectors exact Dom sets O((d+2)·m·n/w) with w-bit words · slow above a few thousand nodes (n² bits) full Dom sets, easy to inspect ~30 lines teaching, verification oracles (WebKit NaiveDominators) yes, more passes
Cooper–Harvey–Kennedy exact idoms O((d+2)·m·h) · fast on small CFGs; lab: 1.9 ms at 10 000 blocks, 609 ms on a 20 000-block comb idoms only; pass count shows irreducibility ~40 lines small graphs: GCC's incremental fix-up, Cranelift's baseline, Pebble's analysis yes, more passes
Lengauer–Tarjan (simple / sophisticated) exact idoms and semidominators O(m log n) / O(m α(m,n)) · robust; lab: 1.05 ms at 10 000, 0.8 ms on the 20 000-block ladder idoms; sdom as a by-product ~120 lines, subtle GCC, Go, HotSpot, WebKit (large graphs) yes, no extra cost
Semi-NCA exact idoms O(n²) worst, near-linear in practice · lab: fastest on random CFGs (0.85 ms at 10 000), 94 ms on the ladder idoms; a partial tree usable for updates ~70 lines LLVM, rustc, Cranelift, WebKit yes

Incremental dominators (lesson 15.2)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
DBS edge insertion exact; only affected nodes change O(m) worst per insertion, usually a few nodes the affected set, all re-parented to one NCA ~60 lines LLVM InsertReachable
Subtree-rebuild deletion exact Semi-NCA on the subtree of NCA(u, v); whole tree if that is the root rebuilt subtree ~80 lines + support test LLVM DeleteReachable/DeleteUnreachable
Batched updates (DomTreeUpdater) exact once flushed lazy: one flush per batch; recompute when updates > n/40 (n > 100) tree valid only after flush() API discipline every CFG-changing LLVM transform

Dominance frontiers (lesson 15.3)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Cytron DF all DF sets O(n + Σ|DF|) but Θ(n²) output on nested repeat-until loops · fast explicit sets for every node ~30 lines LLVM DominanceFrontier, classic SSA
CHK runners all DF sets same bound; touches only join nodes · fastest in practice same sets ~15 lines GCC compute_dominance_frontiers
Iterated DF worklist DF⁺ of any set O(Σ|DF|) after DF · needs all DF sets phi blocks ~15 lines GCC compute_idf, Cytron SSA
DJ graph / Sreedhar–Gao DF⁺ without DF sets O(n + m) per query · no quadratic DF phi blocks, deterministic order ~50 lines LLVM IDFCalculator (mem2reg, ADCE), Go (≥ 500 blocks)

Post-dominance and control dependence (lesson 15.4)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Post-dominator tree (virtual exit) exact given the roots; infinite loops need a root choice same as the dominator algorithm used ipdom; roots tell where exits are small on top of a dominator algorithm LLVM PostDominatorTree, GCC CDI_POST_DOMINATORS
Control dependence (FOW / RDF) exact CD relation O(m · h) walk, or DF on the reverse CFG CD sets per node or per edge ~20 lines ADCE, program slicing, PDG, if-conversion

Loop nesting forests (lesson 15.5)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use Irreducible CFGs
DFS edge classification classifies every edge O(n + m) tree/back/forward/cross per edge; back edges depend on the DFS ~40 lines every loop and SCC algorithm back edges ≠ loops
Natural loops (LoopInfo) reducible loops only O(n + m) after dominators header, latches, body, nesting ~60 lines LLVM, GCC, Cranelift loop optimizers ignored
Tarjan loop nesting reducible graphs; stops on irreducible O(m α(m,n)) forest + reducibility verdict ~70 lines reducibility test, structural analysis detected, not handled
Havlak all cycles; header = first DFS entry O(m α(m,n)) with Ramalingam's fix forest with irreducible flags and entries ~90 lines LLVM CycleInfo, GPU divergence analysis yes
Steensgaard all cycles; all entries are headers O(n·m) worst (nested SCCs) forest with header sets ~50 lines with an SCC routine GCC irreducible marking (SCC-based) yes

Reducibility (lesson 15.6)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
T1/T2 exact verdict O(n·m) naive, near-linear with care limit graph shows the irreducible core ~40 lines textbooks, verification
Interval derived sequence exact verdict + intervals O(n·m) nested intervals (Allen–Cocke analysis) ~60 lines interval dataflow analysis (Ch 14)
DFS back-edge test exact verdict O(n + m) after dominators the offending retreating edges ~10 lines LLVM containsIrreducibleCFG
Node splitting makes any CFG reducible exponential code growth worst case an equivalent reducible CFG ~80 lines GPU back ends, structurizers; LLVM uses guard blocks instead (fix-irreducible)

Canonical loop forms (lesson 15.7)

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Loop-simplify form canonical shape for every natural loop O(n + m) per loop, plus DomTree/LoopInfo updates new preheader / latch / exit blocks LoopSimplify.cpp is ~900 lines before LICM, unrolling, vectorization
LCSSA every loop-defined value used outside flows through an exit phi O(uses · exits) .lcssa phis in exit blocks LCSSA.cpp is ~550 lines (plus SSAUpdater) before loop transforms, SCEV rewriting

Comparison-lab results (reproduce with build/<preset>/bin/ch15-dombench --reps 5; this machine, LLVM 23.1.2, milliseconds, DFS included): random CFGs with 100 000 blocks — CHK 56, Lengauer–Tarjan 15, balanced LT 15, Semi-NCA 14, LLVM 27; the 20 000-block ladder — CHK 159, LT 0.8, Semi-NCA 94, LLVM 80; the 20 000-block comb — CHK 609, LT 0.8, Semi-NCA 0.6, LLVM 1.2; the 1 000-block bichain — CHK needs 999 passes (4.3 ms) while LT takes 0.03 ms. Lesson 15.1, section 8, has the full table and discusses it.

Route through this chapter

Step What Techniques How it is exercised
1 Lesson 15.1 iterative, CHK, Lengauer–Tarjan, Semi-NCA drills dominators, rpo, lengauer-tarjan; exercises E1–E6 (E7: read the provided DomTree)
2 Lesson 15.2 DBS insertion, subtree deletion, DomTreeUpdater drill dom-update; DomTreeUpdates.* oracle tests
3 Lesson 15.3 Cytron DF, CHK DF, iterated DF, DJ graphs drills dominators (DF), idf; exercises E8–E10
4 Lesson 15.4 post-dominators, control dependence drill post-dominance; exercises E11–E12
5 Lesson 15.5 DFS edges, natural loops, Tarjan, Havlak, Steensgaard drills rpo, natural-loops; exercises E1, E13–E14
6 Lesson 15.6 T1/T2, intervals, DFS test, node splitting drill natural-loops --difficulty hard; exercise E15
7 Lesson 15.7 loop-simplify form, LCSSA drill loop-forms; lit tests with opt
8 Exercises (the spec of the contract pebble/include/pebble/Analysis/Dominance.h) Pebble uses CHK (full implementation) plus everything above ./course test 15
9 Comparison lab labs/ch15-dominance/SPEC.md CHK vs Lengauer–Tarjan (vs ★ balanced LT, ★ Semi-NCA) vs LLVM ch15.LabAgreement.*, ch15.lab.bench-smoke, ch15-dombench
10 Theory test all ./course quiz 15 (≥ 80 % to finish)

Practice and check

./course drill dominators --difficulty easy          # warm up; --solution shows every step
./course drill lengauer-tarjan --seed 12 --solution   # the EVAL/LINK table
./course drill natural-loops --difficulty hard        # irreducible CFGs appear here
./course flash 15                                     # daily, a few minutes
./course quiz 15                                      # after the lessons
./course test 15                                      # after the exercises
./course status                                       # done = quiz ≥ 80 % and tests pass

References

The chapter's annotated bibliography (papers, textbook sections, pinned source files for LLVM 23.1.2, GCC 15, Go, rustc, Cranelift, HotSpot, WebKit and V8, docs and talks) is in references.md. Start with: [LT79] and [CHK01] (the two dominator algorithms of the lab), [GTW06] (why LLVM chose Semi-NCA), [CFRWZ91] (frontiers, phi placement and control dependence), [Hav97] with [Ram02] (loop forests), [HU74] (reducibility), and for a gentler route [EaC3 §9.2] and [Dragon2 §9.6]. The LLVM files to keep open are [LLVM-GDTC], [LLVM-IDF], [LLVM-LoopInfo], [LLVM-Cycle] and [LLVM-LoopTerm].