Precise Exceptions

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 pipelined machine has five instructions at five different stages when the fault strikes. Which of them "already happened"? The answer must match the program order, not the physical order in the pipe — and making that true is the seed of one of the most important structures in modern CPUs.

What "precise" actually means

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 virtual memory would be impossible. Precision is not a luxury — it is what makes exceptions usable.

The hard part: instructions in flight

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.

Interrupts vs. traps

Two flavours of "stop and handle this" get lumped together, but they differ in where they come from:

Trap / exceptionInterrupt
Sourcethe instruction itself (synchronous)external device — timer, disk, network (asynchronous)
Timingat a specific instruction, reproduciblybetween instructions, whenever it arrives
Examplespage fault, divide-by-zero, illegal opcodekeypress, 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.

Resolving a fault, in code

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.

// Precise-exception policy, applied at the in-order commit point. // Instructions are numbered 1..n in program order; `faultIdx` raises an exception. function resolve(n: number, faultIdx: number): void { let committed = 0, squashed = 0; for (let i = 1; i <= n; i++) { if (i < faultIdx) { committed++; console.log(`I${i}: commit (older than the fault -> keeps its effects)`); } else if (i === faultIdx) { squashed++; console.log(`I${i}: FAULT (save PC, squash, jump to handler)`); } else { squashed++; console.log(`I${i}: squash (younger than the fault -> no effect)`); } } console.log(`--> ${committed} committed, ${squashed} squashed. State is precise.`); } resolve(5, 3); // 5 instructions in flight; the 3rd faults

This is the punchline the whole lesson is building toward. A modern out-of-order core executes instructions whenever their inputs are ready — wildly out of program order — which would seem to make precise exceptions impossible. The fix is a reorder buffer (ROB): instructions may execute out of order, but their results are held aside and committed strictly in program order, one entry retiring at a time. If an instruction at the head of the ROB carries an exception, everything behind it in the buffer is simply thrown away. The ROB is exactly the commit-in-order idea from this lesson, scaled up — which is why precise exceptions in the simple pipeline are the seed of the ILP module.

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.