The running example throughout is one deliberately repetitive assignment,
It has a unary minus, two multiplications, an addition and a final assignment, plus a repeated
sub-expression
A quadruple ("quad") is a record with four fields:
| # | op | arg1 | arg2 | result |
|---|---|---|---|---|
| 0 | minus | c | — | t1 |
| 1 | * | b | t1 | t2 |
| 2 | minus | c | — | t3 |
| 3 | * | b | t3 | t4 |
| 4 | + | t2 | t4 | t5 |
| 5 | = | t5 | — | a |
The great virtue is independence. Every temporary has a name of its own, so an
instruction can be picked up and dropped anywhere its operands are still available — the references
travel by name, not by position. Deleting the redundant second copy of
+ t2 t2 and delete instructions 2 and 3, and nothing else needs to
change. This is why quadruples are the layout of choice for optimising compilers.
A triple drops the result field entirely:
(i) to mean "the value produced by triple
| # | op | arg1 | arg2 |
|---|---|---|---|
| 0 | minus | c | — |
| 1 | * | b | (0) |
| 2 | minus | c | — |
| 3 | * | b | (2) |
| 4 | + | (1) | (3) |
| 5 | = | a | (4) |
Triples are compact — no temporaries to store in the symbol table, one fewer field per
record — which is why they were beloved in the memory-scarce era and still show up in tree-shaped IRs
(a triple is essentially a node with two child pointers). But look what "position is the name" costs
us. Instruction 4 refers to (1) and (3) by their row numbers. Move a
triple, insert one, or delete one, and every subsequent row number shifts — so every reference
that pointed past the change is now wrong. Reordering, the optimiser's bread and butter,
becomes a nightmare of renumbering.
The classic engineering escape is a level of indirection. Keep the triples in a pool in any order, but drive execution through a separate listing array that holds pointers to triples in the order they should run. Reordering the program now means shuffling the small listing array; the triples themselves never move, so no reference into them ever breaks.
| listing | → triple # | triple # | op | arg1 | arg2 | |
|---|---|---|---|---|---|---|
| [0] | (0) | (0) | minus | c | — | |
| [1] | (1) | (1) | * | b | (0) | |
| [2] | (2) | (2) | minus | c | — | |
| [3] | (3) | (3) | * | b | (2) | |
| [4] | (4) | (4) | + | (1) | (3) | |
| [5] | (5) | (5) | = | a | (4) |
To swap two statements, swap two entries in the listing column — the triples' own numbers stay
put, so (1) still means the same triple no matter where it appears in the schedule. This is
the same trick a linker's relocation table or a virtual-memory page table plays: any problem in
computer science can be solved by adding a level of indirection (except the problem of too many levels of
indirection).
The two layouts fall out of the same post-order walk. Below we translate
The quad rows carry -> t1 … t5; the triple rows carry no result at all, and their
cross-references are the parenthesised row numbers (0)…(4). Same computation, two philosophies
of naming.
Modern optimising compilers almost universally use something quad-like, because their central data
structure is
The trap with triples is that renumbering is invisible. If you insert a new triple at position 2,
the triple formerly at 2 is now at 3, at 3 is now at 4, and so on — but a reference written as
(3) elsewhere still literally says (3), and now points at the wrong
instruction. Nothing errors; the code just computes garbage. This is exactly why an optimiser that wants
to reorder cannot use raw triples. The two honest fixes are: (a) switch to quadruples,
where references are stable names, or (b) add the listing array of indirect triples, so
the triples never move and only the schedule does. Never hand an instruction-scheduler a bare triple
list.