The
I2 needs the r1 that I1 is still computing. This is a
data hazard: an instruction depends on a result that an earlier, still-in-flight
instruction has not yet made available. Left alone, the pipeline would hand I2 a stale
r1 and silently compute the wrong answer. This lesson is about how the hardware notices, and
the beautiful trick that fixes it almost for free.
Trace the cycles. I1 only produces r1 at the end of its
EX stage (cycle 3), and only writes it back to the register file in
WB (cycle 5). But I2, following one cycle behind, wants r1 as an
input to its own EX in cycle 4 — a full cycle before I1 writes it.
This is the classic read-after-write (RAW) hazard, and the gap is the enemy:
Read that chain carefully: the value exists in cycle 3, sitting in the EX/MEM pipeline register.
It is just not in the register file until cycle 5. The naive fix — stall I2 until
I1's WB finishes — would cost two or three wasted cycles on nearly every instruction pair.
Unacceptable. There is a better way.
The value I2 wants already exists in cycle 3 — why route it the long way through the register
file? Forwarding (also called bypassing) adds short-circuit wires and
multiplexers that grab a freshly computed result straight out of the EX/MEM or
MEM/WB pipeline register and feed it back into the ALU inputs of a following instruction.
The result is delivered a cycle after it is born, exactly when the consumer's EX needs it. Step through
the figure: first the hazard, then the bypass wire that erases it.
With a full forwarding network — paths from EX/MEM and MEM/WB back into EX — a back-to-back dependent pair
of ALU instructions runs with zero stall cycles. The pipeline keeps its
Forwarding works because an ALU result is ready at the end of EX. But a load's data isn't ready until the end of MEM — one stage later. So if the very next instruction needs that loaded value, even forwarding arrives too late:
The data would have to travel backwards in time. It can't, so the hardware inserts exactly
one bubble (a stall of one cycle), delaying the add so its EX lands in cycle
5 — and then forwarding from MEM/WB delivers r1 just in time. This unavoidable
load-use stall is the reason compilers try to slot an independent instruction between a
load and its first use.
A write-after-read (WAR) hazard means a later instruction overwrites a register before an
earlier one has read it; a write-after-write (WAW) means two writes to the same register
land out of order. In our in-order pipeline both are impossible, and the reason is structural: every
instruction reads its operands in ID (early) and writes its result in WB
(late), and instructions enter and leave in program order. An earlier instruction always reads
before a later one writes, and always writes before a later one writes. These "name" hazards only appear
once we let instructions execute
The forwarding unit is really just a comparator: does this instruction read a register that a recent instruction is about to write? Here is the logic in miniature — it classifies each adjacent pair as independent, a forwardable ALU hazard, or a load-use stall.
More than you'd guess. A result must be forwardable to both ALU inputs, from both the EX/MEM and MEM/WB registers, so each ALU operand gets a multiplexer choosing among the register-file value and several bypass sources. On a superscalar machine that issues four instructions per cycle, every one of the eight ALU inputs can come from any of a dozen recently produced results — the forwarding network becomes a dense crossbar that can dominate the wiring and even limit the clock. This is a recurring theme in architecture: the logic is cheap, but the bypass wiring that keeps everything fed is expensive. Wide out-of-order cores spend a startling fraction of their area just shuffling results around.
A tempting summary is "forwarding makes all data hazards free." Not quite. Forwarding kills the ALU-to-ALU RAW hazard because an ALU result exists at the end of EX. But a load produces its value one stage later, at the end of MEM, and no wire can send data back in time to the consumer's EX in the same cycle. So the load-use case always costs one bubble — unless the compiler hides it by scheduling an unrelated instruction into the gap. Whenever someone claims "forwarding removes every stall," ask them about the instruction right after a load.