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 a0,b0,…a_0, b_0, \ldots.
  • Interior node. One node per distinct operation ⟨op,left child,right child⟩\langle op, \text{left child}, \text{right child}\rangle on current values.
  • Labels. Each node carries the list of variables whose current value it is. An assignment x=…x = \ldots moves label xx 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 x=yopzx = y \mathbin{op} z in order:

  1. Find the node for the current value of yy: the node labelled yy if any, otherwise a leaf y0y_0 (created if needed). Do the same for zz.
  2. Look for an existing node with operator opop and exactly these children. If one exists, use it; otherwise create it.
  3. Remove label xx from whatever node had it, and attach xx to this node.

A copy x=yx = y creates no node: label xx is attached to yy's current node.

Example 1: a block with a hidden trap

Text
1) t1 = a * b
2) t2 = a * b
3) c  = t2 + d
4) a  = c - d
5) e  = a * b
6) t3 = c - d
StepEffect on the DAG
1Leaves a0a_0, b0b_0; node n1=a0∗b0n_1 = a_0 * b_0 labelled t1t_1
2Same operator and children: n1n_1 gets label t2t_2
3Leaf d0d_0; node n2=n1+d0n_2 = n_1 + d_0 labelled cc
4Node n3=n2−d0n_3 = n_2 - d_0 labelled aa; label aa now means n3n_3, not a0a_0
5Current aa is n3n_3, so n4=n3∗b0n_4 = n_3 * b_0 is a new node, labelled ee
6c−dc - d is n2−d0n_2 - d_0, which is n3n_3: n3n_3 gets label t3t_3

The DAG has 7 nodes: 3 leaves (a0,b0,d0a_0, b_0, d_0) and 4 interior nodes, with 8 edges. Labels: n1{t1,t2}n_1\{t_1, t_2\}, n2{c}n_2\{c\}, n3{a,t3}n_3\{a, t_3\}, n4{e}n_4\{e\}.

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.

DAG of the six-instruction block with node labels
Figure 1. DAG of the block t1 = a * b, t2 = a * b, c = t2 + d, a = c - d, e = a * b, t3 = c - d: 3 leaves a0, b0, d0 and 4 interior nodes labelled t1 t2, c, a t3 and e, with 8 edges; the second a * b uses the new a and gets its own node.

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

Text
t1 = a * b
c  = t1 + d
a  = c - d
e  = a * b

Four instructions replace six. t2t_2 and t3t_3 disappear (common subexpressions), and n1n_1 keeps a temporary because no live variable labels it. Writing a = c - d is safe here because the only node that reads leaf a0a_0 (n1n_1) 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

Text
a = b + c
b = a - d
c = b + c
d = a - d

Line 2 computes a−d0a - d_0, and line 4 computes a−d0a - d_0 again because dd has not been reassigned before line 4. Lines 2 and 4 share one node, labelled bb and dd. Line 3, b+cb + c, uses the new bb (that node) and c0c_0, 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 bb is not live on exit, the code is a = b + c; d = a - d; c = d + c.

Example 3: counting nodes, edges and instructions

Text
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
StepEffect on the DAG
1Leaves a0a_0, b0b_0; n1=a0+b0n_1 = a_0 + b_0 labelled t1t_1
2Same node: n1n_1 gets t2t_2
3Leaf c0c_0; n2=n1∗c0n_2 = n_1 * c_0 labelled t3t_3
4Leaf d0d_0; n3=n2−d0n_3 = n_2 - d_0 labelled bb
5bb is now n3n_3: n4=a0+n3n_4 = a_0 + n_3 is new, labelled t4t_4
6t2t_2 is n1n_1, so t2∗ct_2 * c is n1∗c0=n2n_1 * c_0 = n_2: n2n_2 gets t5t_5
7n5=n4+n2n_5 = n_4 + n_2 labelled xx

The DAG has 9 nodes (4 leaves a0,b0,c0,d0a_0, b_0, c_0, d_0 and 5 interior nodes) and 10 edges. Line 5 repeats the text of line 1 but is a new node, because bb changed in line 4; line 6 differs in text from line 3 but is the same node. With bb and xx live on exit, the seven instructions become five:

Text
t1 = a + b
t2 = t1 * c
b  = t2 - d
t3 = a + b
x  = t3 + t2

Here t1 = a + b must come before b = t2 - d, which overwrites b0b_0. At most two temporaries are live at any point: t2t_2 stays live from its definition to the last instruction, and only t3t_3 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.14 becomes x = 6.28.
  • Algebraic identities. x+0=xx + 0 = x, x×1=xx \times 1 = x, x−x=0x - x = 0 (for integers), x/1=xx / 1 = x.
  • Reduction in strength. Replace an expensive operator by a cheaper one: x2→x×xx^2 \to x \times x, 2×x→x+x2 \times x \to x + x, x/2→x×0.5x / 2 \to x \times 0.5 for floating point, multiplication by a power of 2 by a shift for unsigned integers.
  • Commutativity and associativity. If the compiler may use them, x×yx \times y and y×xy \times x can share a node; the basic method does not assume this.
  • Copy propagation within the block. After x = y, a later use of x may read y only if that copy reaches the use with no intervening redefinition or possible modification of either x or y, including through aliases. This may make the copy dead. In x=y; y=9; z=x, replacing z=x by z=y is 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

Text
x = a[i]
a[j] = y
z = a[i]

z cannot reuse x: if i=ji = j, z is yy 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 immediate LD R0, a can 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 L1 where L1: goto L2 becomes goto 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 * c and t3 = t1 * c when t1t_1 and t2t_2 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

  1. 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).

Next steps