Stalling and the Load-Use Hazard
Consider a five-stage pipelined processor. It executes `lw s0)` immediately followed by `add t0, t0` the instant it enters its Execute stage. But the `lw` does not produce that value until its Memory stage completes. No amount of clever wiring can send a value backward in time. This is the load-use hazard. It is the one data hazard that Data Hazards and Forwarding cannot fully solve. That's because forwarding only fixes hazards where the needed value already exists somewhere in the pipeline when it is needed. This matters because every pipelined processor must confront the same physical fact. This includes everything from a five-stage MIPS classroom core to a modern out-of-order superscalar. Loads have latency. Instructions that consume loaded data too soon will read garbage unless the hardware intervenes. Real processors solve this with hazard detection logic. This logic freezes part of the pipeline for exactly one cycle. It inserts a "bubble" that lets the load finish before its consumer proceeds. Getting this detection wrong produces a bad processor. It is either incorrect (silently forwarding stale data) or absurdly slow (stalling on every instruction, hazard or not). The big picture: forwarding paths move data sideways across stage boundaries. This happens within the same cycle or across adjacent cycles. Stalling moves an instruction backward in time. It does this by holding the instruction in place. Meanwhile a bubble — a no-op — flows through the stages ahead of it. Stalling is strictly more expensive than forwarding, since it costs cycles. So a well-designed pipeline forwards whenever it can. It stalls only when it must. The load-use hazard is precisely the boundary case where forwarding runs out of road. This article builds the hazard-detection unit from first principles. It derives exactly which instruction pairs trigger a stall. It shows the pipeline register modifications needed to freeze and squash. It walks through the control logic in pseudocode. It traces a full cycle-by-cycle example. This includes a case that is easy to get wrong — back-to-back loads feeding a single consumer.
- A load-use hazard occurs when an instruction needs a register value that the immediately preceding `lw` has not yet fetched from memory.
- Forwarding cannot close this one-cycle gap because the data does not exist yet at the point it is needed — this requires stalling instead.
- The fix: freeze PC and IF/ID for one cycle, insert a bubble into ID/EX, and let forwarding handle the rest normally afterward.
- Hazard detection compares the destination register of the instruction in ID/EX (if it is a load) against the source registers of the instruction in IF/ID.
- The stall costs exactly one cycle per load-use hazard, recoverable in software by instruction scheduling (compiler reordering).
- The same detect-and-bubble mechanism generalizes to other unresolvable hazards, foreshadowing how Control Hazards use stalling and prediction together.
When a value legitimately does not exist yet, the pipeline must stall — no forwarding path can deliver data from the future.
The idea
A data hazard arises when an instruction depends on the result of one still in flight ahead of it. Forwarding (or bypassing) solves most such hazards by routing a value directly from the pipeline register where it becomes available to the pipeline register where it is needed, skipping the slow round trip through the register file. This works whenever the producing instruction computes its result before the consuming instruction needs it, given the fixed stage offsets of a pipeline. The load-use hazard is the exception. In the classic five-stage pipeline (IF, ID, EX, MEM, WB), an arithmetic instruction produces its result at the end of EX, one stage before it would be needed by a back-to-back dependent instruction — and that is exactly early enough to forward EX/MEM into the next instruction's EX stage. A load instruction, however, does not produce its result until the end of MEM, one stage later. If the very next instruction needs that value in its own EX stage, the data simply is not ready: MEM for the load and EX for the consumer occur in the same cycle, and MEM finishes only at the cycle's end while EX needs the value at the cycle's start. Forwarding EX/MEM→EX assumes the value exists when EX begins; here it does not. The only correct fix is to buy time: hold the dependent instruction (and everything fetched after it) in place for one cycle while a bubble — a no-op that carries no register writes or memory operations — is injected into the stage the dependent instruction would have entered. After that single stall cycle, the load's result is available in MEM/WB or has just left MEM, and ordinary MEM/EX forwarding can deliver it. The general principle: forwarding closes gaps of zero or negative latency between production and consumption; stalling manufactures extra latency when the gap is positive, i.e., when the data genuinely will not exist in time no matter how the wires are drawn.
How it works
Step 1 — Identify the hazard condition. The instruction sitting in ID/EX is examined: is it a load (`MemRead` control signal asserted)? If so, its destination register `rd` (or `rt` in MIPS I-type load encoding) is compared against the source registers (`rs`, `rt`) of the instruction currently in IF/ID, which is about to enter ID/EX next cycle.
Step 2 — Detect at decode time. This comparison must happen while the load is in ID/EX and the dependent instruction is in IF/ID — one cycle before the dependent instruction would enter EX. This gives the control logic a full cycle's notice to act.
Step 3 — Stall the front of the pipeline. If a hazard is detected:
- The PC is held (not incremented / not loaded with the next fetch address) — the same instruction will be fetched again.
- The IF/ID pipeline register is held (its contents are not overwritten) — the dependent instruction stays parked in decode.
- The ID/EX pipeline register is instead loaded with a bubble: all control signals zeroed (`RegWrite = 0`, `MemWrite = 0`, `MemRead = 0`, `Branch = 0`, etc.), so it behaves as a no-op flowing through EX, MEM, WB.
Step 4 — Resume. One cycle later, the load has moved into MEM/WB (or is finishing MEM), the dependent instruction — unchanged in IF/ID — is finally allowed into ID/EX, and it is now one cycle "younger" relative to the load's progress, precisely aligning with the existing MEM/EX or WB/ID forwarding paths, which take over from here exactly as in the non-load case. The elegance of this design is that the hazard-detection unit only ever needs to insert one bubble per load-use hazard, and after insertion the existing forwarding logic (already built for the ALU-to-ALU case) automatically supplies the correct value — no new forwarding path is needed, only a one-cycle delay. ``` // Hazard Detection Unit — evaluated every cycle, combinationally, // using the instruction currently latched in IF/ID and ID/EX. function DETECT_LOAD_USE_HAZARD(ID_EX, IF_ID): // ID_EX holds the instruction one stage ahead (older) of IF_ID's instruction. if ID_EX.MemRead == true: // producer is a load load_dest = ID_EX.rt // destination register of the load if load_dest != 0: // ignore hazards on the zero register if (IF_ID.rs == load_dest and USES_RS(IF_ID.opcode)) or (IF_ID.rt == load_dest and USES_RT(IF_ID.opcode)): return STALL return NO_STALL // Called once per cycle, before latching new pipeline register values. function PIPELINE_CYCLE_UPDATE(): stall = DETECT_LOAD_USE_HAZARD(ID_EX, IF_ID) if stall == STALL: PC_write_enable = false // freeze PC: re-fetch same instruction next cycle IF_ID_write_enable = false // freeze IF/ID: keep dependent instruction parked ID_EX_next = BUBBLE() // insert a no-op into ID/EX (all control signals = 0) else: PC_write_enable = true IF_ID_write_enable = true ID_EX_next = DECODE(IF_ID) // normal decode proceeds latch_all_pipeline_registers() function BUBBLE(): ctrl = zero_all_control_signals() // RegWrite=0, MemWrite=0, MemRead=0, // Branch=0, ALUOp=NOP, MemtoReg=0 return { control: ctrl, rd: 0, data: undefined } ``` The forwarding unit from Data Hazards and Forwarding runs unmodified alongside this logic; it simply now sees the load and its consumer separated by an extra cycle, which is exactly the gap its EX/MEM- and MEM/WB-forwarding paths were built to close.
Complexity & correctness
Time cost. Each load-use hazard costs exactly one stall cycle in the classic five-stage pipeline (the gap between a load's MEM stage and the earliest possible EX stage of its consumer is exactly one cycle, given the standard MEM/EX and MEM/WB forwarding paths already present). If a program has `k` load-use hazards among `n` executed instructions, total cycle count becomes: ``` Cycles = n + (pipeline_depth - 1) + k ``` where the `(pipeline_depth - 1)` term is the fixed one-time pipeline fill latency and `k` counts stall bubbles. This is a strictly additive, per-hazard cost — no hazard causes more than one stall cycle in this design, because after one bubble the existing forwarding hardware fully resolves the dependency. Compare this to a naive pipeline with no forwarding at all, which would need to stall roughly two or three cycles per RAW dependency (waiting all the way for WB and a register-file read) — forwarding is what keeps the common case free, and stalling is reserved for the one case forwarding cannot cover.
Space cost. The hazard detection unit adds a small constant amount of combinational logic: two register-number comparators (5 bits wide each, for a 32-register file) and a few AND/OR gates, evaluated once per cycle — O(1) in both gates and delay, independent of program size or register file size (comparator width grows only as O(log(number of registers))). Additional state is one extra multiplexer/bubble-generation path per pipeline register — again O(1) hardware, not proportional to pipeline depth or program length.
Correctness argument. The invariant the hazard unit must preserve is: no instruction reads a source register in EX before all in-flight instructions that will write that register have either written it (WB) or made it available via a valid forwarding path. Forwarding paths in the baseline design connect:
- EX/MEM → EX (result computed one cycle prior, needed one cycle later — available exactly in time)
- MEM/WB → EX (result computed two cycles prior — trivially available)
For a load-producer, the register value is not computed until the end of MEM, which is concurrent with the EX stage of an immediately following consumer. There is no forwarding path landing "at the start of this same cycle" because nothing in the pipeline computes a value strictly before it is needed within the identical cycle — that would require negative latency. Hence forwarding is provably insufficient exactly when: ``` producer_stage_that_creates_value(load) == MEM consumer_stage_that_needs_value == EX and producer and consumer occupy the SAME cycle (i.e., are adjacent in program order) ``` Stalling for one cycle shifts the consumer's EX stage to align with the load's MEM/WB or later, restoring the invariant using forwarding paths that already exist. The hazard-detection condition (`ID_EX.MemRead ∧ match(rd, rs_or_rt)`) is both necessary (any load-use adjacency violates the invariant without it) and sufficient (once the load is not immediately followed in EX by its consumer, existing forwarding suffices) — no over- or under-stalling occurs, assuming register `0` are architecturally discarded, so no hazard exists there).
Tradeoff. This is a correctness-preserving but throughput-costing mechanism: every stall is a wasted cycle. The alternative — software instruction scheduling — lets a compiler reorder independent instructions into the stall slot, achieving the same correctness at zero hardware-inserted delay if the compiler can find useful work; when it cannot, a nop or a genuinely necessary stall remains, and the hardware mechanism above is the fallback that guarantees correctness regardless of what code is scheduled.
Worked example
Trace this sequence (register file initially: `s1 = 7`): ``` I1: lw s0) # t1, s1 # t0 + t2, s0 # t1 - $s0 ```
| Cycle | IF | ID | EX | MEM | WB | Hazard unit action |
|---|---|---|---|---|---|---|
| 1 | I1 | – | – | – | – | no ID/EX instruction yet |
| 2 | I2 | I1 | – | – | – | ID/EX empty, no stall |
| 3 | I3 | I2 | I1 | – | – | ID_EX = I1, MemRead=1, rt=t0 → HAZARD → stall |
| 4 | I3 (re-fetch, held) | I2 (held) | bubble | I1 | – | PC frozen, IF/ID frozen, ID/EX gets bubble |
| 5 | I4... | I3 | I2 | bubble | I1 | I1 in WB writes t1 = 42+7 = 49 |
| 6 | ... | ... | I3 | I2 | bubble | I3 uses $t1; by now WB (cycle5→6) or MEM/EX forwarding supplies 49 to I3's EX |
Without the stall, cycle 4 would have let I2 enter EX using whatever the register file (or a nonexistent forward) supplied for `t1 = 42 + 7 = 49` correctly. I3, needing `t0, 0(t1, 4(t2, t1 # depends on BOTH loads ``` Here the hazard unit fires when I1 is in ID/EX and I2 is in IF/ID? No — I2 does not use `t1 = I2`'s destination. The unit stalls once, for I3 relative to I2, exactly as before — I1's dependency on nothing and I2's independence from I1's destination mean only one stall is needed total, not two, illustrating that the hazard unit correctly examines only the immediately adjacent producer in ID/EX, not the entire pipeline, because that is the only position from which a same-cycle EX/MEM conflict can arise.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Forgetting to check `MemRead` before comparing registers | Any register match in ID/EX is mistaken for a hazard, causing stalls even for non-load producers that forwarding already handles | Gate the register comparison with `ID_EX.MemRead == true` |
| Not excluding register `$0` | Instructions writing `$0` are architecturally no-ops; a naive comparator sees a false match and stalls unnecessarily | Add `load_dest != 0` to the hazard condition |
| Forgetting to also stall the PC | Freezing only IF/ID lets the next instruction be fetched into IF/ID and overwritten, effectively dropping an instruction | Freeze PC and IF/ID together — both must hold |
| Not zeroing all control signals in the bubble | A "bubble" that still asserts `RegWrite` or `MemWrite` corrupts state as if it were a real instruction | Explicitly zero every control signal, not just the opcode field |
| Assuming forwarding alone suffices for loads | Confusing the load-use hazard with the ALU-ALU hazard, which forwarding does solve | Recognize the one-cycle latency gap unique to `MemRead` producers |
| Stalling on load-load pairs with no shared register | Checking equality without verifying the specific field usage (e.g., `rs` used by `add` but not by some other opcode) | Use `USES_RS`/`USES_RT` predicates keyed to instruction type, not blind field comparison |
Pioneers & history
The load-use hazard and its resolution are inseparable from the history of pipelined processor design, an idea whose modern instruction-level formulation is closely tied to the RISC movement of the early 1980s, where exposing pipeline hazards to explicit hardware control (or even to the compiler) became a design philosophy rather than an afterthought.
Origins · 1 minJohn L. Hennessy and how this was discovered
Hennessy's work on the MIPS architecture in the early 1980s treated pipeline hazards — including the load-use case — as a first-class design problem to be solved either by hardware interlocks or by compiler scheduling, a duality that shaped how the field teaches and builds pipelines to this day.
Where this matters
Every classroom RISC pipeline (MIPS, RISC-V single-issue designs) implements exactly this hazard-detection-and-bubble mechanism, and it is the canonical example used to teach the limits of forwarding. In real silicon, the same principle scales up dramatically: modern out-of-order processors (Intel, AMD, ARM cores) replace the simple one-bubble stall with scoreboards and reservation stations that track, for every in-flight load, exactly when its result becomes available, and hold dependent micro-ops until then — the load-use hazard becomes a scheduling problem rather than a fixed pipeline-depth stall, but the underlying physical constraint (data cannot travel backward in time) is identical. Compilers exploit the same insight through instruction scheduling, deliberately reordering independent instructions into the cycle that would otherwise stall, a technique used by every optimizing compiler backend (LLVM, GCC) targeting pipelined hardware. Cache-miss-extended loads make the problem more severe still: a load that misses in L1 may take dozens of cycles, and the same type of dependency stall — now dynamically variable in length — is what out-of-order execution and speculative scheduling exist to hide. Understanding the fixed, one-cycle load-use stall in a simple pipeline is the essential stepping stone to understanding these more elaborate mechanisms, and directly foreshadows how Control Hazards and Branch Prediction use analogous detect-and-recover logic when the hazard is not a data dependency but an uncertain instruction stream.
Ask the tutor: How would the hazard-detection logic and stall count change in a six-stage pipeline with a separate cache-access stage before MEM?
Why doesn't the compiler just always avoid this hazard?
Compilers can and do reorder instructions to move independent work into the slot right after a load, a technique called instruction scheduling, which eliminates the stall entirely when enough independent instructions exist nearby. But scheduling cannot always find replacement work — sometimes the very next instruction genuinely needs the loaded value and no reordering is legal without violating program semantics or register renaming limits. In those cases the compiler simply cannot avoid the dependency, and the hardware's hazard-detection unit is what guarantees correctness regardless of how well or poorly the compiler scheduled the code. This is why real processors implement both: aggressive compiler scheduling to minimize stalls in the common case, and hardware interlocks as an unconditional correctness backstop.
Is a load-use stall the same thing as a pipeline flush?
No, they solve different problems and cost different amounts. A stall (bubble insertion) freezes part of the pipeline for one cycle to wait for data that will definitely arrive shortly — it never discards any fetched instruction, it only delays it. A flush, used for control hazards like mispredicted branches, discards instructions that were fetched down the wrong path entirely, because those instructions should never have been executed at all. Stalls cost exactly the number of cycles needed to wait; flushes cost the number of instructions incorrectly fetched, which is often several cycles' worth of wasted, discarded work — a strictly more expensive and architecturally different kind of hazard.
Does every pipelined processor need exactly one stall cycle for a load-use hazard?
Only pipelines with the same relative stage offset between a load's data-ready point and a consumer's data-need point require exactly one cycle. The number of stall cycles is determined by the pipeline's specific stage structure: it equals the gap between when MEM produces the value and when the earliest forwarding path could deliver it to EX, given the pipeline's depth and forwarding paths. Deeper pipelines with an extra stage between MEM and EX, or without full forwarding networks, could require more stall cycles; superscalar and out-of-order machines instead track load latency dynamically via scoreboards, replacing the fixed one-cycle assumption with a variable wait tied to actual memory latency, including cache misses.
What exactly is a "bubble" in hardware terms?
A bubble is not a special signal or magic value — it is simply an instruction-shaped set of pipeline register contents whose control signals are all set to their inactive/no-op state: `RegWrite = 0`, `MemWrite = 0`, `MemRead = 0`, `Branch = 0`, and so on. As this bubble flows through EX, MEM, and WB in subsequent cycles, it performs no register writes, no memory writes, no branches — it simply occupies a pipeline slot for one cycle without doing anything, functionally equivalent to executing a `nop` instruction, except it is inserted by hardware rather than fetched from the instruction stream.
Can hazard detection mistakenly trigger a stall when none is needed?
Yes, if the detection logic is implemented too coarsely. Common overtriggering bugs include forgetting to check that the load's destination is not register `$0` (writes there are always discarded, so no real hazard exists), or forgetting to verify that the consuming instruction's opcode actually reads the matched register field (some instructions do not use `rs` or `rt` at all, e.g., certain immediate or jump instructions). A correctly implemented hazard unit is precise: it stalls if and only if a genuine load-use dependency exists, never more often, since unnecessary stalls directly and needlessly reduce throughput.
How does this relate to control hazards and branch prediction?
Both load-use hazards and control hazards require the pipeline to pause or discard work when correctness cannot otherwise be guaranteed, but they differ in cause and remedy. A load-use hazard is a data dependency with a known, fixed resolution time, so a single deterministic stall suffices. A control hazard arises from not yet knowing which instruction to fetch next after a branch, which cannot be fixed by a fixed-length stall alone — this is why processors instead use branch prediction to guess the target speculatively, only flushing (not merely stalling) when the guess is wrong. The two mechanisms — detect-and-stall versus predict-and-flush-on-misprediction — share the same underlying goal of preserving correctness under pipelining but apply fundamentally different techniques suited to each hazard's nature.