Dynamic Scheduling and Scoreboarding

A simple pipeline is a queue with rigid manners: instructions execute in exactly the order the compiler wrote them, and if one instruction stalls — waiting on a slow multiply, or a value still travelling back from memory — everybody behind it waits too, even the ones that have nothing to do with the stall. That is the tragedy of in-order execution: a single traffic jam freezes a whole road of cars that could happily have taken the next exit.

Dynamic scheduling is the hardware's answer. The processor is allowed to look a little way down the instruction stream, notice which instructions are ready — their inputs are available and a functional unit is free — and run those now, letting them slip past a stalled instruction ahead of them. The program still means the same thing; it just isn't executed in textbook order. This is the seed of every high-performance CPU built since, and it was born in 1964 in Seymour Cray's CDC 6600 and its scoreboard.

The motivating stall

Consider four instructions, where F registers hold floating-point values:

DIV.D F0, F2, F4 ; F0 = F2 / F4 -- a floating divide: SLOW (~20+ cycles) ADD.D F6, F0, F8 ; F6 = F0 + F8 -- needs F0, so it MUST wait for the divide SUB.D F10, F12, F14 ; F10 = F12 - F14 -- totally independent! MUL.D F16, F12, F18 ; F16 = F12 * F18 -- also totally independent!

The \texttt{ADD.D} genuinely depends on the divide's result (F0) — a true read-after-write (RAW) data hazard, and there is no honest way to run it early. But the \texttt{SUB.D} and \texttt{MUL.D} touch none of the divide's registers. On an in-order machine they are stuck in line behind the \texttt{ADD.D}, idling for twenty cycles for no reason. Dynamic scheduling lets them jump the queue and execute while the divider grinds away.

How the scoreboard is wired

The scoreboard is a central bookkeeper. Instructions are issued in order from the queue, but once issued each one lives at its own functional unit and proceeds independently. The scoreboard watches every register and every unit, and answers one question over and over: is it safe to let this instruction take its next step yet? Because the divide and the subtract live at different units, they run in parallel and finish in whatever order their latencies dictate.

The four scoreboard stages

Scoreboarding replaces the single "execute" step with four bookkept stages. An instruction may not advance to the next stage until the scoreboard clears it:

StageWhat happensThe scoreboard blocks it until…
1. Issuedecode; allocate a functional unitthe needed unit is free and no other active instruction targets the same destination register (avoids a WAW hazard)
2. Read operandsread source registersboth sources are available (no pending write to them) — this resolves RAW
3. Executethe functional unit computes— (runs freely; may take many cycles)
4. Write resultwrite the destination registerno earlier instruction still needs to read the old value of that register (avoids a WAR hazard)

Notice what has appeared. On a plain in-order pipeline the only data hazard that mattered was RAW (a true dependence). The moment we let instructions execute and finish out of order, two new hazards crawl out of the woodwork — and the scoreboard's stall conditions in stages 1 and 4 exist precisely to police them.

The three data hazards, named

Every pair of instructions that touch the same register can conflict in one of three ways. Only one is a real dependence; the other two are accidents of reusing register names.

WAR and WAW are false dependences: they exist only because the two instructions happened to be assigned the same architectural register name, not because any value truly flows between them. The scoreboard copes by stalling — a blunt but correct tool. The next lesson, Tomasulo's algorithm, will make them vanish entirely by renaming registers.

What we bought, and what we paid

Run the little model below. It walks our four instructions through an in-order machine and then a dynamically-scheduled one, counting the cycles wasted waiting behind the slow divide.

// A toy latency model of the four-instruction sequence. // Each instruction has a latency; DIV is slow. The independent SUB/MUL can // overlap the divide only if the machine is allowed to execute out of order. interface Inst { name: string; latency: number; dependsOnDiv: boolean; } const prog: Inst[] = [ { name: "DIV.D", latency: 20, dependsOnDiv: false }, { name: "ADD.D", latency: 3, dependsOnDiv: true }, // needs F0 from DIV { name: "SUB.D", latency: 3, dependsOnDiv: false }, // independent { name: "MUL.D", latency: 5, dependsOnDiv: false }, // independent ]; // In-order: nobody starts until the instruction ahead has finished. let inOrder = 0; for (const i of prog) inOrder += i.latency; // Out-of-order: the divide runs; independent ops overlap it; the ADD waits for the divide. const div = prog[0].latency; // 20 const add = div + prog[1].latency; // ADD can't start before DIV done // SUB and MUL overlap the divide entirely (they start at cycle 0 on free units) const outOfOrder = Math.max(add, prog[2].latency, prog[3].latency); console.log(`in-order total: ${inOrder} cycles`); console.log(`out-of-order total: ${outOfOrder} cycles`); console.log(`saved: ${inOrder - outOfOrder} cycles by not blocking on the divide`);

The saving is real, but so is the cost: the scoreboard is a chunk of hardware that must track the status of every instruction, every register, and every functional unit, and check a web of conditions every cycle. Seymour Cray spent that silicon in 1964 and made the fastest computer in the world. The idea never left.

The 6600's scoreboard resolved a RAW hazard by making the consumer wait until the producer had written the register file, then read it in stage 2. There was no forwarding path to hand the result directly from one unit to the next. So even a ready result sat one extra beat in the register file before its consumer could grab it. Worse, the scoreboard's stall conditions for WAR and WAW meant instructions could block at issue or block at write-back purely over reused register names — throughput lost to bookkeeping, not to real work. Tomasulo's design a few years later fixed both: results are broadcast directly to waiting instructions, and renaming makes the WAR/WAW stalls disappear. Scoreboarding was the brilliant first draft.

A scoreboard issues in order but lets instructions execute and write back out of order. Students often conclude the program's meaning can change — it cannot. The hardware only reorders instructions it has proven independent; every true (RAW) dependence is still honoured to the letter, and WAR/WAW are policed by stalls. The visible result is exactly what in-order execution would produce, just sooner. Reordering is an optimisation you are never supposed to be able to observe — until, decades later, speculation makes tiny timing leaks observable, which is a story for the security lessons.

Where this leads

Scoreboarding gives us the core bargain of instruction-level parallelism: issue in order, execute out of order, and let independent work overlap a stall. Its weaknesses — no result forwarding, and stalls on false (WAR/WAW) dependences — are exactly what the next generation of designs attacks. Tomasulo's algorithm adds a common data bus that broadcasts results the instant they are ready, and register renaming that abolishes false dependences altogether. Everything modern — superscalar issue, the reorder buffer, speculation — is built on the foundation you have just seen.