The
The clean way to think about it is tree tiling. Express the computation as an expression tree; give each machine instruction a tile — a little tree pattern it can implement, with a cost. Selecting instructions is then covering the whole tree with non-overlapping tiles so that every node is covered exactly once, minimising total cost. Each tile laid down becomes one emitted instruction; the edges crossing a tile's boundary become the registers that flow between instructions.
A tile is a fragment of tree — an operator with some children — annotated with the instruction it emits and its cost (cycles, or code size). A few tiles for a typical machine:
| Tile (tree pattern) | Instruction | Covers | Cost |
|---|---|---|---|
ADD Rd, Rs, Rt | 1 node | 1 | |
MUL Rd, Rs, Rt | 1 node | 1 | |
MADD Rd, Rs, Rt, Ru | 2 nodes | 1 | |
LD Rd, c(Rs) | 2 nodes | 1 | |
LD Rd, (Rs,Rt,c) | 3 nodes | 1 |
The last two are the payoff of a CISC addressing mode: a single load instruction that swallows an entire address-arithmetic subtree — a multiply-by-scale, an add of a base, a constant displacement — for the price of one. A big tile that covers three nodes at cost 1 beats three little tiles at cost 3. Instruction selection is largely a hunt for these big, cheap tiles.
The simplest good algorithm is maximal munch. Starting at the root, choose the
largest tile that matches there — the one covering the most nodes — emit its instruction,
then recurse on each subtree left dangling below the tile's leaves. It is greedy and top-down, and it
runs in linear time. Below, the tree for MADD tile. Reveal the tiling:
Each dashed region is one tile — one emitted instruction. The value produced by a tile leaves
through its top edge as a register that the tile above consumes. Where the two-node
MADD tile lands, it replaces what would have been a separate MUL then
ADD — one instruction instead of two. That is exactly the win instruction selection
exists to capture.
Maximal munch is locally optimal — the biggest tile at each step — but not necessarily globally optimal. Grabbing a fat tile high in the tree can force awkward, expensive tiles below it, for a worse total than a slightly smaller top tile would have allowed. When you truly want the minimum-cost tiling, use dynamic programming.
This is the idea behind tools like BURS (bottom-up rewrite systems) and code
generators such as iburg/burg: you write the tiles as a cost-annotated
tree grammar, and the tool produces a linear-time, provably optimal tiler by dynamic programming
over the tree. Modern compilers largely automate instruction selection this way rather than by
hand-written case analysis.
Here is maximal munch in code. Each node is tiled by the largest matching pattern; the fused
MADD tile is tried before the plain ADD, so it wins wherever a
The fused run emits one fewer arithmetic instruction: the big tile paid off. Swap the tree for
something whose shape doesn't match MADD and the big tile simply never fires — which is
the whole point of pattern matching: tiles apply only where the tree actually has their shape.
Because an addressing mode is a big tile. Writing LD R1, 8(R2) quietly performs
an add (lea or a
scaled-index load like mov eax, [rbx + rcx*4 + 8] covers an even bigger subtree: a
multiply-by-4, two adds, and (for a load) a dereference — four IR operations, one instruction. The
selector's job is to notice those subtrees in the IR and reach for the fat instruction instead of
emitting the arithmetic longhand. This is why CISC machines lean so heavily on a good tiling
selector, and why RISC machines — with simpler, smaller tiles — make selection almost trivial but
push the cleverness into
"Always grab the biggest tile" feels like it must be best, but it is a greedy heuristic and greed can misfire. Suppose the biggest tile at the root covers four nodes but leaves behind two subtrees that can only be tiled by three costly instructions each; a slightly smaller top tile might have left cheaply-tileable subtrees and won on total cost. Maximal munch never reconsiders — it commits to the local maximum and recurses. When you need the genuine minimum, use the dynamic-programming tiler: it computes the optimal cost of every subtree once, bottom-up, and combines them, so a locally smaller tile is chosen whenever it leads to a globally cheaper cover. Greedy is fine for many machines and always linear; DP costs a little more machinery and buys provable optimality.