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 typesEnglishProblem descriptionlife analogy
Structural hazardStructural HazardTwo 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 hazardData HazardThe next instruction depends on the result of the previous instruction, but the result hasn't been written back yetTo 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 hazardControl HazardWhen 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)

IF fetch
ID decode
EX execute
WB write back

No pipeline (serial execution) — 16 cycles total

With pipeline (4-stage pipeline) — 7 cycles total

Total cycles without pipeline
16
Total cycles with pipeline
7
Speedup
2.29x
Throughput improvement
129%
Key observation:
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 modelNumber of pipeline stagesFeatures
Classic RISC (e.g., MIPS)5 levelsSimple and efficient, suitable for teaching. Classic 5-stage pipeline: IF, ID, EX, MEM, WB
Intel Pentium 4(NetBurst)20~31 stagesUltra-deep pipeline, pursuing high frequency. But branch prediction failures are costly, and it was eventually abandoned.
Intel Core series (since 2006)14~19 stagesShorter 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