COMPILER DESIGN / 5. OPTIMIZATION

Compiler Optimization

Making code faster and smaller — the techniques that make compiled code beat handwritten code


EXPLANATION

Optimization transforms the IR into faster or smaller code, without changing the program's meaning (observable behavior). Modern compilers perform hundreds of optimization passes. This is why optimized compiled C can be faster than hand-written assembly — humans can't track all the interactions between optimizations.

Key principle: correctness first. An optimization that changes behavior (even to make it "faster") is a BUG in the compiler.

LOCAL OPTIMIZATIONS (within a basic block):

Constant Folding:
Evaluate constant expressions at compile time.
"x = 2 + 3" → "x = 5" (no runtime addition needed)
"y = 60 * 60 * 24" → "y = 86400"

Constant Propagation:
Replace variable uses with their known constant values.
"x = 5; y = x + 3" → "x = 5; y = 8"

Algebraic Simplification:
Use algebraic identities to simplify expressions.
"x * 1" → "x"
"x + 0" → "x"
"x * 2" → "x << 1" (shift is faster than multiply)
"x * 0" → "0"

Dead Code Elimination (DCE):
Remove code whose results are never used.
"t = x + y; return x" → "return x" (t is dead — never used after)
Unreachable code after return/goto.

Common Subexpression Elimination (CSE):
Don't compute the same expression twice.
"a = b+c; d = b+c" → "t=b+c; a=t; d=t"

GLOBAL OPTIMIZATIONS (across basic blocks):

Loop Optimizations (most impactful — loops run many times):
- Loop Invariant Code Motion (LICM): move computations that don't change inside the loop to before the loop. "for i in range(n): x = a+b; arr[i] = x*i" → hoist "a+b" out of loop.
- Loop Unrolling: execute loop body 2, 4, 8 times per iteration. Reduces loop overhead, enables more instruction-level parallelism.
- Loop Fusion: combine two loops over same range into one. Better cache behavior.
- Strength Reduction: replace expensive operations in loops. "i*4" → accumulate "+4" each iteration instead of multiply.

Inlining:
Replace function call with function body. Eliminates call overhead, enables further optimization across the call boundary. Too much inlining = code bloat.

Tail Call Optimization (TCO):
A recursive call in tail position (last thing function does) is transformed into a loop. Prevents stack overflow for recursive programs.

Register Allocation:
Assign IR temporaries to physical CPU registers. Variables used together compete for registers. Spilled variables go to memory (stack). Graph coloring algorithm: variables that are "live" at the same time can't share a register (adjacent in interference graph).

Instruction Scheduling:
Reorder instructions to avoid pipeline stalls (data hazards). Don't put dependent instructions adjacent — insert independent instructions between them to let the pipeline fill.

DIAGRAM

OPTIMIZATION PIPELINE:

  Original TAC:           After constant folding + propagation:
  x = 2                   x = 2
  y = 3                   y = 3
  t1 = x + y              t1 = 5       ← 2+3 computed at compile time
  t2 = t1 * 1             t2 = 5       ← *1 eliminated
  t3 = t2 + 0             t3 = 5       ← +0 eliminated
  result = t3             result = 5   ← all propagated

  After dead code elimination:
  result = 5              ← x, y, t1, t2, t3 never used again

  LOOP INVARIANT CODE MOTION:
  Before:                 After:
  for i in range(n):      c = a + b          ← hoisted out!
      c = a + b           for i in range(n):
      arr[i] = c * i          arr[i] = c * i

  COMMON SUBEXPRESSION ELIMINATION:
  Before:                 After:
  a = x*y + z             t = x*y            ← computed once
  b = x*y - z             a = t + z
                          b = t - z

  INLINING:
  def square(x): return x*x
  y = square(5)           y = 5*5 = 25       ← inlined + folded

CODE

PYTHON
1from copy import deepcopy
2
3# ── Optimization passes on TAC ─────────────────────────
4
5def constant_fold(instructions):
6 """Replace constant expressions with their values"""
7 result = []
8 for instr in instructions:
9 if isinstance(instr, TACBinOp):
10 # Try to evaluate if both operands are constants
11 try:
12 left = float(instr.left)
13 right = float(instr.right)
14 val = eval(f"{left} {instr.op} {right}")
15 # Convert to int if whole number
16 val = int(val) if val == int(val) else val
17 result.append(TACCopy(instr.result, str(val)))
18 print(f" Fold: {instr.left} {instr.op} {instr.right} {val}")
19 continue
20 except (ValueError, ZeroDivisionError):
21 pass
22 result.append(instr)
23 return result
24
25def constant_propagation(instructions):
26 """Replace variable uses with known constant values"""
27 constants = {} # var → constant value
28 result = []
29 for instr in instructions:
30 if isinstance(instr, TACCopy):
31 try:
32 float(instr.value) # is it a constant?
33 constants[instr.result] = instr.value
34 print(f" Prop: {instr.result} = {instr.value} (constant)")
35 except ValueError:
36 if instr.value in constants:
37 old = instr.value
38 instr = TACCopy(instr.result, constants[instr.value])
39 print(f" Prop: replace {old} {instr.value}")
40 constants.pop(instr.result, None)
41
42 elif isinstance(instr, TACBinOp):
43 left = constants.get(instr.left, instr.left)
44 right = constants.get(instr.right, instr.right)
45 if left != instr.left or right != instr.right:
46 print(f" Prop: {instr.left}{left}, {instr.right}{right}")
47 instr = TACBinOp(instr.result, left, instr.op, right)
48 constants.pop(instr.result, None)
49
50 result.append(instr)
51 return result
52
53def dead_code_elimination(instructions):
54 """Remove assignments whose results are never used"""
55 # Count uses of each variable (backwards scan)
56 used = set()
57 for instr in reversed(instructions):
58 if isinstance(instr, (TACReturn, TACCondJump)):
59 if isinstance(instr, TACReturn):
60 used.add(instr.value)
61 else:
62 used.add(instr.condition)
63 elif isinstance(instr, TACBinOp):
64 if instr.result in used or not instr.result.startswith('t'):
65 used.add(instr.left)
66 used.add(instr.right)
67 elif isinstance(instr, TACCopy):
68 if instr.result in used or not instr.result.startswith('t'):
69 used.add(instr.value)
70
71 result = []
72 for instr in instructions:
73 if isinstance(instr, (TACBinOp, TACCopy)):
74 var = instr.result
75 if var.startswith('t') and var not in used:
76 print(f" DCE: Remove dead assignment to {var}")
77 continue
78 result.append(instr)
79 return result
80
81def algebraic_simplification(instructions):
82 """Apply algebraic identities"""
83 result = []
84 for instr in instructions:
85 if isinstance(instr, TACBinOp):
86 l, op, r = instr.left, instr.op, instr.right
87 # x * 1 → x
88 if op == "*" and r == "1":
89 print(f" Alg: {l} * 1 {l}")
90 result.append(TACCopy(instr.result, l)); continue
91 # x + 0 → x
92 if op == "+" and r == "0":
93 print(f" Alg: {l} + 0 {l}")
94 result.append(TACCopy(instr.result, l)); continue
95 # x * 0 → 0
96 if op == "*" and r == "0":
97 print(f" Alg: {l} * 0 0")
98 result.append(TACCopy(instr.result, "0")); continue
99 # x * 2 → x << 1
100 if op == "*" and r == "2":
101 print(f" Alg: {l} * 2 {l} << 1")
102 result.append(TACBinOp(instr.result, l, "<<", "1")); continue
103 result.append(instr)
104 return result
105
106# ── Run optimization pipeline ──────────────────────────
107raw_tac = [
108 TACBinOp("t1", "2", "+", "3"), # constant fold → 5
109 TACBinOp("t2", "t1", "*", "1"), # algebraic simplification → t1
110 TACBinOp("t3", "t2", "+", "0"), # algebraic simplification → t2
111 TACCopy("result", "t3"),
112 TACReturn("result"),
113]
114
115print("Original TAC:")
116for i in raw_tac: print(i)
117
118print("
119Pass 1: Constant Folding")
120opt = constant_fold(raw_tac)
121
122print("
123Pass 2: Algebraic Simplification")
124opt = algebraic_simplification(opt)
125
126print("
127Pass 3: Constant Propagation")
128opt = constant_propagation(opt)
129
130print("
131Pass 4: Dead Code Elimination")
132opt = dead_code_elimination(opt)
133
134print("
135Optimized TAC:")
136for i in opt: print(i)
137
138# ── Python's own optimization ──────────────────────────
139import dis
140print("
141Python constant folding in action:")
142code1 = compile("x = 2 + 3", "<>", "eval")
143code2 = compile("x = 60*60*24", "<>", "eval")
144print(f" '2 + 3' constant {eval(code1)}")
145print(f" '60*60*24' constant {eval(code2)}")
146print(" Python folds these at compile time no runtime addition!")
← PREV4. Intermediate Code GenerationNEXT →6. Code Generation