Worked examples — Precise exceptions in pipelines
This is the worked-examples child of Precise exceptions in pipelines. The parent told you what a precise exception is and which mechanisms enforce it. Here we do the opposite: we throw every kind of situation at those mechanisms and trace the machine cycle-by-cycle until the answer is undeniable.
Before we start, one promise: every symbol below is spelled out the first time it appears. If you have never seen a pipeline stage before, read 5.2.1-Pipelining-basics first — but you can survive on the mini-glossary in the first callout.
The whole game of precision is one sentence: architectural state must change strictly in program order, and never for an instruction that logically shouldn't have run.
The scenario matrix
Every case this topic can throw at you falls into one of these cells. Each later example is tagged with the cell(s) it covers, so by the end you will have touched every single one.
| # | Case class | The awkward thing about it | Covered by |
|---|---|---|---|
| C1 | Fault behind older un-retired work | Faulting instr is in EX while an older instr hasn't written back yet (out-of-order completion) | Ex 1 |
| C2 | Multiple faults, same cycle | Two instructions raise exceptions together — who wins? | Ex 2 |
| C3 | Two faults inside ONE instruction | A single instruction can trip several checks (page fault and overflow) | Ex 3 |
| C4 | Speculative fault, wrong path | A fault on an instruction that a mispredicted branch should have skipped | Ex 4 |
| C5 | Speculative fault, RIGHT path | Same fault, but the prediction was correct — now it must fire | Ex 4 (twin) |
| C6 | Out-of-order execute, in-order commit (ROB) | Younger instr finishes before an older faulting one | Ex 5 |
| C7 | Degenerate: exception on the very first instruction | Nothing older exists to complete | Ex 6 |
| C8 | Degenerate: exception on the very last / no younger instr | Nothing younger to flush | Ex 6 |
| C9 | Limiting case: back-to-back exceptions | Handler itself faults, or a second fault the cycle after resume | Ex 7 |
| C10 | Real-world word problem | Page fault → disk → resume, measured in cycles | Ex 8 |
| C11 | Exam twist: store already sent to memory | A younger store "leaked" to memory before an older fault — is it still precise? | Ex 9 |
Example 1 — Fault behind older un-retired work (C1)
Forecast: Guess now — do we take the exception this cycle, or do we wait? And for how many cycles?

Step 1 — Detect and tag. In cycle 5, the EX stage's divide unit sets a fault flag on I3. Why this step? Detection is not the same as taking the exception. We only record "I3 wants to fault"; taking it now would strand I1 and I2 mid-flight and violate rule 1 (all older instructions must complete). Look at the amber flag on I3 in the figure.
Step 2 — Convert younger instructions to bubbles. I3 (EX), I4 (ID), I5 (IF) are turned into NOP ("no operation" — an instruction that changes nothing). Freeze the PC so no new fetch happens. Why this step? Rule 2: the faulting instruction and everyone after it must not touch architectural state. Turning them into NOPs guarantees their WB writes nothing. The cyan crosses in the figure mark the killed slots.
Step 3 — Drain the older instructions.
- Cycle 5: I1 finishes WB. ✅ committed.
- Cycle 6: I2 (LOAD) moves MEM→WB and writes R4. ✅ committed.
Why this step? These are older than I3, so by precise semantics they truly executed. We must let them complete.
Step 4 — Take the exception at cycle 6. After I2's WB, save I3's PC into EPC (Exception Program Counter — the register the handler reads to know where the fault was) and vector to the handler. Why this step? This is the first cycle at which "everything older is done, nothing younger changed state" is simultaneously true — the definition of precise.
Verify: Architectural registers changed: R-dest(I1) and R4 (from I2). Registers NOT changed: R6 (I3), plus whatever I4/I5 targeted. EPC = address(I3). Instructions committed = 2, instructions squashed = 3. Total pipeline drain = 2 cycles (5→6). Consistent with "drain depth = number of older un-retired instructions," which is 2 here. ✔
Example 2 — Two faults, same cycle (C2)
Forecast: The illegal opcode was caught earlier in the pipe (ID is before EX). Does "caught earlier in hardware" win, or does something else?
Step 1 — Attach program-order sequence numbers. I3 is older than I4 (I3 was fetched first). Why this step? Precision is defined against a sequential machine. A sequential machine reaches I3 before I4, so it would fault on I3 and never even fetch I4.
Step 2 — Select the oldest fault. Pick I3's divide-by-zero (Exception B). Discard I4's exception entirely. Why this step? Reporting I4 would falsely tell the OS that instructions up to and including I3 all succeeded — a lie, since I3 divides by zero.
Step 3 — Flush younger, drain older. Squash I3, I4, I5. Let I1, I2 complete. Take B.
Verify: Reported exception = divide-by-zero at PC(I3). The illegal-opcode of I4 vanishes (I4 never runs in the sequential model). This matches the parent's rule: "report the earliest one in program order." ✔ The trap: hardware detected A first in time but B first in program order — program order wins.
Example 3 — Two faults inside ONE instruction (C3)
Forecast: Same instruction, so program order can't break the tie. What's the tie-breaker?
Step 1 — Order faults by pipeline stage of detection within the instruction. Fetch/address faults are detected before the data actually returns; alignment/memory-access faults are checked as part of the access. Why this step? Within one instruction, the earliest logical sub-step to fail defines what "should have" happened. A per-instruction fixed priority list encodes this (see parent): fetch faults > illegal/privilege > arithmetic. Alignment (an address-computation fault) outranks the page-fault-triggered data fetch here only if the address is computed to be illegal before the translation is attempted; on most ISAs the page/TLB translation is checked first.
Step 2 — Apply the fixed priority. Following the parent's priority (fetch/translation highest), report the page fault. Suppress the alignment report for now. Why this step? If we report alignment first and the OS "fixes" it, it would immediately hit the page fault on retry — needless churn. Reporting the higher-priority translation fault first lets the OS load the page; the alignment check re-fires on retry only if still relevant.
Verify: Exactly one exception is delivered per instruction per attempt. EPC = PC(LOAD) (unchanged, so retry lands here). See 7.2.5-Virtual-memory-exceptions for why page faults are restartable (state was never modified — precise). ✔
Example 4 — Speculative fault: wrong path AND right path (C4, C5)
Forecast: In one case the divide-by-zero reaches the OS; in the other it silently disappears. Which is which?

Step 1 — Mark, don't fire. When I2 faults, set a speculation bit on I2 in the ROB (Reorder Buffer — the in-order waiting list where results park before commit; see 6.3.4-Reorder-buffer-in-superscalar). Record the fault inside the ROB entry, do NOT jump to the handler. Why this step? We don't yet know if I2 is even supposed to exist. Firing now could report a fault on a phantom instruction.
Step 2 — Wait for the branch to resolve. I1 computes its condition.
Step 2a — Case (a), branch actually TAKEN (wrong-path, C4). I2 was on the discarded path. Flush I2 (and everything after the branch on the wrong path) from the ROB. The fault is thrown away with it. Why? A sequential machine that took the branch never executes I2, so its fault must not exist. See the amber "discarded" arrow in the figure.
Step 2b — Case (b), branch actually NOT taken (right-path, C5). I2 is genuine. It becomes the ROB head, the recorded fault now fires: EPC = PC(I2), vector to handler. Why? Now the sequential machine really would reach I2 and divide by zero, so precision demands the exception.
Verify: Same physical fault, two outcomes decided purely by which path commits. C4 → 0 exceptions delivered; C5 → 1 exception delivered at PC(I2). This is exactly why speculation and precise exceptions coexist (5.2.11-Branch-prediction). ✔
Example 5 — Out-of-order execute, in-order commit (C6)
Forecast: I4 is done — is R8 already updated? If so, isn't precision broken?

Step 1 — Execute out of order, write results to the ROB, not to registers. After execution:
| ROB entry | Instr | Status | Result parked in ROB |
|---|---|---|---|
| 1 | I1 | COMPLETE | R1 value |
| 2 | I2 | COMPLETE | R4 value |
| 3 | I3 | FAULTED | (div-by-zero) |
| 4 | I4 | COMPLETE | R8 value |
Why this step? "COMPLETE" means computed, not committed. R8's value sits in ROB entry 4 — the architectural R8 is untouched. This decoupling is the whole trick.
Step 2 — Commit in strict head-to-tail order.
- Head=1 → commit I1 → architectural R1 updated. ✅
- Head=2 → commit I2 → architectural R4 updated. ✅
- Head=3 → I3 is FAULTED → take exception here.
Why this step? Commit is the only place architectural state changes, and it is in program order. So even though I4 finished first, its result is still stuck in the ROB.
Step 3 — Flush from the faulting head onward. Discard ROB entries 3 and 4. The parked R8 value in entry 4 is never copied to the register file. Why? I4 is younger than the fault → must not affect state.
Verify: Committed: R1, R4. Never committed: R6, R8. Note R8 was computed yet architectural R8 is unchanged — precision holds because "computed ≠ committed." This is the ROB's superpower. ✔
Example 6 — Degenerate ends: first-instruction and last-instruction faults (C7, C8)
Forecast: Do the "drain older" and "flush younger" steps break when one side is empty?
Step 1 — Case (a): empty "older" set. "Let all older instructions complete" is vacuously satisfied — there are none. Immediately save EPC = PC(I1) and vector to the handler. Why this step? The rule "all older complete" is an ∀ (for-all) over an empty set → automatically true. No special case needed; the same logic just does nothing.
Step 2 — Case (b): empty "younger" set. "Flush all younger" is vacuously satisfied. Drain the older ones (Ex 1's logic) and take the fault. Why this step? Same reasoning: the flush loop runs zero times.
Verify: In (a): committed = 0, EPC = PC(I1), so retry re-fetches I1 after the OS maps the page. In (b): committed = all older, squashed = 1 (just the faulting instr). Both are precise with no extra hardware — the general mechanism already covers the degenerate boundaries. ✔ (This is why we say the mechanism is total: no scenario is left unhandled.)
Example 7 — Limiting case: back-to-back / nested faults (C9)
Forecast: If the second fault overwrites the first fault's saved PC, can we ever get back to the user program?
Step 1 — EPC must be saved before the handler can be re-interrupted, and the handler must save EPC to memory promptly. Why this step? The single EPC register holds the original faulting PC. If a nested exception occurs before the handler has copied EPC to its stack, EPC is clobbered and the user program is lost.
Step 2 — Mask further exceptions on entry (or use a separate handler stack + banked EPC). Most ISAs auto-disable interrupts and set a "in-exception" mode bit on trap entry. Why this step? This gives the handler a safe window to spill EPC and status before anything else can interrupt. See 3.4.7-Exception-handling-in-ISA.
Step 3 — On resume: restore EPC into PC. Because the LOAD never modified state (precise!), re-executing it is safe and idempotent.
Verify: Requirement = EPC preserved across nesting ⇒ the handler must save EPC(=PC of original LOAD) before enabling nested traps. Number of times the LOAD must be re-runnable without side effects = ≥1. Precision guarantees no partial side effect, so re-run is correct. ✔
Example 8 — Real-world word problem: page-fault cost (C10)
Forecast: Will "making it precise" even be visible next to a 10-million-cycle disk read?
Step 1 — Sum the fixed CPU overheads. Why this step? These are the cycles caused by handling the fault in the processor, independent of disk.
Step 2 — Add the disk wait.
Step 3 — Fraction attributable to pipeline precision (drain + vector = 10 cycles).
Why this step? The "precision tax" is just the drain+vector cost; the rest is unavoidable OS + disk work.
Verify: , i.e. about one part per million — utterly negligible. The disk dominates (). Lesson: precise exceptions are essentially free on rare, expensive faults, which is exactly the workload they target. ✔
Example 9 — Exam twist: did a younger store leak to memory? (C11)
Forecast: Memory writes are permanent. If the STORE already happened, can we ever be precise?
Step 1 — Identify the danger. If I2 writes memory before I1's fault is resolved, architectural memory changed for a younger instruction → precision is violated. Why this step? Rule 2 forbids any younger instruction from modifying architectural state (registers or memory).
Step 2 — The fix: a store buffer that commits in order. Stores do not write memory in MEM; they park in a store buffer and only drain to memory at commit, in program order — exactly like ROB register writes. Why this step? This makes memory obey the same "in-order commit" rule as registers. When I1 faults at commit, I2 is still sitting in the store buffer and is flushed, never reaching memory.
Step 3 — Take I1's exception; discard the buffered store.
Verify: Memory changed by I2? No — its bytes never left the store buffer. Committed state = whatever is older than I1 (nothing here). EPC = PC(I1). Precise ✔. This is why loads/stores interact with precise exceptions through forwarding AND a commit-ordered store buffer, never a bare MEM write.
Recall Self-test — cover the answers
Which fault wins when two fire the same cycle? ::: The one oldest in program order (Ex 2). In a ROB, when does architectural state actually change? ::: Only at commit, in program order — never at execute/complete (Ex 5). A speculatively-executed instruction faults. What do you do first? ::: Mark it, don't fire; wait for the branch to resolve (Ex 4). Why is the "exception on the first instruction ever" case not special? ::: "Drain all older" is vacuously true over an empty set (Ex 6). How is memory kept precise for a younger store? ::: A store buffer that drains to memory only at in-order commit (Ex 9). What fraction of a page-fault's cost is the precision overhead? ::: About one part per million — negligible (Ex 8).