GATE/Compiler Design/Code Optimisation & Code Generation
Medium16 min readCompiler Design

Code Optimisation & Code Generation

Code optimisation improves intermediate code without changing semantics. Code generation translates to target machine code. GATE tests common optimisation techniques and their applicability.

Key Points

  • ·Machine-independent optimisations: performed on IR before target selection
  • ·Constant folding: evaluate constant expressions at compile time (3*4 → 12)
  • ·Constant propagation: replace variable with known constant value
  • ·Dead code elimination: remove code that has no effect on output
  • ·Common subexpression elimination (CSE): compute once, reuse (uses DAG)
  • ·Loop optimisations: loop-invariant code motion, induction variable elimination, loop unrolling
  • ·Loop-invariant code motion: move computations that don't change inside loop to outside
  • ·Induction variable: variable that changes by fixed amount each iteration; eliminate derived IVs
  • ·Peephole optimisation: local, machine-dependent; replace instruction sequences with shorter equivalents

What is Code Optimisation?

Analogy: Writing a first draft of an essay (intermediate code) and then editing it to be shorter and more impactful without changing the meaning (optimisation). The edited version says the same thing but better.

Key constraint: Optimisation must NEVER change the output of the program!


Optimisation Levels

Machine-independent (on Intermediate Representation):
  Applied before knowing the target CPU.
  Works on abstract TAC or similar IR.

  Examples: constant folding, CSE, dead code elimination, loop optimisations

Machine-dependent (after code selection):
  Applied to target-specific instructions.
  Knows register count, instruction latencies, etc.

  Examples: peephole, register allocation, instruction scheduling

Constant Folding — Compute at Compile Time

If an expression involves only constants, evaluate it now — no need to compute at runtime.

Before:            After:
  x = 3 * 4 + 1    →    x = 13
  y = 2 ** 10      →    y = 1024
  if (5 > 3) ...   →    always true → can eliminate the if

Constant Propagation — Substitute Known Values

If a variable always holds a known constant at some point, replace uses with the constant.

Before:
  x = 5          ← x is always 5 here
  y = x + 3      ← substitute: y = 5 + 3
  z = y * 2      ← after folding: y = 8, so z = 8 * 2

After constant folding too:
  x = 5
  y = 8
  z = 16

Now x and y may be dead code if not used elsewhere!

Dead Code Elimination

Remove code whose result is never used.

Before:
  x = 5 * 6    ← x never read again → DEAD
  y = 3 + 4    ← y never read again → DEAD
  z = a + b    ← z is used → KEEP
  return z

After:
  z = a + b
  return z

How to detect: LIVE VARIABLE ANALYSIS
  A variable is LIVE at a point if its current value may be READ later.
  A variable is DEAD if its value is NEVER read after its last write.

Common Subexpression Elimination (CSE)

If the same expression is computed multiple times with the same operands (not modified in between), compute it once.

Before:
  t1 = a + b
  t2 = a + b    ← same expression!
  x = t1 * t2

After CSE:
  t1 = a + b
  x = t1 * t1   ← reuse t1

But ONLY valid if a and b are NOT modified between the two computations!
CSE uses value numbering or DAG to detect same expressions.

Loop Optimisations — The Biggest Wins

Loops execute many times — even small improvements compound!

Loop-Invariant Code Motion (LICM)

Move code OUT of loop if it produces the same result every iteration.

Before:
  for i in range(1000):
      limit = n * n      ← n does not change in loop → INVARIANT
      if i < limit:
          A[i] = A[i] + 1

After LICM:
  limit = n * n          ← moved OUTSIDE the loop
  for i in range(1000):
      if i < limit:
          A[i] = A[i] + 1

Saves: 1000 multiplications → 1 multiplication

Strength Reduction — Replace Expensive with Cheap

Before:
  for i in range(n):
      j = i * 4    ← multiplication every iteration

After strength reduction:
  j = 0
  for i in range(n):
      // use j
      j = j + 4   ← addition (much cheaper than multiplication)

Multiplications → Additions: replace j = i*4 with j += 4

Loop Unrolling — Reduce Loop Overhead

Before (100 iterations, checking i<100 each time):
  for i in range(0, 100, 1):
      A[i] = B[i] + 1

After unrolling by 4 (25 iterations, check 25 times):
  for i in range(0, 100, 4):
      A[i]   = B[i]   + 1
      A[i+1] = B[i+1] + 1
      A[i+2] = B[i+2] + 1
      A[i+3] = B[i+3] + 1

Saves: 75 branch instructions and loop counter updates
Enables: more instruction-level parallelism

Peephole Optimisation — Local Cleanup

Slide a small window over the generated code and replace patterns with equivalents.

Pattern → Replacement:

ADD R1, #0        → (remove: adding 0 does nothing)
MUL R1, #1        → (remove: multiply by 1 does nothing)
MOV R1, R2        →
MOV R2, R1        → keep only first MOV (second is redundant copy-back)
JMP L             →
L:   ...          → (remove JMP: already at L!)
x = x + 1        →
y = x + 1        → x = x + 1; y = x  (avoid recomputing x+1)
SHL R1, 1         ← replace MUL R1, #2 (shift is faster than multiply)

Register Allocation — Graph Colouring

Problem: IR has unlimited temporaries; hardware has ~16-32 registers
         Variables that cannot fit in registers must be "spilled" to memory

Solution: Build INTERFERENCE GRAPH
  Nodes = variables (temporaries)
  Edge between X and Y if both are LIVE at the same time (cannot share register)

k-register allocation = k-colouring of this graph
  If graph is k-colourable: every variable gets a register
  If not: spill some variables (store/load from memory)

Graph k-colouring is NP-complete → practical heuristics used
  Chaitin's algorithm: iteratively remove nodes with degree < k

Quick Check

Q1. Apply constant folding and propagation to: x=2; y=x*3; z=y+1; print(z)

x = 2 (constant)
y = x*3 = 2*3 = 6 (propagate x, fold multiply)
z = y+1 = 6+1 = 7 (propagate y, fold add)
print(7) (propagate z)

After dead code elimination: x, y, z never read after last assignment
Optimised: print(7)

Q2. What is the condition for loop-invariant code motion to be safe? Answer: The computation must: (1) not depend on any variable that changes in the loop, (2) produce the same result every iteration, (3) be safe to execute (no exceptions) — or guaranteed to execute in the loop (to avoid changing program behaviour for loops that execute zero times).

Q3. How does peephole differ from other optimisations? Answer: Peephole is LOCAL (looks at a small window of 2-5 instructions) and MACHINE-DEPENDENT (knows target instructions). Other optimisations like CSE, LICM work on abstract IR and are machine-independent.

Key Formulas

  • Strength reduction: Replace t = i×c (multiply) with t += c (add) each iteration
  • Loop unrolling: Reduce loop iterations by factor k, replicate body k times

GATE Exam Tips

  • LICM condition: computation must not depend on any variable modified in the loop.
  • Constant folding at compile time; constant propagation substitutes values into expressions.
  • Peephole is local (small window); LICM, CSE, DCE are global (whole function).
  • GATE applies optimisations in sequence: fold → propagate → DCE → CSE → loop opts.

Finished reading this topic?

Mark it complete to track your study progress.