Introduction
Local optimisation improves one basic block at a time, using only what is known inside the block. Because a block runs straight through, the compiler knows exactly which value each name holds at each point, and simple, safe transformations follow: reuse a value already computed, drop an assignment whose result is never used, fold constants, and replace expensive operations by cheaper ones.
The standard tool is the DAG of a basic block. Each node stands for a value computed in the block; each variable is attached as a label to the node holding its current value. Building the DAG detects local common subexpressions automatically, and regenerating code from it removes redundant and dead instructions. The working tasks are counting DAG nodes or edges and producing the minimal code after optimisation.
Prerequisites
- Basic Blocks and Control-Flow Graphs: what a basic block is and how a program is partitioned into blocks.
- DAGs for single expressions and the value-number method (see the note Intermediate Representations: Syntax Trees, DAGs and Three-Address Code).
Core concepts
- Leaf. One node per variable or constant whose initial value (on entry to the block) is used, written .
- Interior node. One node per distinct operation on current values.
- Labels. Each node carries the list of variables whose current value it is. An assignment moves label to the new (or found) node and removes it from any other.
- Local common subexpression. An instruction whose operator and operand nodes match an existing node: no new node is created, and only a label is added.
- Live on exit. Variables whose values may be used after the block. Only nodes reachable from them need code.
- Dead code. An instruction whose result is not live on exit and not used later in the block.
How it works
Building the DAG of a block
For each instruction in order:
- Find the node for the current value of : the node labelled if any, otherwise a leaf (created if needed). Do the same for .
- Look for an existing node with operator and exactly these children. If one exists, use it; otherwise create it.
- Remove label from whatever node had it, and attach to this node.
A copy creates no node: label is attached to 's current node.
Example 1: a block with a hidden trap
1) t1 = a * b
2) t2 = a * b
3) c = t2 + d
4) a = c - d
5) e = a * b
6) t3 = c - d| Step | Effect on the DAG |
|---|---|
| 1 | Leaves , ; node labelled |
| 2 | Same operator and children: gets label |
| 3 | Leaf ; node labelled |
| 4 | Node labelled ; label now means , not |
| 5 | Current is , so is a new node, labelled |
| 6 | is , which is : gets label |
The DAG has 7 nodes: 3 leaves () and 4 interior nodes, with 8 edges. Labels: , , , .
The trap is line 5: textually it repeats line 1, but a was redefined in line 4, so the value differs. Line 6 is the opposite case: it looks different from line 4 in its target, but it computes the same value from the same nodes.

Regenerating code from the DAG
Evaluate interior nodes in an order consistent with their edges (children first), emitting one instruction per node that some live variable needs. Name each node by one of its live labels when doing so does not overwrite a value that a later node still reads; otherwise use a temporary.
If only a, c and e are live on exit, the block becomes
t1 = a * b
c = t1 + d
a = c - d
e = a * bFour instructions replace six. and disappear (common subexpressions), and keeps a temporary because no live variable labels it. Writing a = c - d is safe here because the only node that reads leaf () has already been computed. When a live label would overwrite an initial value still needed later, the result goes to a temporary and is copied at the end.
Example 2: shared nodes despite a redefinition
a = b + c
b = a - d
c = b + c
d = a - dLine 2 computes , and line 4 computes again because has not been reassigned before line 4. Lines 2 and 4 share one node, labelled and . Line 3, , uses the new (that node) and , so it is a new node and is not the same as line 1, even though it reads the same names. The DAG has 3 interior nodes. If is not live on exit, the code is a = b + c; d = a - d; c = d + c.
Example 3: counting nodes, edges and instructions
1) t1 = a + b
2) t2 = a + b
3) t3 = t1 * c
4) b = t3 - d
5) t4 = a + b
6) t5 = t2 * c
7) x = t4 + t5| Step | Effect on the DAG |
|---|---|
| 1 | Leaves , ; labelled |
| 2 | Same node: gets |
| 3 | Leaf ; labelled |
| 4 | Leaf ; labelled |
| 5 | is now : is new, labelled |
| 6 | is , so is : gets |
| 7 | labelled |
The DAG has 9 nodes (4 leaves and 5 interior nodes) and 10 edges. Line 5 repeats the text of line 1 but is a new node, because changed in line 4; line 6 differs in text from line 3 but is the same node. With and live on exit, the seven instructions become five:
t1 = a + b
t2 = t1 * c
b = t2 - d
t3 = a + b
x = t3 + t2Here t1 = a + b must come before b = t2 - d, which overwrites . At most two temporaries are live at any point: stays live from its definition to the last instruction, and only is ever live beside it. No valid order of the five instructions does better.
Other local transformations
- Dead-code elimination. Delete any root node (no parents) that has no live label, and repeat.
- Constant folding. Evaluate operations on constants at compile time:
x = 2 * 3.14becomesx = 6.28. - Algebraic identities. , , (for integers), .
- Reduction in strength. Replace an expensive operator by a cheaper one: , , for floating point, multiplication by a power of 2 by a shift for unsigned integers.
- Commutativity and associativity. If the compiler may use them, and can share a node; the basic method does not assume this.
- Copy propagation within the block. After
x = y, a later use ofxmay readyonly if that copy reaches the use with no intervening redefinition or possible modification of eitherxory, including through aliases. This may make the copy dead. Inx=y; y=9; z=x, replacingz=xbyz=yis invalid.
Array references and pointers
An assignment to an array element must kill the nodes that read that array, since the compiler usually cannot tell whether two subscripts are equal. In
x = a[i]
a[j] = y
z = a[i]z cannot reuse x: if , z is while x is the old element. The assignment a[j] = y makes the earlier a[i] node unavailable. Similarly, an assignment through a pointer *p = w or a procedure call can change any variable the pointer or callee might reach, so it kills all nodes whose operands could be affected.
Peephole optimisation
Peephole optimisation examines a short sliding window of target instructions:
- Redundant loads and stores: after
ST a, R0, an immediateLD R0, acan go (if the load is not a jump target). - Unreachable code: instructions after an unconditional jump with no label pointing to them.
- Jumps to jumps:
goto L1whereL1: goto L2becomesgoto L2. - Algebraic simplification and strength reduction on target instructions.
- Machine idioms: using auto-increment addressing or special instructions.
Repeated passes can enable each other.
Important properties and observations
- DAG nodes = leaves for initial values used + distinct operations on current values. Count carefully after each reassignment.
- Edges = 2 per binary node + 1 per unary node.
- A DAG detects only local common subexpressions; global ones across blocks need available-expression analysis (see the note Common Subexpression Elimination).
- Textual equality is neither necessary nor sufficient: equal text can denote different values (Example 1, line 5) and different text can denote equal values.
- Order of regeneration matters: a label may be assigned only when no later node still needs the variable's old value.
Common misconceptions.
- "A redefined variable gets a new leaf." A leaf stands only for the value on entry; after a redefinition the variable labels the node for its new value, which may be an existing leaf for a copy or an operation node for a computation.
- "Copy statements create nodes." They only attach a label.
- "Array assignments can be optimised like scalars." They must kill nodes that read the same array.
GATE examination perspective
- Count DAG nodes and edges. Apply this to a block; watch reassigned variables and repeated expressions.
- Minimum number of instructions. After local common-subexpression and dead-code elimination, given the variables live on exit.
- Spot the trap. Where a textual repeat is not a common subexpression because an operand changed.
- The opposite trap. Different text, same value, as
t5 = t2 * candt3 = t1 * cwhen and label one node (Example 3). - Order constraints when regenerating. An instruction that overwrites a variable must follow every instruction that reads the variable's old value.
- Name the transformation. Constant folding, strength reduction, algebraic simplification, copy propagation, dead-code elimination, peephole patterns.
- Array and pointer kills. As a reason an apparent common subexpression is not one.
Summary
Sources
- A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers, Principles, Techniques, and Tools, 2nd ed., Pearson Addison-Wesley, 2007: Section 8.5 (the DAG representation of basic blocks, finding local common subexpressions, dead-code elimination, the use of algebraic identities, representation of array references, pointer assignments and procedure calls, reassembling basic blocks from DAGs), Section 8.7 (peephole optimisation: redundant loads and stores, unreachable code, flow-of-control optimisations, algebraic simplification and reduction in strength, machine idioms), Section 9.1.3 (semantics-preserving transformations).