Inside a single basic block there is no branching, no merging — just a straight run of three-address instructions executed top to bottom. That simplicity is an opportunity. If we can capture what the block computes, stripped of the order in which it happened to be written, we can spot every redundant recomputation, every value that is never used, and every copy that could be short-circuited — all at once, with one data structure. That data structure is the directed acyclic graph of the basic block: the DAG.
The
A DAG for a basic block has one node per distinct computed value:
The construction is essentially value numbering: before creating a node for
Watch the DAG grow for the Dragon Book's classic four-statement block. Each interior node appears as we process its statement; the payoff comes at the last line, where a value we already have is reused rather than recomputed:
The revelation is the fourth statement, d = a - d. Its value is
b = a - d, already computed. Its node already exists, so we do not build a new one: we
just attach the label
Once built, the DAG hands you three optimizations for free when you regenerate code from it:
| Optimization | How the DAG delivers it |
|---|---|
| Common-subexpression elimination | Two computations of the same value map to one node; you emit the operation once. |
| Dead-code elimination | A node whose attached variables are all dead on exit (and that no live node depends on) need not be evaluated at all — don't emit it. |
| Copy propagation | A copy x = y attaches x to y's node instead of making a new one, so later uses read y directly. |
To regenerate optimised code, walk the interior nodes in an order that respects the edges (children before parents — a topological order), emitting one instruction per live node and, for a node with several attached live variables, one assignment to bind each of them. The block that comes out never computes the same value twice.
Here is the value-numbering builder. Each interior node is keyed by its signature
op,leftId,rightId; if the key is already in the table, we reuse the existing node — that
reuse is the detected common subexpression. Watch the log flag the fourth statement.
The output shows node #4 — the value b and d attached, and the fourth statement flagged as a reuse.
Four multiplications' worth of source has become three distinct interior nodes.
Because array and pointer stores can invalidate nodes. When you build the DAG and reach an
assignment through a memory reference — a[i] = x — you cannot assume any node built from
an array load like a[k] is still valid, because i and k might be
equal. The safe rule is that a store to a kills every node that loaded
from a: they must be recomputed if used again. In the first snippet no intervening store
can alias, so y = a[i] reuses the just-stored value; in the second, a[j] = z
might alias a[i], so the load node is killed and y = a[i] is recomputed. This
conservative killing is exactly what keeps the DAG correct in the presence of aliasing.
A DAG summarises the values computed within one basic block. It knows nothing about what happens across a branch or a loop back-edge, and it treats every variable live on entry as an opaque leaf. So you must never carry a DAG's conclusions across a block boundary: a value that is a "common subexpression" within a block may be recomputed legitimately in a successor block where an operand has changed. Two further traps: a store through a pointer or array kills the loaded-value nodes it might alias (see above), and any function call that could have side effects must be treated as killing whatever it might modify. Global redundancy elimination across blocks needs the heavier machinery of available-expressions dataflow analysis, not a single DAG.