Pipelining: Five Stages, Three Strategies

How a five-stage in-order core overlaps instructions, and what it costs

CS 4250 — Computer Architecture

2026-10-21

Roadmap

The machine is the one in the chip: five stations, in order, one instruction per cycle. The questions are the ones you would ask of any such machine.

  1. The five-stage machine — what the stations are, and what the registers between them must hold
  2. Data hazards — the register file hands back a stale value; stall, bypass, or speculate
  3. Control hazards — NextPC is not known until the branch is decided
  4. Exceptions and interrupts — control transfers you cannot predict, and why the machine waits until retire to take them
  5. What the C906 really does — and what changes when the pipeline gets deeper
  6. Synthesis — CPI accounting, and quoted versus derived

Part 0 — Framing

The Machine in the Chip

Two caches, not one. IF and MEM each have their own L1, so they never contend for a port. The textbook machine has a single memory and a structural hazard; ours cannot have one. That is an architectural decision, paid for in silicon.

One Instruction Per Clock

The datapath you just saw is the datapath. The only question is whether there is a register between the stations.

With no registers we have the single-cycle machine: one clock has to cover all five stations, so every instruction pays for the whole datapath.

\[t_{CLK} = t_{IF} + t_{RF} + t_{ALU} + t_{MEM} + t_{WB}\]

Put a register at each cut and it becomes the pipelined machine: the clock only has to cover the slowest piece — the largest of the five, plus the register and skew overhead.

\[t_{CLK} = \big(\text{slowest stage}\big) + t_{skew}\]

Single cycle Pipelined
Clock period sum of five stages slowest of five stages
CPI 1 ≈ 1
Instructions in flight 1 5
Speed 1× ≈ 4×

It does not make an instruction faster. It makes the machine faster, by having five different instructions in it at once.

What We Are Leaving Out

We keep We ignore, for now
five stations, strictly in order the FPU and VPU — separately pipelined, multi-cycle
one instruction issued per cycle two-instruction fetch (the IFU fetches 2, issues 1)
4-byte instructions, PC ← PC + 4 compressed (2-byte) instructions
32 registers, x0 hardwired to zero the MMU and TLB — assume no translation miss
separate L1 instruction and data caches the AXI bus and everything behind it
branch prediction exists how it predicts — that is Part III

Abstraction is a promise about which complications we will not have. Every row on the right is a real complication in a real core. We add them back one at a time.

Part I — The Five-Stage Machine

One Instruction, Five Stages

ld x5, 8(x10) — the one instruction that touches all five stations.

Station What happens State touched
IF fetch the word at PC; PC ← PC + 4 I-cache, PC
ID decode; read x10; immediate = 8 register file (2 reads)
EX address = x10 + 8 ALU
MEM read the D-cache there D-cache
WB write into x5 register file (1 write)

rd — the destination, x5 — is read out of the instruction word in ID and carried all the way to WB, because by then the instruction word itself is long gone.

Every instruction occupies all five stations, even where a station has nothing to do: add x6, x11, x12 walks through MEM and does nothing at all.

Read the register file in ID, write it at the end of WB. Everything in Parts II–IV follows from that one sentence.

The Only New State

Register Must carry
IF/ID instruction word, PC + 4
ID/EX A, B, immediate, rs1, rs2, rd, control
EX/MEM ALU result Y, B (store data), rd, control
MEM/WB memory data RD, ALU result Y, rd, control

The datapath is the single-cycle datapath. Four registers, each holding exactly what the next station needs to know.

Five Instructions at Once

  • Six instructions, ten cycles: CPI = 10/6 = 1.67
  • In general, n instructions through k stations: n + k − 1 cycles, so CPI = 1 + (k−1)/n
  • The +4 is fill and drain — the first overhead, and the smallest

Part II — Data Hazards

Six instructions with six different destinations ran at one per clock. Change one register number.

The Program

ld   x5, 8(x10)
add  x6, x11, x12      # writes x6
sub  x7, x6, x14       # reads  x6     <-- the one register changed
and  x8, x15, x16
or   x9, x17, x18
xor  x19, x20, x21

The six instructions from the last slide, with one register changed: sub used to read x13, and now takes its first source from add’s destination.

The program is legal, and the machine will not complain.

So: which cycle does sub read x6, and which cycle does add write it?

One Register, Two Instructions

sub reads x6 in cycle 4. add does not write x6 until the end of cycle 6.

So sub reads whatever x6 held before the add. The machine has not slowed down — it has computed the wrong answer, at full speed.

Where the Value Is

The value is not missing. It is in a pipeline register, one station ahead of where sub is looking. A result exists in the machine for two cycles before it exists in the register file.

Three ways to fix that, and every processor uses some combination:

  • stall — make sub wait until the value reaches the register file
  • bypass — hand sub the value from the pipeline register, as soon as it exists
  • speculate — guess the value, and check later

Stall

1 2 3 4 5 6 7 8 9 10 11 12
IF ld x5 add x6 sub x7 and x8 and x8 and x8 or x9 xor x19
ID ld x5 add x6 sub x7 sub x7 sub x7 and x8 or x9 xor x19
EX ld x5 add x6 NOP NOP sub x7 and x8 or x9 xor x19
MEM ld x5 add x6 NOP NOP sub x7 and x8 or x9 xor x19
WB ld x5 add x6 NOP NOP sub x7 and x8 or x9 xor x19

sub’s ID stage is frozen for two cycles, and a NOP is injected into EX each time. The bubbles then travel down the machine on their own.

Six instructions now take 12 cycles instead of 10 — CPI 2.0 instead of 1.67.

The Stall Logic

The hardware has to detect the hazard before it can repair it.

Compare With Why
rs1, rs2 of the instruction in ID rd of the instruction in EX its result is one stage away
rd of the instruction in MEM its result is two stages away
rd of the instruction in WB safe: the write happens before the read in that cycle

If any comparison matches, and the destination is not x0, assert STALL.

STALL = 1 Effect
freeze PC do not fetch
freeze IF/ID keep the stalled instruction in ID
clear the control bits into ID/EX inject a NOP into EX

Bypass

The value sub needs already exists — in EX/MEM, one stage ahead. Wire it round to the ALU input instead of waiting for it to reach the register file.

add’s result is in EX/MEM during cycle 5, which is exactly when sub is in EX. Zero stalls.

What Bypassing Costs

  • Wires. Every ALU input now has three sources, and the wires have to reach across the datapath. They fan out, they are long, and they carry 64 bits each.
  • Muxes. Two wide muxes in front of the ALU, each with a compare and a priority encoder deciding the select.
  • A longer EX stage. The mux delay sits inside the critical path, so bypassing can cost clock frequency — the very thing the pipeline was built to buy.
  • A design choice, not a correctness one. How many bypass paths to build is a CPI-versus-silicon trade. Fewer bypasses means more stalls, and the machine is still correct.
Producer → consumer distance Where the value is Path needed
1 instruction (adjacent) EX/MEM one bypass
2 instructions MEM/WB a second bypass
3 or more register file none — the write is visible

Load-to-Use

ld produces x5 at the end of MEM. add needs it at the start of EX. Both are cycle 4 — the value is needed at the moment it is being produced.

One stall, even with full bypassing. This is the case forwarding cannot remove, and the reason our core needs both mechanisms.

What We Bought

Stall Bypass
Paid in cycles — CPI goes up silicon — wires, muxes, clock
Correctness always always
Fails on nothing load-to-use, which still needs one stall
Logic a comparator bank and a freeze a forwarding network and a priority rule
Our core yes, for loads yes, in the integer unit

More stages means more frequent hazards: a deeper pipeline lowers \(t_{CLK}\) and raises CPI. The two effects pull against each other, and finding the balance is the whole art of pipeline design.

The Compiler Can Help

The load-to-use stall is a property of the program order, and the compiler chooses that order.

Before — one stall

ld   x5, 0(x10)
add  x6, x5, x1

After — none

ld   x5, 0(x10)
and  x7, x8, x9      # independent
add  x6, x5, x1

Reordering works when independent instructions exist to fill the gap. It cannot create them.

“I’m altering the ISA. Pray I do not alter it further.”

The 1990s answer was a delay slot: the instruction after a load executes unconditionally, and the compiler fills it. It works, it is in MIPS and SPARC, and it is a trap — ISAs outlive implementations. The slot is a burden forever, for a pipeline that will be replaced.

Part III — Control Hazards

A data hazard means the value is not ready yet. A control hazard means you do not know what to fetch.

The Branch

loop:  ld    x5, 0(x10)
       addi  x10, x10, 8
       bne   x10, x11, loop

bne is the first instruction in this deck whose next PC is not PC + 4. IF must choose a next address this cycle, and there are two candidates.

This loop has no data hazards: ld reads x10 before addi writes it, and addi → bne is adjacent and covered by the EX/MEM bypass. Every cycle it loses from here is a control cycle.

Both inputs to the decision are already in ID — the register file is read there, and the immediate is in the instruction word. The information exists in cycle 2. The comparison hardware is in the ALU, in EX.

The Shadow

1 2 3 4 5 6
IF bne X Y ld addi bne
ID bne X NOP ld addi
EX bne NOP NOP ld
MEM bne NOP NOP
WB bne NOP

bne is checked in EX, at the end of cycle 3. Two instructions have been fetched behind it by then — X and Y, the code after the loop — and both are wrong.

Penalty: 2 cycles. The first useful instruction enters IF in cycle 4 instead of cycle 2. Not 1, and not 3: the penalty is the number of instructions fetched between the branch and its check.

Stall Until We Know

1 2 3 4 5 6
IF bne held held ld addi bne
ID bne NOP NOP ld addi
EX bne NOP NOP ld
MEM bne NOP NOP
WB bne NOP

The fix that is never wrong: hold IF for the two cycles the branch is in flight, and fetch nothing at all. No wrong instruction is ever created, so nothing has to be killed.

Two cycles on every branch, taken or not. The loop runs 3 + 2 = 5 cycles per iteration: CPI 1.67.

A machine that stalls is a machine that is wrong every time, and pays for it every time. Guessing “not taken” costs the same two cycles but only on the taken branches — so guessing is never worse than waiting, and it costs nothing to try.

Guess, and Be Right About Loops

The cheapest predictor is a rule with no state in it:

The branch goes Predict Because
backward (target behind the PC) taken it is a loop, and loops repeat
forward (target ahead of the PC) not taken it is an if, and the if body is usually skipped

The sign of the offset is in the instruction word we just fetched, so the fetch stage can apply this rule without decoding anything, and the target is PC + imm — also in that word.

The loop now runs at 3 cycles per iteration: CPI 1.00, with one miss per execution of the loop, on the iteration that falls out.

But the accuracy is fixed. A forward branch that is taken nine times in ten is predicted wrong ten times in ten. The rule cannot learn, and it cannot tell one loop from another.

This is the one program where the free rule looks as good as an expensive one: backward-taken is right about loops by construction. It has no way to be wrong about our loop, and no way to be right about the branches it was not designed for.

The Predictor in the Fetch Stage

  • BHT — direction. 8 or 16 Kb, GSHARE, one branch per cycle.
  • BTB — target. 16 entries, fully associative, indexed by part of the PC.
  • RAS — return addresses. 4 levels of nesting; jal and jalr push, jalr pops.

Every table is indexed by PC, so the answer arrives before the instruction is decoded. That is why a correct prediction costs zero cycles and not one: IF is not waiting for ID to tell it where to go.

GSHARE — index the table with PC bits XOR’d against a shift register of recent branch outcomes. The table then learns patterns per branch, not just a per-branch bias.

One Cycle

The penalty is the number of instructions fetched between the branch and its check. So the only way to shrink it is to check earlier.

Branch checked in Wrong instructions in flight Penalty What it costs to build
EX — the ALU compares 2 2 cycles nothing; this is our machine
ID — a comparator, and an adder for the target 1 1 cycle hardware in a stage that had neither
IF — before decode 0 — only possible as a guess, which is the BHT

BJU 单周期内完成分支预测错误处理

The branch unit completes mispredict handling in one cycle. — C906 manual, §2.2.3

One cycle, not two. The loop runs 3 + 1 = 4 cycles per iteration: CPI 1.33, against 1.67 for a machine that checks in EX.

That is not what a textbook five-stage machine does, and it is not free: a comparator and a target adder that ID did not have, plus a flush that is one cycle wide. It is a deliberate purchase of CPI with silicon — the same trade as bypassing, spent on a different hazard.

What We Bought

Stall Static predict Dynamic predict The C906
Paid in cycles nothing a BHT, a BTB, a RAS all three,
plus a comparator
Wrong when never often sometimes sometimes
Cost per branch 2, every time 2 when wrong 2 when wrong 1 when wrong
Our loop 5 cycles · CPI 1.67 3 · 1.00 3 · 1.00 4 · 1.33

The ISA fix we did not take. The 1990s answer was the branch delay slot: the instruction after a branch executes unconditionally, and the compiler fills it. Same trap as the load delay slot in Part II, for the same reason — an ISA outlives every pipeline built to run it. RISC-V has neither.

A branch is an instruction that changes the PC on purpose. Part IV is about the ones that change it without asking — and about the guarantee the machine owes when it cancels an instruction: that the cancelled instruction never happened.

Part IV — Exceptions and Interrupts

A branch changes the PC on purpose. An exception changes it without asking — and then has to put everything back.

The Unasked Branch

    addi x10, x0, 1      # fd = 1, stdout
    addi x17, x0, 64     # syscall 64: write
    ecall                # the PC leaves the program here
    addi x10, x0, 0      # ... and must come back to exactly here

ecall is a control transfer, like a branch. Three things about it are different:

A branch A trap
Where it goes PC + imm — in the instruction mtvec — a CSR, set up once at boot
Why it went the comparison, computed in EX mcause — a number the hardware writes
Coming back it does not come back it must, at exactly the right PC

So the machine has to be able to say, precisely, which instruction it was on. That is mepc. Everything else in this part is about making that one register true.

A branch is decided by hardware. A trap is only raised by hardware — the handler reads mcause and decides what to do, in software, on the other side of the jump.

The Ordering Problem

Faults are noticed at different depths: at decode (an illegal encoding, ebreak, ecall), in EX (a misaligned address), in MEM (a page fault, when the MMU answers) — and nowhere at all for an interrupt. So two can be in flight at once.

1 2 3 4 5
IF ld x5 ecall add x6
ID ld x5 ecall add x6
EX ld x5 ecall add x6
MEM ld x5 ecall
WB ld x5

In cycle 4 the ecall is in EX and the ld is in MEM. Both fault, in the same cycle — and the ld is the older instruction.

Trap on the ecall and mepc names the wrong instruction. Worse, the ld’s page fault becomes unreportable — the instruction that raised it has already been flushed.

So a fault is not acted on where it is found. It is carried — to the one stage where instructions leave in order.

Retire

异常在指令的边界上被处理,即处理器在指令退休时响应异常……即使异常指令退休前被识别,异常也要在相应的指令退休时才会被处理。

Exceptions are handled at instruction boundaries: the processor responds when the instruction retires. Even if the exception is identified before it retires, it is only handled when it retires. — C906 manual, §3.5

Detect anywhere, act in one place.

Part III wanted the decision as early as possible. Part IV wants it as late as possible — a branch wants to stop guessing, a trap wants to be sure of the order.

What a Trap Saves

The whole architectural effect of a trap, in five steps:

Machine mode, on entry
1 mepc ← the PC of the instruction that faulted
2 mcause ← why; mtval ← the bad address, or the offending instruction bits
3 mstatus.MPIE ← MIE; MIE ← 0
4 mstatus.MPP ← the privilege mode we came from; switch to machine mode
5 PC ← mtvec, and the handler starts

And mret runs it backwards: PC ← mepc, MIE ← MPIE, MPIE ← 1.

That is all the state. The register file, the caches, the pipeline registers — none of it is saved. That is only possible because the trap is taken at an instruction boundary: at the moment we act, nothing is half-done.

Interrupts Are the Same Thing

An interrupt is a trap with no instruction to blame.

  • Same mechanism. One mcause space — the top bit set means interrupt — one mtvec, one mret.
  • Same boundary. It is taken between instructions, so mepc is the PC of the instruction that would have run next.
  • New: masking and priority. mstatus.MIE globally, the MIE CSR per source, and outside the core the PLIC (1023 sources, 32 priority levels) and CLINT (timer and software interrupts).

The unification is the design statement. x86 keeps exceptions and interrupts in separate machinery; RISC-V has one cause register, one vector, one return. An implicit branch is a branch, so it uses the branch hardware.

MIE is cleared on entry so the handler cannot be interrupted by the very source it is handling. Nesting is possible — but it is a decision the handler makes, not something the hardware does on its own.

What We Bought

Part III — branches Part IV — traps
Penalty 1–2 cycles ~5 cycles: the flush, plus the handler’s prologue
Frequency about 1 in 5 instructions 10⁻³ to 10⁻⁶
CPI contribution 0.2 – 0.4 at most 0.005, usually far less
What you optimise speed correctness

An out-of-order machine cannot retire in order for free — it finishes instructions in whatever order they become ready. It needs a reorder buffer to put them back in program order, and the reason the reorder buffer exists is to make exceptions precise.