COMPUTER ORGANIZATION & ARCHITECTURE / 7. PIPELINING

Pipelining — Instruction-Level Parallelism

Overlapping instruction execution — from 1 to 5 instructions per cycle


EXPLANATION

Pipelining is the single most important performance technique in CPU design. Instead of completing one instruction before starting the next, you overlap multiple instructions like an assembly line.

The 5-stage RISC pipeline (classic):
- IF  — Instruction Fetch: read instruction from memory at PC
- ID  — Instruction Decode: decode opcode, read registers, extend immediates
- EX  — Execute: ALU performs operation, compute branch/memory address
- MEM — Memory Access: read or write data memory (load/store only)
- WB  — Write Back: write result to register file

Without pipelining: each instruction takes 5 cycles. 1000 instructions = 5000 cycles.
With pipelining: after filling the pipeline, one instruction completes every cycle. 1000 instructions ≈ 1004 cycles (5 cycle fill + 999 cycles). Throughput = 5× better.

Pipeline Hazards — three types of problems:

1. Structural Hazard: two instructions need the same hardware resource in the same cycle.
   Example: only one memory port, but IF and MEM both need memory.
   Solution: separate instruction cache and data cache (Harvard architecture). Modern CPUs do this.

2. Data Hazard: instruction needs result from previous instruction not yet written back.
   Example: ADD R1, R2, R3 then SUB R4, R1, R5 — R1 isn't written until WB, but SUB needs it in ID.
   Types:
   • RAW (Read After Write): most common, also called "true dependency"
   • WAR (Write After Read): happens in out-of-order execution
   • WAW (Write After Write): also out-of-order
   Solutions:
   • Stalling (bubbles): insert NOPs, waste cycles. Simple but slow.
   • Forwarding (bypassing): route result from EX/MEM stage output directly back to EX input. Eliminates most RAW stalls without wasting cycles. Used in all real CPUs.
   • Out-of-order execution: reorder instructions to avoid hazards. Complex but powerful.

3. Control Hazard: branch instructions — we don't know next PC until EX stage.
   Problem: we've already fetched 2 more instructions into the pipeline. Wrong instructions!
   Solutions:
   • Flush: discard the 2 wrongly-fetched instructions (2 cycle penalty).
   • Branch prediction: guess which way the branch goes, speculatively execute. Modern CPUs predict with ~95%+ accuracy. Misprediction penalty: 15-20 cycles (emptying the deep pipeline).
   • Branch Delay Slot (MIPS): always execute the instruction after the branch (programmer fills it or assembler inserts NOP). Exposes pipeline to software.

Branch Prediction — how CPUs guess:
- Static prediction: always predict taken, or always not taken
- 1-bit predictor: remember last outcome
- 2-bit saturating counter: bimodal predictor — needs 2 misses to change prediction
- Tournament predictor: choose between local and global predictors
- Modern: neural branch predictors with 95%+ accuracy

DIAGRAM

WITHOUT PIPELINING (5 cycles per instruction):
  Cycle:   1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
  Instr1: IF ID EX MM WB
  Instr2:                IF ID EX MM WB
  Instr3:                               IF ID EX MM WB
  3 instructions = 15 cycles

  WITH PIPELINING (1 instruction per cycle after fill):
  Cycle:   1  2  3  4  5  6  7  8  9
  Instr1: IF ID EX MM WB
  Instr2:    IF ID EX MM WB
  Instr3:       IF ID EX MM WB
  Instr4:          IF ID EX MM WB
  Instr5:             IF ID EX MM WB
  5 instructions = 9 cycles  (vs 25 without)

  DATA HAZARD + FORWARDING:
  ADD R1, R2, R3   IF ID EX→─┐ MM WB
  SUB R4, R1, R5      IF ID ↑EX MM WB
                           └─ forwarded result (no stall!)

  BRANCH MISPREDICTION (15-cycle penalty on modern CPUs):
  BEQ R1, R2, target  IF ID EX ← branch resolved here
  wrong_instr1:           IF ID ← FLUSHED (wasted)
  wrong_instr2:              IF ← FLUSHED (wasted)
  correct_instr:                IF ID EX MM WB

CODE

PYTHON
1# Simulate a 5-stage pipeline with hazard detection
2
3class Pipeline:
4 STAGES = ["IF", "ID", "EX", "MEM", "WB"]
5
6 def __init__(self):
7 self.stages = [None] * 5 # one slot per stage
8 self.cycle = 0
9 self.stalls = 0
10 self.flushes = 0
11 self.completed = 0
12 self.log = []
13
14 def tick(self, new_instr=None, flush=False):
15 self.cycle += 1
16
17 if flush:
18 # Branch misprediction — flush IF and ID stages
19 self.stages[0] = None
20 self.stages[1] = None
21 self.flushes += 2
22
23 # Move instructions forward (WB first to avoid overwrite)
24 for i in range(4, 0, -1):
25 if self.stages[i-1] is not None:
26 if i == 4: # WB stage completes
27 self.completed += 1
28 self.stages[i] = self.stages[i-1]
29 else:
30 self.stages[i] = None
31
32 # Fetch new instruction
33 self.stages[0] = new_instr if not flush else None
34
35 state = " | ".join(
36 f"{self.STAGES[i]:>4}: {(self.stages[i] or '---'):>10}"
37 for i in range(5)
38 )
39 self.log.append(f"Cycle {self.cycle:>3}: {state}")
40
41def simulate_pipeline(instructions, hazard_stalls=None, branch_flush_at=None):
42 """
43 instructions: list of instruction names
44 hazard_stalls: set of instruction indices that cause a 1-cycle stall before them
45 branch_flush_at: cycle number where branch misprediction causes flush
46 """
47 pipe = Pipeline()
48 instr_queue = list(instructions)
49 idx = 0
50
51 for cycle in range(len(instructions) + 4 + (hazard_stalls and len(hazard_stalls) or 0) + 2):
52 stall = hazard_stalls and idx in hazard_stalls
53 flush = branch_flush_at and cycle == branch_flush_at
54
55 if stall and not flush:
56 pipe.tick(None) # insert bubble
57 pipe.stalls += 1
58 else:
59 next_instr = instr_queue[idx] if idx < len(instr_queue) else None
60 pipe.tick(next_instr, flush=flush)
61 if next_instr:
62 idx += 1
63
64 if pipe.completed >= len(instructions):
65 break
66
67 return pipe
68
69# ── Ideal pipeline (no hazards) ───────────────────────
70instrs = ["ADD R1,R2,R3", "SUB R4,R5,R6", "LW R7,100(R0)",
71 "AND R8,R1,R2", "OR R9,R3,R4"]
72
73print("IDEAL PIPELINE (no hazards):")
74pipe = simulate_pipeline(instrs)
75for line in pipe.log:
76 print(f" {line}")
77print(f" {pipe.completed} instructions in {pipe.cycle} cycles, {pipe.stalls} stalls
78")
79
80# ── With data hazard stalls ────────────────────────────
81print("WITH DATA HAZARD STALLS (no forwarding):")
82pipe2 = simulate_pipeline(instrs, hazard_stalls={1, 2})
83for line in pipe2.log:
84 print(f" {line}")
85print(f" {pipe2.completed} instr in {pipe2.cycle} cycles, {pipe2.stalls} stalls
86")
87
88# ── Branch prediction stats ───────────────────────────
89print("BRANCH PREDICTION SIMULATION:")
90import random
91random.seed(42)
92
93def simulate_branch_predictor(outcomes, strategy="2bit"):
94 state = 0 # 2-bit counter: 0=strong-not-taken, 3=strong-taken
95 hits = misses = 0
96 for taken in outcomes:
97 prediction = state >= 2 # predict taken if counter >= 2
98 correct = prediction == taken
99 if correct: hits += 1
100 else: misses += 1
101 # Update 2-bit saturating counter
102 if taken and state < 3: state += 1
103 elif not taken and state > 0: state -= 1
104 total = hits + misses
105 print(f" {strategy}: {hits}/{total} correct = {100*hits/total:.1f}% accuracy, {misses} mispredictions")
106
107# Loop branch (taken 99% of the time)
108loop_branches = [True] * 99 + [False] # loop runs 99 times then exits
109simulate_branch_predictor(loop_branches * 5, "2-bit (loop)")
110
111# Random branches
112random_branches = [random.choice([True, False]) for _ in range(500)]
113simulate_branch_predictor(random_branches, "2-bit (random)")
← PREV6. CPU Datapath & ControlNEXT →8. Cache Memory