8.2 Principal Sources of Code Optimization: Local, Global & Loop Transformations
💡 Core Intuition: The 3-Second Mental Model
🍳 The Everyday Analogy: The Formula 1 Pit Crew & Track Engineer
Writing high-level source code is like a racing driver navigating a Grand Prix course. The Code Optimizer is the telemetry engineering team and pit crew that tunes the vehicle before race day:
- Constant Folding: Instead of having the car's onboard computer compute every millisecond while hurtling down the straight, the engineer pre-computes in the garage before the race begins.
- Loop-Invariant Code Motion (Hoisting): If a mechanic needs to verify tire pressure, they do it once in the pit lane before the driver starts their stint, rather than jumping onto the chassis to check the tire on every lap around the circuit.
- Strength Reduction: Instead of driving a heavy hydraulic turbine (multiplication) on every revolution, they swap it for an incremental tooth gear (addition) that delivers the exact same mechanical output with 80% less engine friction.
💻 Bridging to Computer Science: Pruning Computational Waste Across Scope Boundaries
In compiler optimization:
- Optimization passes transform intermediate code into equivalent code that runs faster, uses less memory, and consumes less energy.
- Local Optimizations operate within a single basic block (e.g. local common subexpression elimination via DAGs).
- Loop Optimizations target the inner loops where programs spend 90% of their execution time (the 90/10 Rule), applying loop-invariant code motion, induction variable elimination, and strength reduction.
📑Table of Contents
- 🏗️ Architectural Foundations: Local vs Global vs Loop Optimizations
- 📐 Formal Foundations: The Three Cardinal Rules of Optimization
- 🛠️ Taxonomy of Machine-Independent Transformations
- 🔄 Deep-Dive: Loop Optimizations
- 🏭 Real-World Production Context: How Industry Builds This
- 📐 Numerical Applications & Worked Examples
- 🎯 Exam & Interview Pitfall Check
🏗️ Architectural Foundations: Local vs Global vs Loop Optimizations
Scope of Optimization: Local vs Global vs Loop
Classifying compiler optimizations by the scope of their analysis windows
Local Optimization
- •Analyzes straight-line code within one isolated basic block.
- •Uses DAG construction and local value numbering.
- •Fastest compile-time execution; zero inter-block dataflow tracking.
- •Eliminates local duplicate calculations and constant expressions.
Loop Optimization
- •Targets the 10% of code that consumes 90% of total CPU execution time.
- •Hoists invariant expressions to the loop preheader.
- •Replaces heavy multiplications with iterative additions (strength reduction).
- •Eliminates redundant induction variables to free up physical hardware registers.
📐 Formal Foundations: The Three Cardinal Rules of Optimization
Any automated code optimization pass in a production compiler must strictly adhere to three foundational axioms:
- Semantic Preservation (The Golden Rule): The transformation must preserve the exact observable behavior of the program across all legal inputs. A transformation that speeds up code by but produces an incorrect answer on of inputs is not an optimization; it is a critical compiler bug.
- Speed & Size Improvement: The transformation must measurably reduce execution time, CPU cycles, or binary code size on average target workloads.
- Reasonable Compile-Time Overhead: The compiler must complete optimization passes in reasonable time. Spending 45 minutes of compile time to save 2 nanoseconds of runtime is mathematically impractical.
🛠️ Taxonomy of Machine-Independent Transformations
Core Machine-Independent Optimization Transformations
The progressive reduction of redundant operations across intermediate code
Constant Folding & Propagation
Evaluating static operations at compile time and forwarding constant values.
Common Subexpression Elimination
Reusing previously computed values instead of recalculating identical expressions.
Dead Code Elimination
Deleting instructions whose results are never read by any live variable.
Loop-Invariant Code Motion
Lifting loop-independent computations out of the loop into a preheader.
1️⃣ Compile-Time Evaluation
-
Constant Folding: The compiler computes the values of expressions whose operands are known compile-time constants:
-
Constant Propagation: If a variable is assigned a constant value , the compiler replaces all subsequent uses of (up until any potential reassignment) with the constant :
2️⃣ Common Subexpression Elimination (CSE)
An occurrence of an expression is a Common Subexpression if was previously computed, and the values of the operands of have not changed since the previous computation.
// Before CSE:
t1 = 4 * i;
v = a[t1];
t2 = 4 * i; // Redundant! Operands 4 and i are unchanged.
w = b[t2];
// After CSE:
t1 = 4 * i;
v = a[t1];
w = b[t1]; // Reuses t1 directly!
3️⃣ Copy Propagation
Given a copy assignment , copy propagation replaces uses of with , provided no assignment to occurs in between. While itself does not seem wasteful, copy propagation frequently turns variable into Dead Code, enabling the entire assignment to be erased!
4️⃣ Dead Code Elimination
A variable is live at point if its value could be read along some execution path starting at ; otherwise, it is dead. Any assignment where is dead and the expression produces no observable side effects (such as memory writes or exceptions) can be deleted outright.
🔄 Deep-Dive: Loop Optimizations
Since software spends the vast majority of execution time inside loops, loop transformations yield the highest performance returns:
1️⃣ Loop-Invariant Code Motion (LICM / Hoisting)
An expression is Loop-Invariant if its operands take on the exact same value across every iteration of the loop.
Preconditions for Hoisting Expressions Out of Loop L:
To safely hoist an assignment to a loop preheader block:
- The definition of must dominate all exits of loop (or must be dead at all loop exits).
- There must be no other assignment to variable within loop .
- The definition of must dominate all uses of inside loop .
// Before Hoisting:
for (int i = 0; i < n; i++) {
x = y + z; // y and z do not change inside the loop!
a[i] = 6 * i + x;
}
// After Hoisting:
x = y + z; // Hoisted out to Loop Preheader!
for (int i = 0; i < n; i++) {
a[i] = 6 * i + x;
}
2️⃣ Induction Variables & Strength Reduction
A variable is an Induction Variable of a loop if its value changes by a constant amount each time around the loop:
- Basic Induction Variable (): Only updated via .
- Derived Induction Variable (): Defined as , where is a basic induction variable.
Strength Reduction replaces expensive operations (e.g. integer multiplication ) with cheaper hardware operations (e.g. addition ):
Strength Reduction & Induction Variable Elimination Trace
Lowering iterative multiplication into incremental addition
Initialize Derived Temp
Replace Multiplication with Temp
Additive Step
Eliminate Basic Variable
🏭 Real-World Production Context: How Industry Builds This
In industrial production compilers (such as GCC, Clang, and HotSpot JIT):
Modern compilers group optimization passes into standardized levels:
-O0: Zero optimizations. Generates literal 1-to-1 machine instructions matching the source. Essential for interactive debugging.-O1: Basic optimizations: local DAG simplifications, dead code elimination, and constant propagation.-O2: Production default: global common subexpression elimination, loop-invariant code motion (LICM), strength reduction, and function inlining.-O3: Aggressive optimizations: automatic SIMD auto-vectorization (AVX-512), loop unrolling, and link-time whole-program optimization.-Os/-Oz: Size-focused optimizations: disables code-duplicating passes (like unrolling) to produce the smallest executable binary for embedded devices.
In LLVM's LoopVectorizePass, loops operating over independent array indices are transformed to execute multiple iterations simultaneously using CPU SIMD vector registers (e.g. processing 8 32-bit floats in a single AVX clock cycle).
📐 Numerical Applications & Worked Examples
Example 1: Full Loop Optimization Pipeline
Consider the following unoptimized Three-Address Code for clearing an array of 50 integers:
(1) i = 1
(2) t1 = 4 * i
(3) a[t1] = 0
(4) t2 = i + 1
(5) i = t2
(6) if i <= 50 goto (2)
Apply:
- Strength Reduction on the derived induction variable .
- Induction Variable Elimination to completely remove the basic induction variable . Show the final optimized instruction stream.
Step-by-Step Analytical Solution:
Step 1: Identify Induction Variables
- Basic induction variable: (starts at 1, incremented by 1 in lines 4–5).
- Derived induction variable: .
Step 2: Apply Strength Reduction on Derived Variable
- In the preheader (before loop entry):
- At , .
- Emit initialization: .
- Inside the loop:
- When increments by , increases by .
- Replace line (2) with an incremental addition at the bottom of the loop: .
Intermediate Code:
(1) i = 1
(2) t1 = 4
(3) a[t1] = 0
(4) t1 = t1 + 4
(5) t2 = i + 1
(6) i = t2
(7) if i <= 50 goto (3)
Step 3: Eliminate Basic Induction Variable
After strength reduction, variable is consumed only to evaluate the loop termination condition: if i <= 50 goto (3).
- At termination: .
- Because line (4) increments before the test, when , reaches .
- Therefore, the condition is mathematically equivalent to: (testing against the updated bound).
- Variables and are now completely dead! We eliminate them entirely.
Final Optimized Three-Address Code:
(1) t1 = 4
(2) a[t1] = 0
(3) t1 = t1 + 4
(4) if t1 <= 200 goto (2)
Optimization Results:
- Loop body reduced from 5 instructions to 3 instructions.
- Costly hardware multiplication eradicated.
- Register pressure halved: variable is deleted from physical register allocation!
Example 2: Legality of Loop-Invariant Code Motion
Consider the loop:
while (p > 0) {
if (flag == 1) {
x = a + b;
}
p--;
}
Can the statement be hoisted out of the loop into a preheader if variables and are never modified inside the loop? Justify analytically.
Step-by-Step Analytical Trace:
- Verify Precondition 1 (Dominance of Exits):
- Does the statement dominate all exits of the loop?
- No. If , the statement is bypassed, yet the loop decrements and terminates when .
- Failure Counterexample:
- Suppose before the loop: and .
- During loop execution, because , is never assigned, so after the loop exits, must remain .
- If the compiler unconditionally hoists to the preheader:
- will be overwritten with even when !
- This alters the value of observable after the loop, directly violating the Semantic Preservation Axiom.
- Verdict: The statement CANNOT be hoisted unless the compiler can prove that is invariant across all iterations, or is dead outside the loop.
🎯 Exam & Interview Pitfall Check
Question 1: "What are the essential preconditions required before an expression can be safely hoisted out of a loop?"
Key Focus Points:
- The statement defining must dominate all loop exits (or must be dead at all loop exits).
- There must be no other assignments to in the loop.
- The definition of must dominate all uses of in the loop.
Question 2: "What is the difference between a basic induction variable and a derived induction variable?"
Key Focus Points:
- Basic Induction Variable: A variable whose only assignments in the loop have the form (where is a loop-invariant constant).
- Derived Induction Variable: A variable whose value is a linear function of a basic induction variable ().
Trap 1: Hoisting potentially trapping operations out of conditional loops.
Answer: Critical safety hazard. If an expression like sits inside a loop that may execute zero times, hoisting it to a preheader can trigger an unexpected floating-point exception (SIGFPE if ) on inputs that would have run safely in the unoptimized program.
Trap 2: Assuming loop unrolling always improves performance.
Answer: Hardware cache penalty. Aggressive loop unrolling inflates machine code footprint. If the loop size exceeds the CPU's Level-1 Instruction Cache (L1 I-Cache), severe cache thrashing occurs, causing the program to run slower than the unrolled original.