Data Hazards and Forwarding
Imagine a five-stage pipelined processor. It executes three back-to-back instructions that all touch register `t0` has not finished writing its result back to the register file. This happens by the time the next instruction tries to read it. If the pipeline naively lets that next instruction read `$t0` anyway, it reads a stale value. The program then silently produces the wrong answer. This is a data hazard. It is one of the fundamental correctness problems introduced by The Pipelined Datapath. Data hazards matter for an important reason. Pipelining is the single most important technique for making processors fast without shrinking transistors. Pipelining only works if overlapping instruction execution never changes the answer a sequential machine would give. Every modern CPU must solve this problem. This includes everything from a microcontroller to a superscalar out-of-order server chip. Instructions constantly depend on the results of instructions that immediately precede them. Examples include arithmetic chains, address computations, and loop counters. A processor that stalled the pipeline every time this happened would lose most of the benefit of pipelining in ordinary code. The elegant fix is forwarding, also called bypassing. This means routing the result of an instruction directly from the pipeline stage that produces it. It goes straight to the pipeline stage that needs it. This happens without waiting for that result to be written into the register file. Forwarding turns what looks like an unavoidable stall into a wire and a multiplexer. It is one of the cleanest examples in computer architecture of solving a correctness problem. It does this with extra hardware paths instead of extra time. This article builds the forwarding mechanism from first principles. It defines data hazards precisely (RAW, WAR, WAW). It derives exactly which forwarding paths a classic five-stage pipeline needs. It gives the forwarding-unit control logic as pseudocode. It also proves that the resulting datapath produces the same results as sequential execution. It closes with the one case forwarding cannot fix by itself. That case is the load-use hazard. This is picked up in the article on Stalling and the Load-Use Hazard.
- A data hazard occurs when an instruction needs a value that a nearby, not-yet-finished instruction is producing.
- In a pipeline that reads registers early and writes them late, a naive design reads stale data — this is the RAW hazard.
- Forwarding (bypassing) copies a result from a later pipeline stage's output latch straight to an earlier stage's input, skipping the register file.
- A forwarding unit compares source and destination register numbers across pipeline stages and drives ALU-input multiplexers.
- Forwarding costs only wires, comparators, and muxes — it adds no cycles for most RAW hazards in a simple in-order pipeline.
- One case — a load immediately followed by a use of its result — cannot be forwarded fast enough and needs a one-cycle stall.
Forwarding resolves a data hazard by routing a computed value directly between pipeline stages, so instructions never have to wait for it to reach the register file.
The idea
A data hazard arises when two instructions that are close together in the pipeline access the same register, and the order in which the pipeline would naturally access it differs from program order. The classical taxonomy has three kinds, defined relative to a pair of instructions (earlier) and (later):
- RAW (read-after-write): reads a register that writes. This is a true dependency — genuinely needs 's result. If reads before writes, it gets the wrong value.
- WAR (write-after-read): writes a register that reads. If writes before reads, picks up the wrong value. This is a false (name) dependency — no real data flows between them.
- WAW (write-after-write): and both write the same register. If they complete out of order, the register ends up with 's value instead of 's.
In the classic in-order, single-issue five-stage pipeline (IF, ID, EX, MEM, WB), only RAW hazards actually occur, because instructions read operands in ID and write results in WB, always in program order and always with each instruction's ID stage happening before later instructions' ID stages, and WB happening in the same relative order. WAR and WAW require instructions to be reordered or to read/write out of program sequence, which does not happen in this simple in-order design; they become real concerns only in more advanced techniques such as out-of-order execution, where Structural Hazards-style resource conflicts and reordering can let a later instruction's write or read race ahead. So the problem to solve here is exactly RAW: an ALU instruction produces a value at the end of its EX stage (or MEM/WB for loads), but that value logically belongs to the destination register the instant the instruction is "conceptually" done, long before it is physically written into the register file in WB. Forwarding (or bypassing) exploits the fact that the value already exists in a pipeline latch several cycles before WB; it just needs a wire and a multiplexer to deliver it early to any later instruction's ALU input, rather than forcing that instruction to fetch a stale copy from the register file.
How it works
Consider the standard five-stage MIPS/RISC-V-style pipeline. Each instruction passes through IF → ID → EX → MEM → WB, one stage per cycle, with the ID stage reading register operands from the register file and the WB stage writing a result back. Register-file reads and writes happen in the same cycle when they alias (write-then-read within a cycle, so a same-cycle WB/ID pair on the same register already works via internal register-file bypassing), but that alone does not fix the tighter cases. Take this instruction sequence: ``` add t1, t0 = t2 sub t0, t0, immediately after I1 defines it and t0, t0 or t0, t0, but by now WB has happened ``` Lay these out on a pipeline diagram (cycles across the top, stages across the rows): ``` Cycle: 1 2 3 4 5 6 I1 (add): IF ID EX MEM WB I2 (sub): IF ID EX MEM WB I3 (and): IF ID EX MEM WB I4 (or ): IF ID EX MEM WB ``` I1 computes `t0` exactly when I1's fresh result is sitting one stage ahead of it. Without forwarding, I2's ID stage (cycle 3) would have already read the register file and gotten the old, wrong value for `t0` back into the register file (WB happens in cycle 5, and by the conventional "write in first half, read in second half of the cycle" register-file discipline, or simply because ID for I4 is cycle 5 and WB for I1 is also cycle 5 with careful ordering, the value is available through the normal register read). I4 needs no forwarding at all — this is the case forwarding does not need to touch, because the pipeline's natural timing already delivers the correct value. This gives exactly two forwarding paths for a classic five-stage integer pipeline: 1. EX/MEM → EX — forward the ALU result computed one instruction ago. 2. MEM/WB → EX — forward the ALU result (or loaded value) computed two instructions ago, taken from the MEM/WB latch (this also covers forwarding a loaded value from a `lw` two instructions back, and it covers the case where EX/MEM forwarding was "skipped" because the intervening instruction did not need it). Two multiplexers are placed on the ALU's two input operands in the EX stage, and a small combinational block called the forwarding unit decides, every cycle, whether each ALU input should come from the register file (no hazard), from EX/MEM (forward from one instruction back), or from MEM/WB (forward from two instructions back).
Forwarding-unit logic
Let the pipeline registers carry, alongside data, the destination register number and a valid/regwrite flag for each in-flight instruction. Denote the instruction currently in EX as the consumer, and the instructions currently in MEM and WB as candidate producers. ``` // Registers available combinationally this cycle: // ID_EX.Rs, ID_EX.Rt -- source register numbers of instruction in EX // EX_MEM.RegWrite, EX_MEM.Rd -- info about instruction now in MEM // MEM_WB.RegWrite, MEM_WB.Rd -- info about instruction now in WB function FORWARDING_UNIT(ID_EX, EX_MEM, MEM_WB): ForwardA = "NONE" // selects source for ALU input A (Rs) ForwardB = "NONE" // selects source for ALU input B (Rt) // --- EX hazard: instruction in MEM (one ahead) defines a source of EX --- if EX_MEM.RegWrite and EX_MEM.Rd != 0 and EX_MEM.Rd == ID_EX.Rs: ForwardA = "FROM_EX_MEM" if EX_MEM.RegWrite and EX_MEM.Rd != 0 and EX_MEM.Rd == ID_EX.Rt: ForwardB = "FROM_EX_MEM" // --- MEM hazard: instruction in WB (two ahead) defines a source of EX, // but only if the EX/MEM case above did NOT already claim it // (EX/MEM hazard is closer in program order, so it takes priority) --- if MEM_WB.RegWrite and MEM_WB.Rd != 0 and MEM_WB.Rd == ID_EX.Rs and not (EX_MEM.RegWrite and EX_MEM.Rd != 0 and EX_MEM.Rd == ID_EX.Rs): ForwardA = "FROM_MEM_WB" if MEM_WB.RegWrite and MEM_WB.Rd != 0 and MEM_WB.Rd == ID_EX.Rt and not (EX_MEM.RegWrite and EX_MEM.Rd != 0 and EX_MEM.Rd == ID_EX.Rt): ForwardB = "FROM_MEM_WB" return ForwardA, ForwardB ``` ``` function ALU_INPUT(ForwardSelect, RegFileValue, EX_MEM_Value, MEM_WB_Value): if ForwardSelect == "FROM_EX_MEM": return EX_MEM_Value if ForwardSelect == "FROM_MEM_WB": return MEM_WB_Value return RegFileValue // "NONE": no hazard, use value read in ID ``` Two subtleties in this pseudocode are load-bearing, not stylistic:
- Checking `Rd != 0` guards against architectures (like MIPS) where register `$0` is hardwired to zero and instructions that "write" to it (or have no destination) must never trigger a forward — otherwise two unrelated instructions that both nominally target register 0 would be treated as dependent.
- Priority of EX/MEM over MEM/WB encodes program order: if both the instruction in MEM and the instruction in WB wrote the same register that EX now needs, the one in MEM is more recent (it is closer to EX in program order, having been issued later), so its value is the correct one and must win. Getting this priority backwards is a classic, silent correctness bug: the pipeline would forward a stale value from further up the pipeline even though a fresher one exists.
Complexity & correctness
Space/hardware cost. Forwarding adds: two 3-to-1 multiplexers on the ALU inputs, a handful of extra wires carrying the EX/MEM and MEM/WB result values back to the EX stage, and a forwarding unit whose logic is a small, constant number of equality comparisons — comparators independent of program size, since it only ever compares the (fixed, small) register-number fields of at most three pipeline registers each cycle. This is negligible next to the ALU, register file, and caches. Compare this to the without-forwarding alternative, which requires no extra hardware but forces the compiler or hardware to insert stall cycles (bubbles) whenever a RAW hazard exists — a real time cost on essentially every dependent instruction pair, since dependent arithmetic chains are extremely common in real code (loop-carried accumulations, address arithmetic, pointer chasing).
Time cost. With full EX/MEM and MEM/WB forwarding as described, a RAW hazard between an ALU instruction and a consumer that is one or two instructions later costs zero extra cycles — the pipeline still issues one instruction per cycle. The only place this specific mechanism fails is when the producer is a load instruction and the consumer immediately follows it, because a load's data does not exist until the end of its MEM stage, one cycle later than an ALU instruction's EX stage — at that point the consuming instruction's EX stage has already happened. No forwarding path can send a value backward in time; this residual case needs a single stall cycle and is handled by hazard detection plus a bubble, covered in Stalling and the Load-Use Hazard.
Correctness argument. The proof obligation is: the pipelined, forwarding-augmented machine computes the same register-file contents (and takes the same architected steps) as a hypothetical unpipelined machine that executes instructions strictly one at a time, in program order. Sketch of the argument: 1. Invariant: at the moment any instruction's EX stage consumes a source register value, that value is the most recently written value for that register among all instructions preceding it in program order (or the initial register-file value if none precede it and write it). 2. In program order, define for each register the sequence of instructions that write . For any consumer instruction reading , let be the closest preceding writer of (if any). 3. Because instructions enter the pipeline in program order, one per cycle (in this simple in-order issue model), the pipeline-stage distance between and is exactly instruction slots, which is also exactly the number of cycles separating their EX stages. 4. Case : 's result is in the EX/MEM register exactly when is in EX — the forwarding unit's EX/MEM check (`EX_MEM.Rd == ID_EX.Rs/Rt`) fires and selects it. Case : 's result is in the MEM/WB register exactly when is in EX — the MEM/WB check fires. Case : has already completed WB before reaches ID, so the ordinary register-file read in 's ID stage already returns the correct, updated value; no forwarding is needed. 5. The priority rule (EX/MEM over MEM/WB) guarantees that if two preceding instructions both wrote (say at distances 1 and 2), the nearer one — which is the true most-recent writer in program order — wins, preserving the invariant even when the naive register-name match alone is ambiguous. 6. The `Rd != 0` guard prevents false hazards on architectures with a hardwired zero register, preserving the invariant that "no write" is never mistaken for "a real write." By induction over instructions in program order, the invariant holds for every instruction, so every read in the pipelined machine sees exactly the value a sequential machine would have produced — establishing that forwarding preserves the sequential-semantics contract that pipelining as a whole must not violate.
Tradeoffs, honestly. Forwarding is not free: it adds combinational-logic depth and mux delay directly into the EX stage's critical path, potentially lengthening the clock period rather than the instruction count — a classic time/complexity-of-hardware tradeoff rather than a time/instruction-count one. Real designs must balance how many forwarding paths to build (from EX/MEM, MEM/WB, and in more aggressive designs even further back, or from store-buffers, floating-point pipelines, etc.) against the wiring cost and the mux delay added to every instruction's ALU input, whether or not it needs forwarding that cycle.
Worked example
Trace this instruction sequence through the pipeline, register file initially `t2=20, t6=2`: ``` I1: add t1, t0 = 10 + 20 = 30 I2: sub t0, t3 = 30 - 1 = 29 (needs I1's t5, t6 ; t5 = 30 & 2 = 2 (needs I1's t0, two instrs. back) I4: add t3, t7 = 29 + 2 = 31 (needs I2's t5, one back) ```
| Cycle | I1 (add) | I2 (sub) | I3 (and) | I4 (add) | Forwarding action this cycle |
|---|---|---|---|---|---|
| 1 | IF | — | |||
| 2 | ID | IF | — | ||
| 3 | EX (computes 30) | ID | IF | I1's ALU result 30 latched into EX/MEM at end of cycle | |
| 4 | MEM | EX | ID | IF | ForwardA for I2 = FROM_EX_MEM (EX_MEM.Rd=t0) → uses 30 instead of stale register-file value; I2 computes 29 |
| 5 | WB | MEM | EX | ID | ForwardA for I3 = FROM_MEM_WB (MEM_WB.Rd=t0, no EX/MEM match since I2 writes $t3) → uses 30; I3 computes 2. Also, I2's result 29 now latched into EX/MEM. |
| 6 | WB | MEM | EX | ForwardA for I4 = FROM_MEM_WB (MEM_WB.Rd=t3) → uses 29. ForwardB for I4 = FROM_EX_MEM (EX_MEM.Rd=t5) → uses 2. I4 computes 31. | |
| 7 | WB | MEM | — | ||
| 8 | WB | $t7 = 31 written to register file |
Every dependent value (`t3` and `$t5` both used by I4 in the same cycle from different forwarding sources) reaches its consumer through a forwarding path, and the pipeline never stalls — four instructions complete in 8 cycles (versus 4 in a fully sequential 5-stage-latency machine that never overlaps, which would take 20 cycles), demonstrating forwarding's throughput benefit even under dense dependencies. Note cycle 6 in particular: I4 needs two different registers from two different distances simultaneously, and ForwardA and ForwardB are computed independently — this is exactly why the multiplexers and forwarding logic are duplicated per ALU input rather than shared.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Forgetting the `Rd != 0` (or "no destination") guard | Treating every instruction as if it always writes a real register | False hazards forward garbage between unrelated instructions that both nominally target a hardwired-zero or don't-care destination |
| Wrong priority between EX/MEM and MEM/WB forwarding | Not reasoning about which producer is closer in program order | Pipeline forwards a stale value from two instructions back even though a fresher one exists one instruction back |
| Assuming forwarding fixes load-use hazards | Treating all RAW hazards as identical in timing | A load's data isn't ready until MEM completes; an immediately following consumer's EX has already passed — this needs a stall, not just a mux, see Stalling and the Load-Use Hazard |
| Only forwarding to the EX stage | Forgetting that stores, branches, or other stages may also consume register values that could be stale | Branch-comparison hazards or store-data hazards go unhandled if the design only wires forwarding into the ALU |
| Comparing register values instead of register numbers | Confusing what the hazard-detection hardware actually has available combinationally | The forwarding unit must compare register indices (which are known early via decode), not compare data values (which is what's actually in question) |
Pioneers & history
Forwarding grew directly out of the pipelining work pioneered for early high-performance computers, where designers first confronted the fact that overlapping instruction execution creates genuine, hardware-visible ordering hazards that must be resolved without breaking the illusion of sequential execution.
Origins · 1 minJohn L. Hennessy and how this was discovered
Where this matters
Every classroom RISC-V or MIPS pipeline simulator implements exactly the EX/MEM and MEM/WB forwarding paths described here, and every real in-order embedded core — from ARM Cortex-M pipelines to simple RISC-V cores used in microcontrollers — needs equivalent bypass logic to keep single-issue throughput close to one instruction per cycle. In high-performance out-of-order superscalars (modern x86 and ARM server/desktop cores), the same idea scales up dramatically: instead of two forwarding paths, there is a bypass network connecting the outputs of many functional units (integer ALUs, multipliers, load/store units, floating-point units) to the reservation stations or issue queues of many other units, all governed by tag-matching logic that generalizes the simple register-number comparison used here. Compilers and instruction schedulers also reason about forwarding latency directly: they know which operand a just-issued instruction can supply "for free" next cycle versus which result needs extra cycles to become visible, and they reorder independent instructions to hide any residual latency — a compiler-side complement to the hardware technique, and a bridge to the broader story of hazards continued in Control Hazards.
Ask the tutor: How does the forwarding-unit design change once the pipeline supports a load-use stall alongside these two forwarding paths?
Does forwarding eliminate all stalls in a pipelined processor?
No. Forwarding eliminates stalls for RAW hazards between an ALU-type producer and any later consumer, because the producer's result exists in a pipeline latch (EX/MEM or MEM/WB) before the consumer's EX stage needs it. It cannot eliminate the load-use hazard, where the producer is a load instruction: the loaded value isn't ready until the end of the MEM stage, which is too late for an immediately following instruction's EX stage in the very next cycle. That specific case still requires a one-cycle stall (a bubble) inserted by hazard-detection hardware, covered separately under load-use hazards. Control hazards from branches are a different category entirely and need their own resolution techniques such as prediction or delay slots.
Why doesn't forwarding just add a cycle for every dependency, the way stalling does?
Forwarding works because the value a later instruction needs already exists physically in a pipeline register several cycles before it would normally reach the register file via writeback — the pipeline just hadn't wired a path to read it early. Since combinational logic (a multiplexer selecting among a few sources) operates within a single clock cycle, routing that already-computed value directly to where it's needed costs no extra time, only extra wiring and mux logic. Stalling, by contrast, is needed only when the value literally does not exist yet at the moment the consumer's EX stage occurs — no amount of wiring can deliver a number that hasn't been computed.
How does the forwarding unit know which register numbers to compare, given that it must act in the same cycle?
Register source and destination numbers are decoded very early — during the ID stage — and then carried forward as metadata alongside the data in each pipeline register (ID/EX, EX/MEM, MEM/WB). The forwarding unit's comparisons are therefore just equality checks on small integers already sitting in latches at the start of the cycle; they involve no ALU computation and settle well within the EX stage's clock period. This is why forwarding is described as adding "comparators and multiplexers," not additional pipeline stages — the register-number bookkeeping was already free, since decode has to identify source and destination registers regardless.
Can a single instruction need forwarding on both of its operands simultaneously, from different sources?
Yes, and this is common. Consider an instruction whose first operand was defined by the immediately preceding instruction (needs EX/MEM forwarding) and whose second operand was defined two instructions earlier (needs MEM/WB forwarding). The forwarding unit computes ForwardA and ForwardB independently, each with its own priority logic between EX/MEM and MEM/WB, and each drives its own dedicated multiplexer on the corresponding ALU input. There is no conflict because the two operand paths are entirely separate pieces of hardware; the only shared resource is the small comparator logic, which is replicated per operand.
Is forwarding specific to five-stage pipelines, or does it generalize to deeper or wider pipelines?
The core idea — route a result from wherever it is first computed straight to wherever it is next needed, skipping the architectural register file — generalizes completely. Deeper pipelines need forwarding paths from more stages further back; wider, superscalar and out-of-order pipelines need a full bypass network connecting many functional-unit outputs to many reservation stations, with tag-based matching replacing simple register-number comparison. The five-stage case is the cleanest teaching example because it has exactly two nontrivial forwarding distances, but the underlying principle — never wait for writeback if the value already physically exists — is identical in every pipelined machine.
What happens if two forwarding sources both match the same operand at once?
This can only happen if two different in-flight instructions wrote (or claim to write) the same destination register, and both are candidates to forward to the same consumer. Because instructions retire writeback in program order in a simple in-order pipeline, the instruction currently in MEM (heading to EX/MEM) is always more recent in program order than the instruction currently in WB (heading to MEM/WB) when both match the same register. The forwarding unit's priority rule — always prefer EX/MEM over MEM/WB when both match — resolves the conflict by choosing the most recent value, which is exactly the value a sequential machine would have used.
Does forwarding change what the compiler or programmer needs to do?
In a pipeline with full forwarding for ALU-to-ALU dependencies, the compiler can schedule dependent arithmetic instructions back-to-back with no performance penalty and no correctness concern — the hardware guarantees the right value appears, transparently. The programmer or compiler only needs to be aware of the residual hazard forwarding cannot cover, the load-use hazard, where inserting an independent instruction between a load and its first use avoids a stall the hardware would otherwise insert automatically. Beyond that, forwarding is designed to be invisible above the instruction set architecture: it affects timing and internal wiring, never the observable semantics of a program.