A correct compiler that emits naïve code is only half a compiler. The front end has faithfully
translated the source into
"Machine-independent" is the key phrase. These transformations reason about the program's meaning, not about how many registers your chip has or how deep its pipeline is. That comes later, in the back end. Here we ask only: is this computation redundant, dead, or movable? — and answer in a way that would help an ARM, an x86 and a RISC-V equally.
Before we list the transformations, internalise the framing that governs all of them. An optimization is a rewrite of the program, and every rewrite must clear two hurdles, in order:
Safety is where the
Here are the workhorse transformations. Each is defined by a local pattern it matches and a rewrite it applies; several depend on a supporting analysis to prove safety.
| Transformation | What it does | Before → after |
|---|---|---|
| Common-subexpression elimination (CSE) | Reuse a value already computed instead of recomputing it. | t1=b+c; … t2=b+c → t1=b+c; … t2=t1 |
| Copy propagation | After x=y, replace later uses of x with y. |
x=y; z=x+1 → x=y; z=y+1 |
| Constant folding | Evaluate a constant expression at compile time. | t=3*4 → t=12 |
| Constant propagation | Substitute a variable known to hold a constant. | x=5; y=x+2 → x=5; y=7 |
| Dead-code elimination (DCE) | Delete a computation whose result is never used. | x=y+z; // x unused → (removed) |
| Code motion (loop-invariant) | Hoist a computation that does not change inside a loop to just before it. | while(…){ t=a*b; … } → t=a*b; while(…){ … } |
| Strength reduction | Replace an expensive operation with a cheaper equivalent. | i*4 in a loop → running add t=t+4 |
| Induction-variable elimination | Remove a loop variable derived from another by keeping only one counter. | drop j=i*4, iterate the address directly |
These do not act in isolation — they feed one another. Constant propagation exposes constant folding; folding and copy propagation expose more dead code; removing dead code frees a register that lets another value live. This is why real optimizers run passes to a fixed point, cycling until nothing more changes.
The same transformation costs different amounts of analysis depending on how far you look:
Optimizers pour a disproportionate effort into loops, and the justification is empirical and blunt: a
program typically spends about 90% of its running time in about 10% of its code, and
that hot 10% is almost always inside loops. A statement in the body of a doubly-nested loop that runs
The frequency factor is enormous inside loops and tiny outside them, so the arithmetic settles the priorities. Hoisting one loop-invariant multiply out of a loop that runs a million times removes a million multiplies; folding a constant in start-up code removes one. Both are safe; only the first is seriously profitable. This is the profitability half of our framing made concrete.
Take a short block and apply the local transformations by hand, then let the code do the same. Start with this three-address code:
Constant propagation + folding: a=3, b=4 so t1 = 3*4 = 12.
Copy propagation: c is just t1, so t2 = t1 + a =
12 + 3 = 15, and d = 15. If a, b, c, t1, t2 turn out to be unused
afterwards, dead-code elimination deletes them, leaving only what the block's live
outputs need. The runnable version below performs constant folding and copy propagation on exactly
this style of block:
Every right-hand side collapses to a literal: the optimizer has folded and propagated its way to
d = 15 without ever running the program. A following dead-code pass would then sweep away
any of these assignments whose targets are not live on exit.
Not blindly. Optimizations that are perfectly safe under the language's abstract semantics can still
surprise you. Floating-point arithmetic is not associative, so reordering (a+b)+c to
a+(b+c) — tempting for a scheduler — can change the last bit of a result; compilers keep
this behind a -ffast-math-style flag for exactly this reason. Aggressive elimination of a
"dead" store to a security-sensitive buffer can leave a password in memory. And heavy inlining trades
speed for code size, which can hurt if it blows the instruction cache. The catalogue tells you
what is legal; judgement and cost models tell you what is wise.
The single most dangerous habit is to spot a "faster" rewrite and apply it because it looks like a win,
without first proving it preserves meaning. Consider hoisting t = a[i] out of a loop
because it looks invariant — but if the loop writes to a or to a pointer that might alias
a, the load is not invariant and hoisting it changes the answer. Profitability
can never buy back a lost correctness: an optimization that is 10× faster and occasionally wrong has
negative value. Always run the analysis that establishes safety before you reach for the
cost model. Every transformation in the catalogue has a matching safety condition; know it before you
apply the rewrite.