On this page

Data Flow Analysis

The prerequisite analysis layer for compiler optimization: iteratively propagating data flow facts on the control flow graph until convergence. The same gen/kill/meet/worklist framework, with a different direction and meet operation, solves three different problems: liveness, reaching definitions, and available expressions.

Overview

A compiler needs to know "which variables are currently in use, which definitions can reach here, and which expressions have already been computed" at every point in the program. This information cannot be derived from a single instruction; it must be propagated across the entire control flow. Data flow analysis is the process of iteratively propagating data flow facts on a program's Control Flow Graph (CFG) until all points converge.

Abstracting this into a unified framework (Kildall, 1973) allows the same iterative algorithm to solve multiple classes of analysis problems by simply changing the "direction," "meet operation," and "transfer functions." This article covers this unified framework and then expands on three core instances: reaching definitions, liveness, and available expressions. These are also direct prerequisites for Classic Optimizations.

Unified Framework

The four components of data flow analysis:

ComponentMeaningExample (reaching defs)
Directionforward (from entry to exit) or backward (from exit to entry)forward
Meet OperationHow to merge facts when paths converge: ∪ (may) or ∩ (must)∪ (may)
Transfer FunctionHow an instruction changes the incoming fact to produce the outgoing factout = gen ∪ (in - kill)
Initial ValueStarting estimate for each node (usually top or bottom)in[entry]=∅, out[B]=∅
  • May Analysis (∪): Asks "what could it be?" — Over-approximation; better to over-report than under-report. Reaching definitions is a may analysis: a definition "may" reach a certain point.
  • Must Analysis (∩): Asks "what must it be?" — Under-approximation; better to under-report than misreport. Available expressions is a must analysis: an expression "must" have been computed.

The transfer function is generally written as: out[B] = gen[B] ∪ (in[B] - kill[B]) (or using ∩ instead of ∪, depending on the meet). gen represents new facts "generated" by the block, and kill represents old facts "invalidated" by the block.

Iterative Solution: Worklist Algorithm

Repeatedly scan the entire CFG until stability (fixed point) is reached:

for each block B: out[B] = ∅ (or top)
worklist = all blocks
while worklist not empty:
    B = worklist.pop()
    in[B] = meet(out[predecessors of B])
    old_out = out[B]
    out[B] = gen[B] ∪ (in[B] - kill[B])     ← Transfer function
    if out[B] changed:
        worklist.push(successors of B)        ← Propagate only changes

Termination is guaranteed by two conditions: the lattice height is finite (the set of facts has a maximum element) and the transfer functions are monotonic (new in → new out will not be less than the old one) — the Fixed Point Theorem. For engineering CFGs, convergence is usually fast (typically 3–5 iterations).

The ordering of the worklist affects convergence speed — processing nodes whose predecessors have already been updated (approximated by CFG topological order) is more efficient than random processing.

Three Classic Analyses

Reaching Definitions (Forward, May, ∪)

Problem: At program point p, which assignments (definitions) may reach p without being overwritten by subsequent assignments?

gen[B]:  The statement in this block that last assigns to variable x (earlier ones are overwritten by later ones)
kill[B]: All assignments to x elsewhere that are "killed" by the new assignment in this block

This forms the basis for constructing use-def chains. After SSA Form is introduced, reaching definitions on SSA IR become sparse and no longer require iterative solving; however, it remains essential on conventional IR.

Liveness (Backward, May, ∪)

Problem: At program point p, will the value of variable x be used in the future (before its next definition)?

Direction changed to backward: propagate from exit to predecessors
gen[B]:  Variables that are "used" in this block but not "redefined" by this block before use
kill[B]: Variables that are "redefined" in this block but not "used" in this block before the new definition (new value overwrites old value)

Liveness analysis directly determines register allocation: two variables can share the same register if they are not simultaneously active (see Register Allocation). This is the most frequently performed analysis in compiler optimization.

Available Expressions (Forward, Must, ∩)

Problem: At program point p, has the expression x + y definitely been computed, and have neither x nor y been reassigned?

gen[B]:  Expressions computed in this block (and x/y not redefined afterwards)
kill[B]: All expressions containing x or y, invalidated after an assignment to x or y in this block
meet:   ∩ (must) — Available expressions at entry = Intersection of out points of all predecessors

Available expressions information is directly used for CSE (Common Subexpression Elimination) — if x + y is available at the current point, the previous result is used directly without recomputation.

MOP vs MFP: The Gap Between Ideal and Fixed-Point Solutions

  • MOP (Meet Over all Paths): For program point p, collect "facts carried when reaching p via all possible paths," then perform a meet on these facts. This is the ideal solution.
  • MFP (Maximal Fixed Point): The solution obtained by the iterative fixed-point algorithm. MFP ≤ MOP (in terms of lattice order, MFP may be more conservative).

For distributive transfer functions — i.e., functions where f(a meet b) == f(a) meet f(b) — MFP = MOP, and the fixed point is the ideal solution. The transfer functions for reaching definitions and liveness are distributive; the transfer function for constant propagation is not distributive, so MFP is more conservative than MOP.

Sparse Data Flow under SSA

The entire iterative fixed-point framework is significantly simplified under SSA Form:

  • Each variable is defined only once → Reaching definition degenerates to "where is the definition for this use?" (direct def-use chain, no analysis needed).
  • Liveness can be performed on the def-use graph — the live range of each definition consists of all use points it can reach, eliminating the need for iteration across the entire CFG.
  • Many "propagate facts across the entire CFG" passes degenerate into "single-pass propagation on the dominator tree" under SSA.

This is why SSA is called "sparse analysis" — data flow facts are attached only to def and use points, rather than every edge in the CFG.

References

  • Kildall (1973): "A Unified Approach to Global Program Optimization" — The unified framework for data flow analysis
  • Dragon Book: Chapter 9, Machine-Independent Optimizations — Contains complete gen/kill tables and fixed-point algorithms
  • Cooper/Torczon: "Engineering a Compiler", Chapters 8–10

Keywords: data flow analysis, gen/kill, meet operator, forward/backward, may/must, reaching definitions, liveness, available expressions, fixed-point iteration, worklist algorithm, lattice, monotonicity, MOP, MFP, distributive function, sparse analysis, SSA