Load-Use Hazard
The load-use hazard (also called the load-to-use or load-delay hazard) is the single Read-After-Write (RAW) data hazard that operand forwarding cannot resolve in the Classic Five-Stage Pipeline. The MIT 6.004 notes state it bluntly: “there’s one data hazard that bypassing doesn’t completely address … the SUBC is trying to use the value the immediately preceding LD is writing. This is called a load-to-use hazard” (MIT 6.004 Ch. 15). The reason is timing: a load’s data is produced at the end of the MEM stage, but a dependent ALU operation in the next instruction needs that value at the start of its own EX stage one cycle later. Forwarding a value from the end of cycle t to the start of cycle t would require backward time travel; no bypass network can do it. The minimum cost is one bubble: the dependent instruction stalls in ID for one cycle, then the load’s data is forwarded from MEM/WB into the consumer’s ALU input via the standard MEM-to-EX bypass. In the definitely-not-esp32 project’s RV32IMC core, the load-use hazard is the residual cost the in-order pipeline always pays on a load followed by an immediate use; reducing its frequency is a job for the compiler, not the hardware.
1. The Timing That Makes It Inevitable
To see why the load-use hazard cannot be forwarded away, walk through the exact stage occupancy of a load followed immediately by a dependent ALU operation in the 5-stage pipeline:
lw x5, 0(x10) # i1: load from address x10+0 into x5
add x6, x5, x7 # i2: x6 = x5 + x7 (immediate use of x5)
Cycle: 1 2 3 4 5 6
i1 lw: IF ID EX MEM WB # x5's value produced at END of cycle 4
i2 add: IF ID EX MEM WB # x5 needed at START of cycle 4 ???In cycle 4, i1 is in MEM: it issues the data-memory read at the start of the cycle, the memory responds during the cycle, and the loaded value is latched into the MEM/WB pipeline register at the end of cycle 4. Simultaneously, i2 is in EX in cycle 4: it expects its operands to be valid at the start of cycle 4 so the ALU can compute the addition in that cycle. The load’s data does not exist at the start of cycle 4; it will exist at the end of cycle 4. Forwarding from the still-pending MEM read to the simultaneously-active EX stage is not a routing problem; the value simply is not yet computed.
The Wikipedia Classic RISC pipeline article puts the constraint precisely: “the data read from [data memory] is not present in the data cache until after the Memory Access stage,” making “backward-in-time forwarding impossible” (Wikipedia, Classic RISC pipeline). The minimum mitigation is to delay the consumer by one cycle so its EX coincides with the load’s WB (or, equivalently, comes one cycle after the load’s MEM ends):
Cycle: 1 2 3 4 5 6 7
i1 lw: IF ID EX MEM WB
i2 add: IF ID -- EX MEM WB
^
bubble: i2 holds in ID for one extra cycleAfter the stall, i2 is in EX in cycle 5. By then, i1 is in WB, its loaded value is in the MEM/WB pipeline register, and the standard MEM-to-EX forwarding path (the same one described in Operand Forwarding) delivers x5 to i2’s ALU input. The hazard costs exactly one cycle of CPI.
2. Mental Model
The mental image is “production end of MEM versus consumption start of EX, one cycle apart.”
flowchart LR subgraph C4["Cycle 4 (the impossible cycle)"] direction LR LW_MEM["lw in MEM<br/>(memory read in flight,<br/>data NOT yet available)"] ADD_EX["add in EX<br/>(needs x5 at start of cycle,<br/>has nothing to forward)"] LW_MEM -. "the value does not exist yet" .-> ADD_EX end subgraph C5["Cycle 5 (after one-bubble stall)"] direction LR LW_WB["lw in WB<br/>(loaded value sits in MEM/WB)"] ADD_EX2["add in EX<br/>(MEM/WB bypass delivers x5)"] LW_WB -- "MEM-to-EX bypass<br/>(standard forwarding)" --> ADD_EX2 end
The load-use timing wall and its one-cycle resolution. What it shows: in cycle 4 the load’s value does not yet exist when the dependent add needs it. After one bubble, in cycle 5, the value is available in the MEM/WB pipeline register and the standard forwarding path completes the routing. The insight to take: forwarding is about spatial routing of a value that exists somewhere; load-use is a temporal gap that no routing can close. The bubble buys the cycle that lets the spatial-routing solution work.
3. The Hazard-Detection Unit
Unlike the EX-stage forwarding unit, which decides only which operand source to multiplex in, load-use detection has to stall the pipeline. It lives logically in the ID stage and looks at a single, narrow condition:
- The instruction in ID/EX (the one about to enter EX) is a load (
MemRead = 1in the textbook control bits). - The destination register of that load (
rd_id_ex) matches one of the source registers (rs1orrs2) of the instruction in the ID stage (the one currently being decoded, which will enter EX in the next cycle).
The CS61C notes on the 5-stage pipeline describe this detection as a small comparator at the boundary between ID and EX: when the condition fires, the stall signal does three things in parallel (CS61C Notes, Five-Stage Pipeline; the CMU 18-447 lecture gives the generic recipe for any hazard stall as “stop all up-stream stages; drain all down-stream stages” (CMU 18-447 Lecture 8)):
- Freeze the PC. The PC does not increment; the same instruction stays in IF for one extra cycle.
- Freeze IF/ID. The fetched instruction stays in IF/ID for an extra cycle (so it does not lose the instruction that was in mid-fetch).
- Inject a bubble into ID/EX. Instead of letting the instruction in ID flow through to ID/EX, replace its control bits with NOP-equivalent zeros. The bubble propagates down through EX, MEM, WB doing nothing.
After one cycle, the in-flight load has advanced from MEM to WB, the bypass condition for the standard MEM/WB-to-EX forwarding path is now satisfied, and the previously-stalled consumer can flow through EX normally.
In RTL pseudocode:
load_use_stall = (
id_ex.MemRead and
id_ex.rd != x0 and
(id_ex.rd == if_id.rs1 or id_ex.rd == if_id.rs2)
)
if load_use_stall:
PC.enable = 0
IF/ID.enable = 0
ID/EX <= NOP_bubble
else:
PC.enable = 1
IF/ID.enable = 1
ID/EX <= normal_decode_of(IF/ID)The hardware cost is tiny: one extra 5-bit comparator (or two, one per source register), an AND with the MemRead control bit, and three enable signals routed to existing latches. The pipeline register and PC need an enable input, which most synthesizable latch designs already have.
The “two consumers after the load” question is sometimes asked: what if the second instruction after the load also depends on it? After the one-cycle stall, the load is in WB and the immediate consumer is in EX, served by MEM-to-EX bypass. The next instruction after that is in ID, and one cycle later it will be in EX, by which time the load has completed WB and the register file has the value. So the second consumer needs no extra stall; the normal register-file read (or the WB-to-ID internal bypass) suffices.
4. The Compiler’s Job: Scheduling to Fill the Slot
The load-use hazard is the textbook compiler-scheduling target. If the compiler can find an instruction that does not depend on the loaded value and place it between the load and the use, the bubble disappears: the in-between instruction runs in EX while the load is in WB, and the consumer’s EX falls one further cycle out, by which time forwarding works.
Consider the unscheduled C code t = a[i] + b[i]; ...:
lw x5, 0(x10) # x5 = a[i]
add x6, x5, x11 # x6 = x5 + b <-- load-use hazard, stalls 1 cycle
sw x6, 0(x12) # store tA scheduler can reorder to eliminate the stall, if there is independent work to do. For example, if there is a later independent computation:
lw x5, 0(x10) # x5 = a[i]
addi x13, x13, 1 # independent: post-increment some loop counter
add x6, x5, x11 # x6 = x5 + b <-- now no stall
sw x6, 0(x12)The addi x13 slips into the cycle that would have been a bubble. The hazard is invisible to the program’s correctness (the compiler is free to reorder because the dependences are preserved), but the performance is improved.
Production compilers (GCC, Clang/LLVM, the Rust toolchain that definitely-not-esp32 targets) all do this kind of scheduling under their instruction-scheduling pass. The microarchitectural cost model the scheduler uses includes a “load latency = 2 cycles” entry for any in-order 5-stage RV32 target: the load itself is one cycle, and the next instruction sees an additional one-cycle stall if it depends.
5. Historical Detour: The Load Delay Slot
The earliest commercial 5-stage RISC, the MIPS R2000 introduced in 1986, did not have hardware detection of load-use hazards. Instead, the ISA exposed the hazard to software: MIPS I defined a load delay slot, the instruction immediately following a load, in which “the instruction in the load delay slot cannot use the data loaded by the load instruction” (Wikipedia, MIPS architecture). The compiler was responsible for either inserting an independent instruction into the slot or, failing that, a NOP. The name “Microprocessor without Interlocked Pipelined Stages” (the M, I, P, S of MIPS) is exactly the description of this design choice: the hardware does not interlock (does not detect and stall on) certain hazards; the software has to guarantee none occur (Wikipedia, MIPS architecture).
This was abandoned remarkably quickly. The same Wikipedia article notes: “MIPS II removed the load delay slot.” The motivation, summarized in the Classic RISC pipeline article: “the original Stanford MIPS relied on compiler-inserted NOPs, but later designs added hardware detection for better instruction cache performance, hence ‘Microprocessor without Interlocked Pipeline Stages’ became a misnomer” (Wikipedia, Classic RISC pipeline). The problem was that NOPs in the binary wasted both instruction-cache space and decoder bandwidth; once chips had transistors for the hazard-detection unit (and they did, almost immediately), it was strictly better to spend them on detection and let the compiler optimize when it could.
By contrast, RISC-V was designed in the 2010s, decades after the cost of hazard detection had become trivial, and the ISA does not expose any pipeline timing. The Wikipedia Delay slot article puts the modern verdict in one line: “ARM, PowerPC, RISC-V” have no delay slots, period (Wikipedia, Delay slot). The result is that the definitely-not-esp32 RV32IMC core must implement the load-use stall in hardware; the ISA gives the compiler no permission to skip the bubble.
6. Worked Example: Cost Accounting
Take a tight loop common in embedded code:
loop:
lw x5, 0(x10) # i1: load array[i]
add x6, x6, x5 # i2: accumulate <-- load-use, 1 bubble
addi x10, x10, 4 # i3: pointer increment
bne x10, x11, loop # i4: loop back if moreWithout scheduling, every iteration pays:
Cycle: 1 2 3 4 5 6 7 8 9
i1 lw: IF ID EX MEM WB
i2 add: IF ID -- EX MEM WB
i3 addi: IF -- ID EX MEM WB
i4 bne: IF ID EX MEM WBFour instructions take 9 cycles (counted from IF of i1 to WB of i4 is 9 cycles; conventionally we count the steady-state extra cost as 1 bubble per loop iteration). The per-instruction CPI inflation is 1/4 = 0.25, so CPI rises from 1.00 to 1.25 just from this one hazard.
After scheduling (swap i2 and i3, which is legal since they have no dependence on each other):
loop:
lw x5, 0(x10) # i1
addi x10, x10, 4 # i3: independent, fills the load-use slot
add x6, x6, x5 # i2: now sees x5 ready via MEM-to-EX bypass
bne x10, x11, loop # i4Now no bubble; CPI drops back to 1.0 (ignoring branch resolution, which is a separate concern). The factor-of-1.25 difference is exactly the CPI calculus that Cycles Per Instruction formalizes.
In aggregated benchmarks, the load-use rate is typically a small but nonzero fraction of all dynamic instructions, contributing on the order of 0.05 to 0.15 to the actual CPI of a small in-order RV32 core running optimized C or Rust code. The rate depends heavily on whether the compiler’s scheduler had useful independent work to slot in.
7. What If the Load Misses in the Cache?
The one-bubble cost above assumes the load hits in the L1 data cache (or, on a small embedded SoC like the ESP32-C3 or the definitely-not-esp32 core, that the load completes in one cycle from local SRAM). If the load misses, the MEM stage stalls for the full cache-fill latency, which can be dozens to hundreds of cycles depending on where the data eventually lives (FPGA block RAM is one cycle; external PSRAM via a controller is many cycles). The load-use hazard is then completely dwarfed by the miss latency.
This is why modern out-of-order CPUs spend so many transistors on hiding load latency (non-blocking caches, memory-level parallelism, speculative scheduling of loads): the minimum cost is one bubble (the load-use hazard) but the average cost on a miss-heavy workload can be orders of magnitude higher. An in-order embedded core like the ESP32-C3’s 4-stage pipeline (Espressif TRM, Ch. 1) typically just stalls; the assumption is that the SRAM is fast enough and the workload predictable enough that miss-driven stalls are bounded.
8. Why Modern ISAs Hide It, Old ISAs Exposed It
The choice between exposing a pipeline-timing constraint to the ISA (the MIPS-I approach: define a load delay slot) and hiding it behind a hardware interlock (the modern approach: detect and stall) is a clean illustration of the broader RISC philosophy’s evolution.
In 1986, transistor budgets were tight, and the MIPS designers chose to push the complexity to the compiler. The compiler had to know the pipeline depth and the load latency to produce correct code (a NOP in the delay slot when no useful instruction could be scheduled). This worked but had two costs: the compiler became microarchitecture-specific, and any future processor that wanted to change the pipeline depth had to either preserve the original timing or risk breaking existing binaries.
By the early 1990s, both costs became unacceptable. The Wikipedia RISC article identifies “instructions to be fixed-length, simplifying pipelines” (Wikipedia, RISC) as the genuine RISC win, while implementation-specific pipeline timing is recognized as an anti-feature that ties the ISA to one microarchitecture. Modern RISC ISAs (ARM, PowerPC, RISC-V) all reject delay slots, branch or load, on this principle. The base RV32I spec is silent on pipeline timing entirely; the spec only defines architectural state and program semantics, leaving the microarchitecture free to be 2 stages, 5 stages, or 15 stages with whatever interlocks it needs (RISC-V Unprivileged ISA, src/unpriv/rv32.adoc). This is precisely the contract that lets a single RV32IMC binary run unchanged on the ESP32-C3’s 4-stage SiFive E2-derived core and on the definitely-not-esp32 project’s hand-built 5-stage core.
9. Failure Modes and Common Misunderstandings
- “The load-use bubble is two cycles, not one.” No: exactly one. A two-cycle stall would be needed only if the load’s data were available even later (e.g., a load that misses the cache and waits a cycle for SRAM); for a one-cycle-access D-memory or D-cache hit, the bubble is one cycle.
- “The hazard detection unit lives in EX.” It lives at the ID/EX boundary, where it can see both the about-to-enter-EX load (in ID/EX) and the about-to-be-decoded consumer (in IF/ID). Putting it later would mean the consumer was already in EX before the stall was decided.
- “Forwarding from the data memory output to the ALU input would fix it.” It would not: the data memory output is not stable until the end of MEM; the ALU input is needed at the start of EX. The two cycles are not the same instruction’s two halves; they are different instructions’ simultaneous stages.
- “Compilers always fill the slot.” Often, but not always. Tight loops with a single dependency chain (a linked-list traversal, a strict reduction) genuinely cannot schedule independent work in the load-use slot, and the bubble is unavoidable.
- “The load delay slot is just like the branch delay slot.” Both are 1986-era ISA-level exposures of pipeline timing, but they cover different hazards: branch delay slot for control hazards, load delay slot for the load-use data hazard. Both were abandoned for the same reason, and the Wikipedia Delay slot article calls out that “load delay slots … are very uncommon because load delays are highly unpredictable on modern hardware” (Wikipedia, Delay slot).
- “This hazard does not apply to stores.” Correct: stores do not produce a register value, so no following instruction can be dependent on the store’s “result.” Stores never cause a load-use hazard.
10. See Also
- Operand Forwarding: the technique that resolves every RAW hazard except this one.
- Pipeline Hazards: the broader taxonomy and the place this hazard sits within it.
- Classic Five-Stage Pipeline: the pipeline whose specific timing makes this hazard inevitable.
- Cycles Per Instruction: how load-use stalls show up in the CPI numbers.
- RV32IMC and RISC-V Instruction Set Architecture: the ISA whose silence on pipeline timing makes the hazard a pure hardware concern.
- ESP32-C3: a 4-stage RISC-V core with the same load-use bubble.
- Computer Architecture MOC: the parent map.