Pipeline -- How to Make CPUs Faster
Without increasing clock frequency, can CPUs still become faster? The answer is yes. Pipeline technology is one of the most important methods. This lecture uses Python code and Gantt-chart-style visual output to show how pipelining greatly improves instruction execution efficiency.
Analogy from daily life: car wash assembly line
Imagine you run a car wash. There are two ways to operate:
Method 1: Single-person all-in-one (no pipeline)
One worker handles all washing steps for one car from start to finish—rinse, foam, scrub, dry. Only after finishing one car can they start the next. Each car takes 40 minutes, so only 1.5 cars can be washed per hour.
Method 2: Pipeline division of labor (with pipeline)
Four workers each handle one step—rinse, foam, scrub, dry. After the first car enters the rinse stage, the rinser can start rinsing the second car, while the foamer is foaming the first car. All four people are busy at the same time, and a car is completed every 10 minutes.
The second method does not make any single step faster (rinsing still takes 10 minutes), but by overlapping the steps of different cars, the overall output rate is greatly improved.
This is the core idea of the CPU pipeline: overlapping the execution of different stages of different instructions.
Serial execution vs. pipelined execution
The problem of serial execution
Reviewing Lecture 12, each instruction goes through four stages: Fetch (IF), Decode (ID), Execute (EX), Write-back (WB).
InSerial execution (no pipeline)In this mode, the CPU must fully execute all four stages of the current instruction before it can start fetching the next instruction.
周期: 1 2 3 4 5 6 7 8 9 10 11 12 I1: [IF] [ID] [EX] [WB] I2: [IF] [ID] [EX] [WB] I3: [IF] [ID] [EX] [WB] 3 条指令 = 12 个周期
Advantages of pipelined execution
InPipeline executionIn this mode, once instruction I1 completes the fetch stage (IF), the CPU's fetch unit becomes idle. Rather than letting it sit idle, you can immediately start fetching the next instruction I2.
Similarly, when I1 completes decode (ID) and enters the execute stage, the decode unit begins processing I2, while the fetch unit begins processing I3.
周期: 1 2 3 4 5 6 7 I1: [IF] [ID] [EX] [WB] I2: [IF] [ID] [EX] [WB] I3: [IF] [ID] [EX] [WB] 3 条指令 = 6 个周期(比串行快了一倍!)
Note the key numbers:
- No pipeline: n instructions = 4n cycles
- With pipeline: n instructions = 4 + (n-1) = n + 3 cycles (i.e., after the pipeline is filled, one instruction completes each cycle)
When n is large, the speedup from pipelining approaches 4 (the number of pipeline stages).
Pipelining does not make a single instruction execute faster (each instruction still needs 4 cycles), but it greatly improves "throughput"—the number of instructions completed per unit time.
Pipeline "hazard" problem
Pipelining is good, but it does not come without cost. There are three typical "hazards" that can stall the pipeline:
| Hazard types | English | Problem description | life analogy |
|---|---|---|---|
| Structural hazard | Structural Hazard | Two stages contend for the same hardware resource (e.g., both need to access memory) | In the car wash, there is only one high-pressure water gun, and both the initial rinse and final rinse need to use it, so they have to queue. |
| Data hazard | Data Hazard | The next instruction depends on the result of the previous instruction, but the result hasn't been written back yet | To make braised pork, you must first cut the meat before putting it in the pot. If the meat cutter has not finished, the cook can only wait. |
| Control hazard | Control Hazard | When encountering a branch instruction, you do not know which instruction to fetch next. | The menu says, "If the guest orders spicy food, switch to another recipe." Before the guest decides, the kitchen does not know what to prepare. |
Modern CPUs use various techniques to mitigate these hazards:
- Data Bypass (Data Forwarding): Without waiting for the result to be written back to the register, directly "steal" the data from the ALU output for the next instruction to use
- Branch PredictionBranch prediction: guess the branch direction and fetch ahead. If guessed correctly, you gain; if incorrectly, flush the pipeline and start over.
- Out-of-Order ExecutionOut-of-order execution: do not follow program order; instead, "execute whichever instruction's data is ready first."
Interactive demo: Pipeline Simulator (Gantt chart style)
The Python code below simulates instruction execution in both pipelined and non-pipelined modes, and prints output in Gantt chart style, allowing you to intuitively compare the differences between the two approaches.
Example
CPU Pipeline Simulator (example demo)
Comparison: Non-pipelined (Serial) vs. Pipelined (Parallel Overlap)
Print the execution timeline of each instruction's stages in Gantt chart style
"""
# Define instruction sequence and abbreviations for each stage
INSTRUCTIONS = ["I1", "I2", "I3", "I4", "I5", "I6", "I7", "I8"]
STAGES = ["IF", "ID", "EX", "WB"] # Fetch, decode, execute, writeback
STAGE_NAMES = {"IF": "Fetch", "ID": "Decode", "EX": "Execute", "WB": "Writeback"}
def simulate_serial(instructions, stages):
"""
No-pipeline mode: after each instruction has fully executed its 4 stages, the next one starts.
Return: (total cycles, 2D timeline array)
Timeline format: time_grid[cycle][stage_slot] = name of the currently executing instruction or None
"""
num_instr = len(instructions)
num_stages = len(stages)
total_cycles = num_instr * num_stages # n instructions × 4 stages
# time_grid[cycle][stage_slot] → which instruction is in this stage at this cycle
time_grid = [[None for _ in range(num_instr)] for _ in range(total_cycles)]
for i, instr in enumerate(instructions):
base_cycle = i * num_stages
for s_idx, stage in enumerate(stages):
cycle = base_cycle + s_idx
time_grid[cycle][i] = f"{instr}({stage})"
return total_cycles, time_grid
def simulate_pipeline(instructions, stages):
"""
Pipeline mode: different stages of different instructions can overlap in execution.
Return: (total cycles, 2D timeline array)
"""
num_instr = len(instructions)
num_stages = len(stages)
# Total pipeline cycles = number of stages + (number of instructions - 1)
total_cycles = num_stages + (num_instr - 1)
time_grid = [[None for _ in range(num_instr)] for _ in range(total_cycles)]
for i, instr in enumerate(instructions):
# Stage s of instruction i executes in cycle (i + s)
for s_idx, stage in enumerate(stages):
cycle = i + s_idx
if cycle < total_cycles:
time_grid[cycle][i] = f"{instr}({stage})"
return total_cycles, time_grid
def print_gantt(instructions, stages, time_grid, total_cycles, mode_name):
"""Print timeline in Gantt chart style"""
num_instr = len(instructions)
# Table header
header = f{'cycle':>4}
for instr in instructions:
header += f" {instr:^10}"
print(header)
print("-" * (6 + 12 * num_instr))
for cycle in range(total_cycles):
row = f"{cycle+1:>4} "
for i in range(num_instr):
cell = time_grid[cycle][i]
if cell:
# Assign different labels to different stages
if "(IF)" in cell:
row += f" [\033[94mIF\033[0m] " # Blue
elif "(ID)" in cell:
row += f" [\033[93mID\033[0m] " # Yellow
elif "(EX)" in cell:
row += f" [\033[92mEX\033[0m] " # Green
elif "(WB)" in cell:
row += f" [\033[91mWB\033[0m] " # Red
else:
row += f" {'':10}"
print(row)
print("-" * (6 + 12 * num_instr))
print(fTotal cycles: {total_cycles})
return total_cycles
def print_simple_gantt(instructions, stages, time_grid, total_cycles, mode_name):
Simple Gantt chart (no ANSI colors, compatible with all terminals)
num_instr = len(instructions)
print(f"\n{'='*70}")
print(f" {mode_name}")
print(f"{'='*70}")
# Table header
header = f{'cycle':>5} |
for instr in instructions:
header += f" {instr:^9} |"
print(header)
print("-" * (7 + 11 * num_instr))
for cycle in range(total_cycles):
row = f" {cycle+1:>3} |"
for i in range(num_instr):
cell = time_grid[cycle][i]
if cell:
# Extract stage identifiers
for stage in stages:
if f"({stage})" in cell:
row += f" [{stage}] |"
break
else:
row += f" |"
print(row)
print("-" * (7 + 11 * num_instr))
print(fTotal cycles: {total_cycles})
return total_cycles
def calculate_metrics(serial_cycles, pipeline_cycles, num_instr, num_stages):
"""Compute performance metrics"""
speedup = serial_cycles / pipeline_cycles
ideal_speedup = num_stages # Ideal speedup = number of pipeline stages
# Throughput: number of instructions completed per cycle
serial_throughput = num_instr / serial_cycles
pipeline_throughput = num_instr / pipeline_cycles
# Pipeline efficiency
# Total slots occupied by all instructions
used_slots = num_instr * num_stages
total_slots = num_instr * pipeline_cycles
efficiency = (used_slots / total_slots) * 100 if total_slots > 0 else 0
return {
'speedup': speedup,
'ideal_speedup': ideal_speedup,
'serial_throughput': serial_throughput,
'pipeline_throughput': pipeline_throughput,
'efficiency': efficiency,
'serial_cycles': serial_cycles,
'pipeline_cycles': pipeline_cycles,
}
# ===== Main program =====
print("=" * 70)
print(CPU Pipeline Simulator — Gantt Chart Style Comparison (example demo))
print("=" * 70)
print(fInstruction sequence: {', '.join(INSTRUCTIONS[:8])})
print(fPipeline stages: 4 (IF=Instruction Fetch, ID=Instruction Decode, EX=Execute, WB=Write Back))
print()
No pipeline (serial)
serial_cycles, serial_grid = simulate_serial(INSTRUCTIONS, STAGES)
print_simple_gantt(INSTRUCTIONS, STAGES, serial_grid, serial_cycles, "Mode A: Non-pipelined (serial execution)")
# 2. With Pipelining
pipeline_cycles, pipeline_grid = simulate_pipeline(INSTRUCTIONS, STAGES)
print_simple_gantt(INSTRUCTIONS, STAGES, pipeline_grid, pipeline_cycles, "Mode B: With pipeline (4-stage pipeline)")
# 3. Performance Comparison
metrics = calculate_metrics(serial_cycles, pipeline_cycles, len(INSTRUCTIONS), len(STAGES))
print(f"\n{'='*70}")
print(f" Performance Comparison Analysis")
print(f"{'='*70}")
print(fInstruction count: {len(INSTRUCTIONS)})
print(fPipeline stages: {len(STAGES)})
print(f"")
print(fTotal cycles without pipeline: {metrics['serial_cycles']} cycles)
print(fPipelined total cycles: {metrics['pipeline_cycles']} cycles)
print(f"")
print(fActual speedup: {metrics['speedup']:.2f}x)
print(fIdeal speedup: {metrics['ideal_speedup']}x (equals the number of pipeline stages))
print(f"")
print(f" stringlineThroughput: {metrics['serial_throughput']:.3f} Directive/weekperiod")
print(f" Pipeline throughput: {metrics['pipeline_throughput']:.3f} instructions/cycle")
print(fPipeline efficiency: {metrics['efficiency']:.1f}%)
print(f"")
print(fConclusion: After using a {len(STAGES)}-stage pipeline, the same 8 instructions)
print(f" from {metrics['serial_cycles']} weekperiod缩短to {metrics['pipeline_cycles']} weekperiod")
print(fSped up {metrics['speedup']:.1f} times!)
print(f"")
print(fNote: The pipeline has 'fill' and 'drain' overhead.)
print(fIn the first {len(STAGES)-1} cycles, the pipeline is not full (filling period),)
print(fIn the last {len(STAGES)-1} cycles, the pipeline gradually drains.)
print(fThe more instructions, the smaller the relative overhead of filling/draining,)
print(fThe closer the speedup ratio is to the ideal value {len(STAGES)}x.)
# 4. Extended Demonstration: Speedup Ratio Trend under Different Instruction Counts
print(f"\n{'='*70}")
print(f" Extended analysis: speedup ratio for different instruction counts")
print(f"{'='*70}")
print(f" {'Directivenumber':>6} | {'stringlineweekperiod':>8} | {'Pipelineweekperiod':>10} | {'Speedup':>8} | {'Speedup/manage想Value':>14}")
print(f" {'-'*6}-+-{'-'*8}-+-{'-'*10}-+-{'-'*8}-+-{'-'*14}")
for n in [1, 2, 4, 8, 16, 32, 64, 128, 256]:
test_instrs = [f"I{i}" for i in range(1, n+1)]
s_cycles, _ = simulate_serial(test_instrs, STAGES)
p_cycles, _ = simulate_pipeline(test_instrs, STAGES)
sp = s_cycles / p_cycles
ideal = len(STAGES)
ratio = (sp / ideal) * 100
print(f" {n:>6} | {s_cycles:>8} | {p_cycles:>10} | {sp:>7.2f}x | {ratio:>13.1f}%")
print()
print(fConclusion: The more instructions, the closer the speedup approaches the ideal value {len(STAGES)}x (i.e., the number of pipeline stages).)
print(fWhen there is only 1 instruction, the pipeline has no speedup effect at all.)
print(fBut modern programs usually contain millions of instructions, making pipeline efficiency close to 100%.)
Run the code above, and you will see:
- Gantt chart comparisonObservation: In non-pipelined mode, each instruction occupies 4 consecutive cycles, with large idle gaps between instructions. In pipelined mode, the stages are tightly arranged with almost no idle cycles.
- Speedup trendObservation: The more instructions there are, the closer the speedup approaches the pipeline depth (here, 4). But with only 1 instruction, pipelining provides no speedup at all, because the fill and drain overhead accounts for 100%.
- ThroughputObservation: After the pipeline is "filled" (starting from the 4th cycle), one instruction is completed every cycle. This is the core value of pipelining.
Interactive demo: pipeline Gantt chart comparison
Below, we use a Chart.js horizontal stacked bar chart to draw Gantt charts, directly comparingWithout pipeline (serial)andWith pipeline (4-stage)the time distribution of each stage for 4 instructions under the two modes. Each color block represents a stage (IF=Fetch, ID=Decode, EX=Execute, WB=Write-back), and the x-axis is clock cycles.
Pipeline Gantt chart comparison (example demo)
No pipeline (serial execution) — 16 cycles total
With pipeline (4-stage pipeline) — 7 cycles total
Non-pipelined mode: each instruction exclusively occupies 4 consecutive cycles, instructions are completely serial, and 4 instructions take 16 cycles.
With pipeline mode: overlapping execution of different stages of different instructions, 4 instructions only need 7 cycles (= 4 + (4-1)).
After the pipeline is "filled" (starting from cycle 4), one instruction completes each cycle — this is why throughput greatly improves.
Comparison of pipeline stage depths
The "depth" of the pipeline determines the theoretical upper limit of speedup, but the deeper the pipeline, the more severe the hazard problems:
| CPU model | Number of pipeline stages | Features |
|---|---|---|
| Classic RISC (e.g., MIPS) | 5 levels | Simple and efficient, suitable for teaching. Classic 5-stage pipeline: IF, ID, EX, MEM, WB |
| Intel Pentium 4(NetBurst) | 20~31 stages | Ultra-deep pipeline, pursuing high frequency. But branch prediction failures are costly, and it was eventually abandoned. |
| Intel Core series (since 2006) | 14~19 stages | Shorter pipeline + high IPC, balancing frequency and efficiency. |
| Apple M1(2020) | 8-stage (decode) | Ultra-wide architecture (8-wide issue), low frequency, high IPC, excellent power efficiency. |
It can be seen that pipeline depth is a trade-off: too many stages make the cost of a single branch misprediction too high; too few stages prevent frequency from going high enough.
The mainstream trend in modern CPU design is“wide and shallow”— moderate pipeline depth + ultra-wide issue width (able to start executing multiple instructions per cycle), rather than blindly pursuing more stages.
other extensions