How a five-stage in-order core overlaps instructions, and what it costs
2026-10-21
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.
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.
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.
| 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.
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.
| 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.
CPI = 10/6 = 1.67n + k − 1 cycles, so CPI = 1 + (k−1)/n+4 is fill and drain — the first overhead, and the smallestSix instructions with six different destinations ran at one per clock. Change one register number.
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?
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.
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:
sub wait until the value reaches the register filesub the value from the pipeline register, as soon as it exists| 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 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 |
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.
| 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 |
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.
| 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 load-to-use stall is a property of the program order, and the compiler chooses that order.
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.
A data hazard means the value is not ready yet. A control hazard means you do not know what to fetch.
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.
| 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.
| 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.
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.
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.
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.
| 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.
A branch changes the PC on purpose. An exception changes it without asking — and then has to put everything back.
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.
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.
异常在指令的边界上被处理,即处理器在指令退休时响应异常……即使异常指令退休前被识别,异常也要在相应的指令退休时才会被处理。
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.
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.
An interrupt is a trap with no instruction to blame.
mcause space — the top bit set means interrupt — one mtvec, one mret.mepc is the PC of the instruction that would have run next.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.
| 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.
CS 4250 · Pipelining — SG2000