By the end of this chapter you'll be able to…

  • 1Partition three-address code into basic blocks using the three leader rules and draw the flow graph
  • 2Apply the seven local optimisations in a productive order and explain why the sequence must be iterated
  • 3Construct a DAG for a basic block and read optimised code back from it
  • 4Specify any data-flow analysis by its direction, domain, meet operator and transfer function
  • 5Classify reaching definitions, available expressions, live variables and very busy expressions correctly
  • 6Explain why removal-justifying analyses need intersection and retention-justifying analyses need union
  • 7Identify natural loops from dominators and back edges
  • 8State the safety condition for loop-invariant code motion and give a case where it fails
  • 9Apply strength reduction and induction variable elimination to an indexing loop
💡
Why this chapter matters in GATE
An optimisation is legal only if it preserves observable behaviour on every input, and data-flow analysis is how that fact is proved rather than assumed. GATE tests whether you can classify an analysis by its direction and meet operator, identify basic blocks and natural loops, and reason about when a transformation is unsafe.

Before you start — revise these

🔗
Three-address code and the structure of intermediate representations
🔗
Basic graph terminology: paths, predecessors, successors
🔗
The notion of a fixed point and monotone iteration

Local Optimisation & Data Flow Analysis

An optimising compiler rewrites code to make it faster or smaller, and the constraint on every rewrite is the same.

A transformation is legal only if it preserves the program's observable behaviour on every input, not merely on the inputs the compiler happened to think about. That requirement is why optimisation needs analysis at all: the compiler must prove a fact about all executions before it may act.

Data-flow analysis is how that proof is computed. It answers questions of the form "what is guaranteed to be true at this point in the program", and each classical analysis answers one such question.

The second organising fact is that every data-flow analysis is defined by four choices: the direction it propagates, the facts it tracks, how it combines facts arriving from several predecessors, and what it does at each statement.

Once those four are fixed, the analysis writes itself, and the differences between reaching definitions, available expressions and live variables are entirely differences in those four choices.

The third is that the scope of an optimisation determines what it may assume. Within a basic block, control flows straight through and everything is known. Across blocks, control may arrive from several places and the compiler must consider all of them.

1. Basic Blocks and the Flow Graph

A basic block is a maximal sequence of instructions with one entry at the top and one exit at the bottom. No jump enters the middle and no jump leaves except at the end.

Blocks are found by identifying leaders, the first instruction of each block.

Three rules identify a leader. The first instruction of the program is one. Any target of a jump is one. Any instruction immediately following a jump is one.

Each block then runs from a leader up to but not including the next leader.

The flow graph has one node per basic block and an edge wherever control can pass from the end of one block to the start of another. Adding an entry and an exit node makes the graph self-contained.

Within a block, analysis is exact. There are no branches, so every instruction executes in order exactly once, and a fact established at one point holds at every later point until something changes it. That is what makes local optimisation cheap.

2. Local Optimisations

Seven transformations apply within a basic block, and each is examined by name.

Constant folding evaluates an expression whose operands are known constants at compile time. Replacing with removes a run-time multiplication.

Constant propagation replaces a variable with its value when that value is a known constant. It typically enables further folding, and the two are applied alternately until nothing changes.

Common subexpression elimination replaces a recomputation with a reference to the earlier result. It is legal only if no operand has changed in between, which the block's straight-line structure makes easy to check.

Copy propagation replaces a variable with the one it was copied from, so that after later uses of become uses of . It rarely helps directly but exposes dead code.

Dead code elimination removes computations whose results are never used. It is usually the last step, because the other transformations create dead code as a side effect.

Strength reduction replaces an expensive operation with a cheaper equivalent. Multiplication by 2 becomes a shift, and a multiplication inside a loop becomes an addition.

Algebraic simplification applies identities: adding zero, multiplying by one, and similar cases disappear entirely.

The order matters and the transformations feed each other. Constant propagation enables folding, folding enables more propagation, copy propagation creates dead copies, and dead code elimination removes them. Real optimisers iterate until a pass changes nothing.

3. The DAG for a Basic Block

A directed acyclic graph represents a basic block with one node per computed value.

When an expression is constructed, the builder first checks whether a node with the same operator and the same children already exists, and reuses it if so. That check performs common subexpression elimination during construction, with no separate analysis.

Leaves are initial values of variables; interior nodes are operations. A variable name attaches to whichever node currently holds its value, so an assignment moves the label rather than creating a node.

Reading the DAG back out gives optimised code. Any node with no attached name and no parent is dead and its computation is simply omitted.

The DAG also reveals which expressions are worth keeping in registers, since a node with several parents is used several times.

One caution: pointer assignments and function calls invalidate the DAG's assumptions, because either may modify a variable the compiler cannot track. Real implementations conservatively kill the affected nodes at such points.

4. Global Data-Flow Analysis

Beyond a basic block, control may reach a point from several predecessors, and the compiler must account for all of them.

Every analysis is specified by four choices.

Direction: forward analyses propagate information along control flow; backward analyses propagate against it.

Domain: what facts are tracked — definitions, expressions, or variables.

Meet operator: how facts from several paths combine. Union gives a may analysis, asserting something holds on at least one path. Intersection gives a must analysis, asserting it holds on every path.

Transfer function: how a block changes the facts, usually expressed as generating some and killing others.

The equations take a standard shape. For a forward analysis:

The solution is computed iteratively: initialise, apply the equations repeatedly, and stop when nothing changes. Convergence is guaranteed because the facts form a finite lattice and each iteration only moves in one direction.

The result is a conservative approximation, not exact truth. A may analysis over-approximates and a must analysis under-approximates, and both err in the direction that keeps transformations safe.

5. The Four Classical Analyses

AnalysisDirectionMeetAnswers
Reaching definitionsForwardUnionWhich assignments may still be current
Available expressionsForwardIntersectionWhich expressions are already computed on every path
Live variablesBackwardUnionWhich variables may be used later
Very busy expressionsBackwardIntersectionWhich expressions will certainly be computed later

Reaching definitions supports constant propagation and use-definition chains. It is a may analysis because a definition reaching along any path must be considered.

Available expressions supports global common subexpression elimination. It is a must analysis because reusing a previous result requires that it was computed on every path, not merely one.

Live variables supports dead code elimination and register allocation. A variable dead at a point need not be kept in a register, and an assignment to a dead variable can be removed. It is backward because liveness depends on future uses, and a may analysis because a use on any path keeps the variable live.

Very busy expressions supports code hoisting, moving a computation earlier when it will certainly be needed.

The pattern is worth extracting. Analyses that justify removing work need must-information, because removal must be safe on every path. Analyses that justify keeping something need may-information, because any path that uses it forces retention.

6. Loop Optimisations

Loops repay optimisation disproportionately because their bodies execute many times.

A natural loop is identified from the flow graph using dominators. A node dominates if every path from entry to passes through . A back edge runs from a node to one of its dominators, and the natural loop of that back edge is the set of nodes that can reach its source without passing through its target.

Loop-invariant code motion moves a computation whose operands do not change inside the loop out to a preheader block placed before the loop. The computation must be guaranteed to execute, or moving it could introduce a fault or a cost the original never paid.

Induction variable elimination recognises variables advancing by a constant each iteration and expresses them in terms of one another, often removing all but one.

Strength reduction within a loop replaces a multiplication by an induction variable with an addition. An array address computed as a base plus becomes a running pointer incremented by 4.

Loop unrolling replicates the body to reduce the per-iteration test and branch overhead, at the cost of code size and possibly instruction cache pressure.

Loop fusion merges two loops over the same range so that the data is traversed once, improving locality. Loop fission does the reverse when the merged body causes register pressure.

7. Worked Examples

Example 1. Identify the basic blocks in this code.

1:  i = 0
2:  t = i * 4
3:  if i >= n goto 8
4:  a[t] = 0
5:  i = i + 1
6:  t = i * 4
7:  goto 3
8:  return

Apply the three leader rules.

Instruction 1 is a leader, being the first instruction of the program.

Instruction 3 is a leader, being the target of the goto at instruction 7.

Instruction 4 is a leader, immediately following the conditional jump at 3.

Instruction 8 is a leader, both as the target of the branch at 3 and as the instruction following the jump at 7.

Each block runs from a leader to just before the next.

Block A: instructions 1 and 2. Block B: instruction 3. Block C: instructions 4 through 7. Block D: instruction 8.

The flow graph has an edge from A to B, from B to C on the false branch, from B to D on the true branch, and from C back to B.

The edge from C to B is a back edge, since B dominates C — every path from entry to C passes through B. The natural loop is therefore the set containing B and C.

Example 2. Optimise this basic block, naming each transformation applied.

a = 5
b = a * 4
c = b + a * 4
d = c * 1
e = d + 0

Constant propagation replaces with 5 in each use, since is assigned a constant and never reassigned.

a = 5
b = 5 * 4
c = b + 5 * 4
d = c * 1
e = d + 0

Constant folding evaluates at compile time in both places.

a = 5
b = 20
c = b + 20
d = c * 1
e = d + 0

Algebraic simplification removes the multiplication by one and the addition of zero.

a = 5
b = 20
c = b + 20
d = c
e = d

Copy propagation replaces with and then with .

a = 5
b = 20
c = b + 20
d = c
e = c

Constant propagation and folding again, since is now a known constant.

a = 5
b = 20
c = 40
d = c
e = c

Dead code elimination removes any assignment whose target is never used afterwards. If only is live at the block's exit, then , and are all dead.

c = 40
e = c

And a final copy propagation plus dead code pass leaves e = 40.

Note the iteration. Each transformation exposed work for another, which is why real optimisers repeat the whole sequence until a pass changes nothing.

Example 3. Why is available expressions an intersection analysis while reaching definitions is a union analysis?

Available expressions justifies removing a computation, replacing it with a reference to a previously computed value.

For that to be safe, the expression must already have been computed on every path reaching this point, with no operand modified since. If even one path arrives without having computed it, the reference would read a value that was never produced.

Safety therefore requires the fact to hold on all paths, which is intersection.

Reaching definitions justifies keeping information about a variable's possible values.

A definition reaches a point if there is some path along which it is the most recent assignment. If a use might see that definition on any path, the analysis must report it, because ignoring it could lead the compiler to substitute a value that is wrong on that path.

Safety therefore requires including anything possible on any path, which is union.

The general principle is that the direction of conservatism follows the use. An analysis supporting removal must under-approximate — claim less than may be true — so it never removes something that was needed. An analysis supporting retention must over-approximate — claim more than may be true — so it never discards something that was needed.

Live variables follows the same logic: it is a union analysis because a use on any path forces the variable to be kept.

Example 4. Perform loop-invariant code motion on this loop and state the safety condition.

i = 0
L: if i >= n goto END
   x = y * z
   a[i] = x + i
   i = i + 1
   goto L
END:

The computation uses only and , neither of which is assigned anywhere inside the loop. It therefore computes the same value on every iteration and is loop-invariant.

Move it to a preheader, a block inserted between the loop entry and the header.

i = 0
x = y * z
L: if i >= n goto END
   a[i] = x + i
   i = i + 1
   goto L
END:

The multiplication now executes once instead of times.

The safety condition is that the moved computation must be guaranteed to execute in the original program.

Here that fails in one case: if is zero or negative, the original loop body never runs and is never computed. The transformed version computes it unconditionally.

For a multiplication that is harmless — the result is simply discarded. But if the computation could fault, such as a division that might divide by zero, or a memory access that might be out of bounds, then hoisting it introduces a fault the original program did not have.

The standard remedies are to hoist only computations that cannot fault, or to guard the preheader with a test that the loop executes at least once.

Example 5. Apply strength reduction and induction variable elimination to an array-indexing loop.

i = 0
L: if i >= n goto END
   t = i * 4
   p = base + t
   *p = 0
   i = i + 1
   goto L
END:

Recognise as a basic induction variable, since it increases by the constant 1 each iteration.

Recognise as a derived induction variable, since it is always . Because increases by 1 per iteration, increases by 4.

Strength reduction replaces the multiplication with an addition. Initialise before the loop and increment it inside.

i = 0
t = 0
L: if i >= n goto END
   p = base + t
   *p = 0
   i = i + 1
   t = t + 4
   goto L
END:

The multiplication is gone, replaced by an addition, which is cheaper on most machines.

Now is also a derived induction variable, always equal to , so the same treatment applies.

p = base
limit = base + n * 4
L: if p >= limit goto END
   *p = 0
   p = p + 4
   goto L
END:

Induction variable elimination has removed and entirely, replacing the loop counter with the pointer itself and rewriting the termination test in terms of it.

The body now contains one store, one addition and one comparison, against the original's multiplication, two additions, a store and a comparison.

Example 6. Compute live variables at each point in this block, and state which assignments are dead.

1: a = b + c
2: d = a * 2
3: a = e + f
4: g = a + d

Live variable analysis runs backward, starting from the block's exit.

Assume only is live at the exit.

After instruction 4: .

Before instruction 4, which uses and and defines : remove from the live set because it is defined here, then add and because they are used. Live set is .

Before instruction 3, which uses and and defines : remove , add and . Live set is .

Before instruction 2, which uses and defines : remove , add . Live set is .

Before instruction 1, which uses and and defines : remove , add and . Live set is .

Now check for dead assignments. An assignment is dead if its target is not live immediately after it.

Instruction 1 defines . The live set after instruction 1 is , which contains , so this assignment is live — its value is used by instruction 2.

Instruction 2 defines , and is live after it, so it is live.

Instruction 3 defines , and is live after it, so it is live. Note that this is a different value from instruction 1's; the earlier one was already consumed.

Instruction 4 defines , which is live at exit, so it is live.

No assignment is dead here. Had not been live at exit, instruction 4 would be dead, which would in turn kill instructions 3 and 2, and then 1 — a cascade that is exactly why dead code elimination is iterated.

Summary

An optimisation is legal only if it preserves observable behaviour on every input, and data-flow analysis is how that fact is proved rather than assumed.

A basic block has one entry and one exit. Leaders are the first instruction, any jump target, and any instruction after a jump. The flow graph joins blocks wherever control can pass.

Within a block, analysis is exact because control flows straight through. Constant folding, constant propagation, common subexpression elimination, copy propagation, dead code elimination, strength reduction and algebraic simplification all apply, and they feed each other, so the sequence is iterated.

A DAG for a basic block performs common subexpression elimination during construction, by reusing any node with the same operator and children. Nodes with no name and no parent are dead. Pointer writes and calls invalidate the assumptions and force conservative killing.

Every data-flow analysis is fixed by four choices: direction, domain, meet operator and transfer function. Union gives a may analysis and intersection a must analysis, and the solution is found by iterating to a fixed point.

Reaching definitions is forward and union; available expressions forward and intersection; live variables backward and union; very busy expressions backward and intersection.

Analyses justifying removal need must-information because removal must be safe on every path. Analyses justifying retention need may-information because any path that uses something forces keeping it.

Natural loops are found from dominators and back edges. Loop-invariant code motion needs the computation to be guaranteed to execute, or a faulting operation could be introduced. Strength reduction turns a multiplication by an induction variable into an addition, and induction variable elimination can remove the counter entirely.

Key formulas & results

Everything to memorise for the exam hall, in one card. Screenshot this for revision.

The organising principle
legal transformation = proof that observable behaviour is preserved on every input
Optimisation is not guesswork. The compiler must establish a fact about all executions before it may rewrite code, and data-flow analysis is the machinery that establishes it.
Four choices define an analysis
analysis = (direction, domain, meet operator, transfer function)
Fix these four and the analysis is fully determined. Reaching definitions, available expressions and live variables differ only in these four choices.
Forward data-flow equations
IN[B] = meet over predecessors P of OUT[P]; OUT[B] = GEN[B] union (IN[B] minus KILL[B])
The standard shape. A backward analysis swaps IN with OUT and predecessors with successors.
May versus must
meet = union gives may (some path); meet = intersection gives must (every path)
Union over-approximates and intersection under-approximates. Both err in the direction that keeps transformations safe.
Leader rules
leader = first instruction, or any jump target, or any instruction following a jump
Each basic block runs from a leader up to but not including the next leader.
Classification table
reaching definitions = forward union; available expressions = forward intersection; live variables = backward union; very busy expressions = backward intersection
The single most frequently examined fact in this chapter. Memorise it as a two-by-two grid.
Dominator and back edge
d dominates n if every path from entry to n passes through d; a back edge runs from n to a dominator d
The natural loop of a back edge is the set of nodes that reach its source without passing through its target.
Code motion safety
hoisting is safe only if the computation is guaranteed to execute in the original program
Otherwise a faulting operation such as division or an out-of-bounds access can be introduced into a run that never performed it.
Strength reduction on induction variables
if i increases by c each iteration and t = a*i + b, then t increases by a*c each iteration
The multiplication in the body is replaced by an addition, with t initialised in the preheader.
Dead assignment test
an assignment is dead if its target is not live immediately after it
Removal can make earlier assignments dead in turn, so the pass is iterated until nothing changes.
⚠️

Traps GATE sets — and how to dodge them

These are the exact option-traps and misreads that cost marks under negative marking.

WATCH OUT
Calling available expressions a union analysis because it is forward
Derive the meet from what the analysis justifies. Available expressions justifies removing a computation, which must be safe on every path, so intersection.
Why it happens: Direction and meet operator are independent choices. Available expressions is forward and intersection; reaching definitions is forward and union.
WATCH OUT
Treating live variables as a forward analysis
Ask what the fact depends on. If it depends on what comes after, the analysis is backward.
Why it happens: Liveness depends on future uses, not past definitions, so information must propagate against the direction of control flow.
WATCH OUT
Missing the leader that follows a conditional jump
Apply all three rules mechanically to every instruction before drawing any block boundaries.
Why it happens: Students remember that jump targets are leaders but forget that the fall-through instruction after any jump is also a leader.
WATCH OUT
Hoisting a loop-invariant division without checking that the loop body executes
Hoist only non-faulting computations, or guard the preheader with a test that the loop iterates at least once.
Why it happens: If the loop runs zero times, the original program never performs the division, but the transformed one does, so a divide-by-zero can appear.
WATCH OUT
Eliminating a common subexpression across blocks without an availability check
Within a block, straight-line order suffices. Across blocks, the expression must be in AVAIL at the reuse point.
Why it happens: The earlier computation might occur on only one of several paths, or an operand might be reassigned on another path.
WATCH OUT
Applying local optimisations in one pass and stopping
Iterate the whole sequence until a full pass makes no change.
Why it happens: Constant propagation enables folding, folding enables further propagation, and copy propagation creates dead code, so a single pass leaves work undone.
WATCH OUT
Building a DAG across a pointer store or a function call as if nothing changed
Conservatively kill the affected nodes at such points, or restrict the DAG to the region between them.
Why it happens: Either may modify a variable the compiler cannot track, invalidating nodes that appear to still hold current values.
WATCH OUT
Assuming a data-flow solution is exact
Read every result as safe rather than precise. A may analysis reports possibly, and a must analysis reports definitely.
Why it happens: The equations compute a conservative approximation over all paths in the graph, including paths that no execution can actually take.

Exam-pattern practice

PYQ-style questions with full solutions. Work through them as a readiness check — mark yourself honestly and get your gap report at the end.

Readiness check

Are you exam-ready for Local Optimisation & Data Flow Analysis?

10 problems from this chapter. Try each one, reveal the worked solution, mark yourself honestly — get your gap report at the end.

10 questions~7 min

5-minute revision

The whole chapter, distilled. Read this the night before the exam.

  • A transformation is legal only if it preserves observable behaviour on every input, and data-flow analysis supplies the proof
  • Leaders: first instruction, any jump target, any instruction after a jump
  • Inside a basic block control flows straight through, so analysis is exact and local optimisation is cheap
  • Seven local optimisations: constant folding, constant propagation, common subexpression elimination, copy propagation, dead code elimination, strength reduction, algebraic simplification
  • They feed each other, so the sequence is iterated to a fixed point
  • A DAG performs common subexpression elimination during construction by reusing nodes with the same operator and children
  • Every analysis is fixed by direction, domain, meet operator and transfer function
  • Union is may and over-approximates; intersection is must and under-approximates
  • Reaching definitions forward union; available expressions forward intersection; live variables backward union; very busy expressions backward intersection
  • Removal needs must-information; retention needs may-information
  • Natural loops come from dominators and back edges
  • Code motion is safe only if the computation is guaranteed to execute
  • Strength reduction turns a multiply by an induction variable into an addition; induction variable elimination can delete the counter entirely

GATE question blueprint

How this topic is asked, tier by tier — so you can prep to the pattern.

Typical weightage: 4

Question styleMarks eachTypical countWhat it tests
Data flow analysis21
Local optimisation11
Loop optimisation11
Basic blocks and flow graphs11

Exam-hall strategy

Battle-tested tips from mentors and toppers for this topic under the sectional clock.

  1. The classification grid is nearly free marks, so make sure direction and meet operator for all four analyses are automatic before the exam. Leader identification questions are mechanical: apply the three rules to every instruction and count. For data-flow computation questions, write IN and OUT columns for each block and iterate in reverse postorder for forward analyses, which usually converges in two passes on GATE-sized graphs. Safety questions almost always hinge on the zero-iteration case or on a faulting operation, so check both whenever a transformation moves code out of a loop. If a question asks whether an optimisation is legal, look for a reassignment between the two occurrences or a path that skips the computation.

Beyond the exam

Where this skill shows up in the job you're competing for — and in life.

LLVM's pass pipeline runs exactly these analyses

LLVM's pass pipeline runs exactly these analyses, with reaching definitions and liveness underpinning its SSA construction and register allocator

GCC's -O2 level applies loop-invariant code motion

GCC's -O2 level applies loop-invariant code motion, induction variable optimisation and global common subexpression elimination as separate named passes

Static analysers for security use the same may and must f…

Static analysers for security use the same may and must framework to track whether tainted data can reach a sensitive sink

JIT compilers in the JVM and V8 run cheap local optimisat…

JIT compilers in the JVM and V8 run cheap local optimisations on hot basic blocks first, escalating to global analysis only when a method stays hot

Dead store elimination in cryptographic libraries is deli…

Dead store elimination in cryptographic libraries is deliberately suppressed, because the compiler's proof that a zeroing write is dead ignores the security requirement that the buffer be cleared

Where else this topic is tested

Prepare once, score in every exam that asks it.

GATE CS
GATE DA
UGC NET Computer Science
ISRO Scientist SC
BARC Computer Science

Questions aspirants ask

Pulled from the Q&A community and mentor sessions.

By definition it is maximal. Splitting a straight-line run into two blocks would not be wrong in the sense of producing incorrect analysis, but it wastes work and is not what the leader rules produce, so exam answers should always give maximal blocks.

Ask what the fact depends on. Liveness and very-busyness both depend on what happens later in the program, so they propagate backward. Reaching definitions and availability both depend on what happened earlier, so they propagate forward.

Yes. For a union analysis, initialise interior sets to empty so the iteration grows to the least fixed point. For an intersection analysis, initialise interior sets to the universal set so the iteration shrinks. Getting this backward gives the wrong answer even though the algorithm still terminates.

The equations consider every path through the flow graph, including infeasible ones that no input can actually cause. Distinguishing feasible from infeasible paths is undecidable in general, so the analysis accepts imprecision in exchange for termination and safety.

No. It reduces branch and test overhead per iteration and exposes more instruction-level parallelism, but it increases code size, which can hurt instruction cache performance and increase register pressure. Compilers unroll by a bounded factor and only for loops whose trip count is known or large.

Yes. Keeping a value alive from its computation to its reuse extends its live range, which can force a spill if registers are scarce. Recomputing a cheap expression is sometimes better than holding it, which is why some compilers perform rematerialisation, the deliberate opposite of the transformation.
Header Logo