Something goes wrong. An instruction divides by zero, touches a page that isn't in memory, or hits an illegal opcode. The processor must stop what it's doing, jump to an operating-system handler, and — crucially — be able to come back and carry on as if nothing happened. For that to work, the machine has to hand the handler a clean, believable snapshot of itself: this instruction faulted, everything before it finished, nothing after it has begun. That clean snapshot is a precise exception, and delivering one from a pipeline with five instructions in flight is surprisingly hard.
A single-instruction-at-a-time machine gets this for free — there's only ever one instruction to point at.
But our
An exception is precise when the saved architectural state is exactly as if instructions had executed one at a time and stopped cleanly at the faulting one:
Without this guarantee the operating system couldn't reliably page in memory and retry, debuggers
couldn't single-step, and
Here is the mess. When I3 faults in EX, older instructions I1 and I2
are further down the pipe (in MEM and WB) and are about to — or already have — changed the
machine's state. Younger instructions I4 and I5 are in ID and IF, harmless so far
but not for long. To be precise, the hardware must let the older ones finish, wipe out the faulting one and
the younger ones, and only then take the trap.
The trick that makes this clean: don't act on an exception the instant it's detected. Instead, tag the faulting instruction with its exception and let that tag ride down the pipeline to the commit point (WB). Because instructions reach WB in program order, by the time the tagged instruction gets there, every older instruction has already committed and every younger one can be squashed. The exception is taken in order, and the state is automatically precise.
Two flavours of "stop and handle this" get lumped together, but they differ in where they come from:
| Trap / exception | Interrupt | |
|---|---|---|
| Source | the instruction itself (synchronous) | external device — timer, disk, network (asynchronous) |
| Timing | at a specific instruction, reproducibly | between instructions, whenever it arrives |
| Examples | page fault, divide-by-zero, illegal opcode | keypress, packet arrived, clock tick |
Both must be precise: whether the cause is inside the code or outside the chip, the handler needs the same clean, resumable snapshot. An interrupt is simply taken at the next convenient commit boundary; a trap is pinned to its instruction. The pipeline machinery — tag, carry, act at commit, squash the rest — is identical.
The whole policy is one rule applied at the commit point: commit everything older than the fault, squash the fault and everything younger. Here it is in miniature — feed it a stream of in-flight instructions and the index that faults, and it prints the precise outcome.
This is the punchline the whole lesson is building toward. A modern
The instinct is to handle a fault the moment you spot it — but in a pipeline that would be a disaster. If
I3 faults in EX and you jumped to the handler immediately, you'd abandon I1 and
I2 mid-flight (state now half-updated and imprecise) and you'd have to somehow undo
I4 and I5. The discipline is: detect early, act at commit. Carry
the exception flag quietly down to WB and only pull the trigger there, in program order. Detection and
action happen at different places in the pipe — conflating them is how you get an imprecise, unrecoverable
machine.