Introduction: one core going faster by issuing several instructions at once

Lesson 5 kept the pipeline FULL by predicting which way a branch would go. But the scalar pipeline of Lesson 4 still ISSUES exactly 1 instruction per cycle, always in strict program order. A modern high-performance CPU goes further: it issues several instructions per cycle (superscalar), and lets a LATER instruction finish BEFORE an earlier one when there is no REAL data dependency between them — this is called out-of-order execution (OOO). The price: you must resolve the FALSE data dependencies created by having only a finite set of architectural registers, and you must guarantee the final architectural state is exactly as if everything had run sequentially. This lesson builds the classic algorithm that solves both: Tomasulo.


📚 Prerequisites
You must have read Lesson 4 (pipelining, RAW hazards) and Lesson 5 (branch prediction). OOO builds DIRECTLY on those two, and needs NO knowledge of caches or virtual memory (those come in Lessons 7–8).

1. Instruction-level parallelism (ILP) & superscalar CPUs

ILP (instruction-level parallelism) measures how far NEIGHBOURING instructions in a program can run in parallel without changing the result. A scalar CPU issues 1 instruction per cycle; a superscalar CPU has several functional units (ALU, multiplier, load/store unit) and can issue and execute SEVERAL instructions at once — but how much of that it can actually use depends ENTIRELY on whether the program contains enough INDEPENDENT instructions.

⚠️ Pitfall: superscalar does not automatically speed up every program
The real performance of a superscalar CPU is BOTTLENECKED by REAL data dependencies (RAW — a later instruction needs an earlier one's result). Even with 8 functional units, a chain of dependent instructions (each needing the one immediately before it) still runs almost SEQUENTIALLY; the hardware cannot parallelise it. Verified in section 4: the same 3 operations written as a real RAW chain take 11 cycles, while the version with only FALSE dependencies (WAR/WAW, which can be removed) takes 9.

2. False data dependencies & register renaming

Besides RAW (the REAL hazard from Lesson 4), there are 2 kinds of dependency that exist only because the PROGRAM reused ONE architectural register name for 2 values that are logically UNRELATED:

Kind What it means Is it a REAL dependency?
RAW (Read-After-Write) A later instruction READS the value an earlier one WROTE Yes — a real data dependency, it CANNOT be removed
WAR (Write-After-Read) A later instruction OVERWRITES a register whose OLD value an EARLIER one is still reading No — a FALSE dependency, purely a NAME collision
WAW (Write-After-Write) 2 instructions WRITE the same register, and "the later write wins" must be preserved No — a FALSE dependency, purely a NAME collision

Register renaming removes WAR/WAW entirely: every time an instruction WRITES to an architectural register it is given a NEW "version" (in Tomasulo, a ROB index — the reorder buffer), and the register alias table (RAT) records "register X currently points at which version". Later instructions reading or writing that register automatically use the CORRECT version — so there is no longer any physical contention between the different "versions" of one register name.

register_renaming_concept.txt (the RAT — register alias table)
// RAT[r] = null      -> the CORRECT value of register r lives in regFile
// RAT[r] = robIndex   -> the NEWEST value of r is still "in flight" in the ROB
//                        at position robIndex, NOT yet committed

// When ISSUING an instruction that writes to register dest:
//   RAT[dest] = newlyAllocatedRobIndex   // "RENAME" - create a NEW VERSION
// -> later instructions reading "dest" get this NEW VERSION through the RAT,
//    while earlier ones (which read "dest" at issue time, before the rename)
//    keep their OLD value - untouched by the overwriting instruction.
⚠️ Pitfall: confusing RAW (unremovable) with WAR/WAW (removable)
RAW is a real DATA constraint — if instruction B needs the COMPUTED result of instruction A, no renaming trick lets B run BEFORE A has finished computing it (that would violate the program's logic, not merely a hardware limit). WAR/WAW, by contrast, are only NAME constraints — renaming solves them COMPLETELY without reordering any computation at all. Verified: instr2 (WAR on R2 against the MUL in instr1) starts executing in cycle 3, LONG before instr1 completes in cycle 6 — proof that renaming eliminates the stall entirely.

3. The Tomasulo algorithm: reservation stations + CDB + ROB

Tomasulo (IBM 360/91, 1967) was the first OOO algorithm and is still the foundation of modern OOO CPUs (x86, ARM). It has 3 main parts:

  • Reservation Station (RS) — each functional unit has a few "reservation slots" holding instructions that are waiting for operands; as soon as BOTH operands are ready the station starts executing on its own (with no need to wait for earlier instructions it does not depend on).
  • CDB (Common Data Bus) — a SHARED data bus that broadcasts results: every reservation station waiting for that value snoops the CDB and updates its operand the moment the value appears. Because it is a SHARED resource, ONLY 1 instruction may write its result per cycle — the LOSER waits another cycle.
  • ROB (Reorder Buffer) — a buffer holding EVERY in-flight instruction in strict program order; even though instructions finish COMPUTING out of order, commit (officially writing to the architectural registers) always happens from the HEAD of the ROB — guaranteeing the final architectural state is identical to sequential execution.
Issue in order Add RS (awaiting operands) Multiply RS (awaiting operands) runs as soon as both operands arrive, in any order Functional unit execute CDB — shared bus, only 1 result broadcast per cycle ROB — holds every in-flight instruction, in PROGRAM order Commit from the ROB HEAD — in order or squash: wiped, as if never run RS snoops the CDB ROB slot reserved at issue time
Only the two ends follow program order — issue and commit. Everything in between is free to be as disorderly as it likes, and that is where the speed comes from.
tomasulo_cycle.js (extract from the shared cpu-core.js engine)
// Every cycle, IN THIS ORDER: Commit -> Write-result (1 broadcast per cycle)
// -> decrement `remaining` on the executing RSs -> WAITING->EXECUTING once the
// operands have arrived -> Issue 1 new instruction (renaming dest via the RAT).
function runTomasulo(instructions, opts) {
  // ... see cpu-core.js for the full version - this is the per-cycle main loop ...
  // 1. COMMIT the head of the ROB if it is ready
  // 2. WRITE-RESULT: only 1 broadcast per cycle, LOWER robIndex wins (older first)
  // 3. Decrement `remaining` on the EXECUTING reservation stations
  // 4. WAITING -> EXECUTING once Qj and Qk are both null (operands arrived via CDB)
  // 5. ISSUE 1 instruction: RAT[dest] = the new robIndex (THE RENAME)
}
// Verified: the program MUL R1 / ADD R2 / SUB R1 (WAR on R2, WAW on R1)
// -> totalCycles=9, ipc=3/9=0.333, final R1=18 (from SUB, thanks to in-order commit)
⚠️ Pitfall: a full ROB causes a "ROB stall"
The ROB has a FINITE CAPACITY. If one instruction gets stuck for a long time (say waiting on slow memory — Lesson 7), it occupies the HEAD of the ROB (because commits must be in order), so EVERY instruction BEHIND it — however long ago it finished computing — cannot commit and must queue up inside the ROB. Once the ROB is full the CPU has to STOP issuing new instructions entirely (a ROB stall) — even with dozens of functional units sitting idle. This is why modern CPUs invest in very large ROBs (hundreds of entries) and still cannot avoid stalling on an extremely slow instruction (such as a cache miss going all the way to DRAM).

4. Computing real IPC & how effective the algorithm is

First, the unit of measurement. IPC (Instructions Per Cycle — instructions completed per cycle) is simply the reciprocal of the CPI that Lesson 4 used: \(\text{IPC} = 1/\text{CPI}\). Two ways of saying the same thing, differing only in which direction is good — the lower the CPI the better, the cao the IPC the better. On an ideal scalar CPU the maximum IPC is 1; superscalar exists to get past that ceiling. At the 3-instruction scale of the examples below the IPC looks low because pipeline start-up cost dominates — what is worth comparing is the IPC of the versions against each other, not the absolute value.

Verified for real with the engine: the 3-instruction program MUL R1,R2,R3 / ADD R2,R4,R5 (WAR on R2) / SUB R1,R6,R7 (WAW on R1) — renaming resolves both false dependencies, and it runs in exactly 9 cycles, IPC = $3/9 = \mathbf{0.333}$. The final R1 is 18 (from SUB, correct per program order thanks to in-order commit — NOT the 12 from MUL which was overwritten, exactly the WAW meaning of "the later write wins"). Note that ADD (instr2) finishes COMPUTING (writeback in cycle 5) BEFORE MUL (instr1, writeback in cycle 6) — completion is OUT OF ORDER — and yet the commits still follow program order exactly (cycles 7, 8, 9 for instr1, instr2, instr3).

tomasulo_schedule_table.txt (the REAL schedule, verified with the engine)
Instruction         Issue  ExecStart  Writeback  Commit
MUL R1, R2, R3        1        2          6          7
ADD R2, R4, R5        2        3          5          8
SUB R1, R6, R7        3        4          7          9

# ADD (writeback=5) finishes BEFORE MUL (writeback=6) - completion is OUT OF ORDER.
# But the commits still follow program order: 7, 8, 9 (MUL, ADD, SUB).
# 9 cycles in total, IPC = 3/9 = 0.333.

The control: the SAME 3 operations written as a REAL RAW chain (each instruction using the result of the one before) takes 11 cycles, an IPC of only $3/11 = \mathbf{0.273}$ — clearly slower, because a real dependency CANNOT be removed by any means, exactly as the pitfall in section 1 said.

ipc_comparison.js (WAR/WAW against real RAW, using the same engine)
// Program A: MUL R1 / ADD R2 (WAR) / SUB R1 (WAW) -> 9 cycles, IPC=0.333
// Program B: MUL R1 / ADD R2,R1 (real RAW) / SUB R3,R2 (real RAW)
//            -> 11 cycles, IPC=0.273 (slower, because the dependency is REAL)
// Program C: MUL R1 / ADD R4 / SUB R5 (no conflicts at all)
//            -> ALSO 9 cycles, same as program A - proof that renaming made A
//               exactly AS FAST AS having no conflict in the first place!
⚠️ What OOO costs: silicon area & power
OOO control logic (the RAT, many reservation stations, the ROB, and a CDB network broadcasting to EVERY station each cycle) is enormously complex next to a simple scalar pipeline — it takes considerable silicon area and power, and not in proportion to the IPC it buys (the returns diminish once the program's available ILP is exhausted — see the pitfall in section 1 again). This is exactly why power-efficient cores (the "LITTLE" cores we will meet again in Lesson 9) usually use a simpler scalar pipeline rather than full OOO.

5. Why the ROB has to exist: precise exceptions

The ROB has appeared dozens of times so far in the role of "commit in order so the final result is right". But if the final result were all that mattered, a structure this expensive would not be needed — section 2 showed that register renaming on its own preserves data-flow semantics. The real reason the ROB exists lies elsewhere, and it only shows up when something goes wrong midway.

Suppose an instruction raises an exception : a divide by zero, an overflow, or most commonly of all — touching an address that is not in memory yet (a page fault, Lesson 8). The CPU has to jump into the operating system's handler, deal with it, and then resume the program. To be able to resume, the register state at that moment must be precise : exactly as if the program had run sequentially up to that instruction and stopped — not one instruction before it missing, and not one instruction after it applied.

But this CPU executes OUT of order. Section 4 measured it: instruction 2 writes back in cycle 5, instruction 1 not until cycle 6. So what does the register state look like if a fault lands in between? Run both kinds of machine on exactly the WAR/WAW program from section 4:

precise_exception.js (verified with the engine — with a ROB against without)
// Same program as section 4. Writeback cycles measured: [6, 5, 7]
// instr2 (ADD) finishes at cycle 5 - BEFORE instr1 (MUL) at cycle 6

// Case A: instruction 1 (MUL) faults
architecturalStateOnFault(program, { initialRegs, faultAt: 0 });
// with ROB    -> R2 = 3   (initial value: nothing has committed yet)
// without ROB -> R2 = 11  written by instr2, which has NOT run yet in program order

// Case B: instruction 2 (ADD) faults
architecturalStateOnFault(program, { initialRegs, faultAt: 1 });
// with ROB    -> R1 = 12  instr1 committed, exactly as the program requires
// without ROB -> R1 = 10  instr1 comes BEFORE the fault yet has not taken effect
⚠️ The "no ROB" state is broken in BOTH directions
Notice that the two scenarios fail in opposite directions. Case A: an instruction from the future has already written (R2 = 11) — the handler sees the result of something that has not happened. Case B: an instruction from the past has not taken effect yet (R1 is still 10) — something that did happen has left no result. In both, the register state corresponds to NO point in the sequential program. This is not "slightly wrong": it is meaningless. The operating system has no way to resume from a state like that, so a page fault could never be recovered from — which means virtual memory could not work at all.

That is exactly the ROB's job: let execution be as disorderly as it likes on the inside, while the effect on the architectural registers follows program order absolutely. At every instant there is a clean boundary — everything committed is settled past, everything still in the ROB is computation that does not officially exist yet.

🔍 The same mechanism makes MISPREDICTION survivable — and that is Spectre's way in
Lesson 5 built the branch predictor: the CPU guesses which way a branch goes and carries straight on instead of waiting. But guesses are sometimes wrong — and if the instructions on the wrong path had already written to the registers, there would be no way back. The ROB handles this too: instructions on the predicted path enter the ROB and execute normally, but do not commit. Once the misprediction is known, the CPU simply wipes (squashes) the part of the ROB after that branch — as if it had never run. In other words, one structure buys both precise exceptions and speculative execution; without a ROB, neither out-of-order execution nor branch prediction would be usable.

And this is precisely where Spectre gets in. A squashed instruction leaves no trace in the thanh ghi — but it did manage to change the cache, and the cache is not rolled back. The architectural state is clean; the microarchitectural state is not. Lesson 5 exploits exactly that gap, and Lesson 7 shows why a cache is measurable from the outside.
⚠️ The limit of this lesson's model: REGISTER dependencies only
The engine here — like most textbook presentations of Tomasulo — only handles dependencies through registers. A real CPU must also handle dependencies through memory: if a store does not yet know which address it will write to, may the load behind it go first? If the two addresses turn out to be the same, letting it go first means reading stale data — wrong. This problem is called memory disambiguation, and real CPUs solve it with a dedicated queue (the load/store queue) plus a guess-and-roll-back mechanism. This lesson does not model that, so do not conclude that renaming + ROB is all a complete out-of-order CPU needs.

6. Hands-on: a miniature interactive Tomasulo simulator

Pick one of the sample programs below to see the REAL Tomasulo schedule (issue / exec start / writeback / commit for each instruction) — comparing a WAR/WAW program (which renaming solves) directly against a real RAW chain (which it cannot) and a program with no conflicts at all:

🗂️ Tomasulo simulator (RS + CDB + ROB)

Summary

  • ✅ A superscalar CPU issues several instructions per cycle but is bottlenecked by REAL data dependencies (RAW) — verified: the real RAW chain takes 11 cycles against 9 for the WAR/WAW version.
  • ✅ WAR/WAW are FALSE dependencies (purely register-name collisions) — register renaming (the RAT) removes them COMPLETELY, unlike RAW which cannot be removed.
  • ✅ Tomasulo = reservation stations (waiting for operands) + the CDB (broadcast, 1 instruction per cycle) + the ROB (committing in program order despite out-of-order completion).
  • ✅ Verified: the final R1 is 18 (from SUB, the LATER write in the program) — in-order commit preserves the correct WAW meaning even though ADD finished computing before MUL.
  • ✅ Pitfall: a full ROB (usually caused by one instruction stuck at its head) triggers a ROB stall, halting instruction issue completely.
  • ✅ The real reason a ROB is needed is precise exceptions: verified, when instruction 1 faults, the machine with a ROB keeps R2 = 3 (nothing has committed) while the machine without one already shows R2 = 11 — the value of an instruction that was NOT yet allowed to run.
  • ✅ Verified in the opposite direction: with the fault at instruction 2, the machine without a ROB shows R1 = 10, meaning instruction 1 — which comes BEFORE the fault — has not taken effect. The state matches no point in the program, so it cannot be resumed.
  • ✅ The same mechanism squashes mispredicted branches — so the ROB is a precondition for both out-of-order execution and branch prediction (Lesson 5), and the reason Spectre leaves its trace in the cache rather than in the registers.

Review quiz

Question 1

Why does a superscalar CPU not automatically make EVERY program several times faster?

Question 2

Which kind of dependency does register renaming solve, and which does it NOT solve?

Question 3

Verified: in the WAR/WAW program, ADD (instr2) writes back in cycle 5, BEFORE MUL (instr1, cycle 6) — yet ADD commits in cycle 8, AFTER MUL (cycle 7). Why?

Question 4

When does a "ROB stall" happen, and why does it halt the CPU while functional units sit idle?

Download the lesson's practice code

File JavaScript CPUJS — a miniature computer-architecture library used across all 12 lessons. Lesson 6 has just added runTomasulo(), architecturalStateOnFault(), evalInOrder() — a cycle-accurate simulation of the Tomasulo algorithm (reservation stations, CDB, ROB, register renaming) plus the precise-exception comparison, with a self-test that checks every number quoted in this lesson (run node cpu-core.js, nothing to install):

Download cpu-core.js

📖 References

Related lessons in this series

Lesson 5: Branch Prediction & the Spectre Vulnerability Lesson 7: The Memory Hierarchy & Cache Architecture Back to the Computer Architecture roadmap

Comments