5.2.6 · D3Processor Datapath & Pipelining

Worked examples — Data hazards and forwarding - bypassing

2,677 words12 min readBack to topic

This page is the "hands dirty" companion to the parent topic. The parent told you what forwarding is. Here we run the machine on every kind of case it can throw at you — one instruction pair after another — until no surprise is left.

Before we start, one promise: we will never use a stage name, a register-name, or a pipeline-register name without saying what it is first. Let's set that vocabulary down once, as a picture, and then reuse it.

Figure — Data hazards and forwarding - bypassing

Look at the picture: the two shelves (EX/MEM, MEM/WB) each have a red return wire looping back to the mux in front of the ALU. All of forwarding is deciding when to flip those muxes.


The scenario matrix

Every worked example below is tagged with the cell(s) it covers. Together they fill the whole table.

Cell What varies Question it answers
A. Distance = 1 (EX→EX) producer 1 instr ahead Forward from EX/MEM
B. Distance = 2 (MEM→EX) producer 2 instr ahead, gap between Forward from MEM/WB
C. Distance ≥ 3 producer far ahead No forward — register file already has it
D. Double hazard / priority two producers write the same register Which shelf wins?
E. Load-use (the one forwarding can't fix) producer is a LW, distance 1 1 stall then forward
F. Zero/degenerate: R0 destination producer writes R0 No hazard at all
G. Both operands hazarded Rs and Rt both stale Two muxes fire at once
H. Store-uses-result consumer is a SW Forward to MEM, not EX
I. Word problem (real code) compiler-scheduled loop Count stalls saved
J. Exam twist reordering to kill a stall See 6.2.03-Compiler-optimization-techniques

Notation used from here on (defined, then reused):

  • = destination register of a producer instruction (the one it writes).
  • = the two source registers a consumer reads.
  • "EX/MEM.Rd" = the value on shelf EX/MEM's destination-number line. Same for MEM/WB.
  • The forwarding rule from the parent, restated so we can apply it:

Now, the examples.


Steps

  1. Line them up in time. I1 is in EX at cycle 3, MEM at cycle 4. I2 is in EX at cycle 4. Why this step? Forwarding is a timing question — the ALU input for I2 is needed exactly at its EX (cycle 4). We must know where I1's answer physically is at cycle 4.
  2. Locate the answer at cycle 4. I1 just left EX at end of cycle 3, so its result sits on shelf EX/MEM during cycle 4. Why this step? That shelf is the freshest copy — and it's exactly the wire the mux can grab.
  3. Apply the EX/MEM check. RegWrite=1 (ADD writes), Rd = R1 ≠ 0, and EX/MEM.Rd (R1) = I2's Rs (R1). ✔ Forward from EX/MEM.
  4. Count stalls. The value is available the instant it's needed → 0 stalls.

Verify: Stall formula for distance-1 ALU→ALU with forwarding gives ; without forwarding the parent showed a 2-cycle gap. Saved = 2 cycles. ✔ (checked in VERIFY as ex1_stalls).


  1. Time-line. I1 EX@3, MEM@4, WB@5. I3 EX@5. Why: we again compare "when needed" (I3's EX = cycle 5) against "where the answer is."
  2. Locate R1 at cycle 5. I1 left EX two ticks ago; its result has slid to shelf MEM/WB by cycle 5. Why: one tick per shelf — EX/MEM at cycle 4, MEM/WB at cycle 5.
  3. Apply checks. EX/MEM at cycle 5 holds I2's result (R6), which ≠ R1 → EX/MEM check fails. MEM/WB.Rd = R1 = I3's Rs → forward from MEM/WB.
  4. Stalls = 0. The independent AND naturally filled the gap.

Verify: distance-2 with forwarding → 0 stalls (ex2_stalls). ✔


  1. Time-line. I1 WB@5. I4 ID@5, EX@6. Why: now we compare against I4's ID (cycle 5), where source registers are read from the file.
  2. Key timing fact — the write-then-read split clock. Register files write in the first half of a cycle and read in the second half (the "write-before-read" file, see 3.1.05-Register-file-design). At cycle 5, I1's WB writes R1 in the first half; I4's ID reads it in the second half. Why: this is exactly why distance-3 needs no forwarding — the file already delivers the fresh value.
  3. Conclusion. Both forwarding checks fail (no shelf still holds R1 by I4's EX), and none is needed. 0 stalls, 0 forwards.

Verify: distance ≥ 3 → 0 stalls, 0 forwards (ex3_forwards). ✔


  1. I2's own hazard (Rs = R1 from I1). At I2's EX (cycle 4), I1 is on EX/MEM → forward EX/MEM to I2's first ALU input. (Cell A again, nested.) Why: every dependency is checked independently; I2 is a consumer and a producer.
  2. I3 at its EX (cycle 5). Who holds R1?
    • EX/MEM holds I2's brand-new R1 (I2 left EX at cycle 4).
    • MEM/WB holds I1's older R1. Both match! Why the priority rule? I3 must use the most recent write to R1, which is I2's. EX/MEM wins.
  3. Both operands of I3 are Rs=R1 and Rt=R1. Two separate muxes (one per ALU input) both select EX/MEM. Why: Rs and Rt have their own forwarding logic; nothing forbids both firing simultaneously.
  4. Result: I3 receives I2's R1 on both inputs. 0 stalls.

Verify: correct value chain — if R2=10,R3=5 then I1:R1=15, I2:R1=15+R6(=2)=17, I3:R4=17−17=0 (ex4_r4). ✔


Figure — Data hazards and forwarding - bypassing
  1. When is a load's data ready? An ALU op finishes in EX. A load finishes only after MEM (the memory read is its work). So I1's R1 is not real until end of cycle 4. Why this step: the whole point — the producer's result appears one station later than for arithmetic.
  2. When does I2 need it? I2's EX is cycle 4 — the same cycle the load is still reading memory. The data does not exist yet. No wire can carry a value that isn't computed.
  3. The fix: 1 stall, then forward. Freeze I2 for one cycle (a bubble). Now I2's EX is cycle 5. I1's loaded R1 sits on MEM/WB at cycle 5 → forward MEM/WB to I2. Why: stalling buys the one tick MEM needs; forwarding then covers the remaining gap.
  4. Cost: exactly 1 stall — not 2, because forwarding still saves one. Look at the figure: the red forward arrow starts at MEM/WB, after the single grey bubble.

Verify: load-use with forwarding → exactly 1 stall (ex5_stalls). ✔


  1. Apply the EX/MEM check literally. RegWrite=1, EX/MEM.Rd = R0, and R0 = I2's Rs. Two of three match — but the guard fails. Why the guard exists: R0 is always 0 in MIPS/RISC-V; the register file ignores writes to it. Its value can never change, so it can never be stale.
  2. Conclusion: forwarding suppressed. I2 reads the constant 0 from the file. 0 stalls, 0 forwards.

Verify: R0 destination → forward=False (ex6_forward). ✔


  1. What does a store actually need R1 for? Not arithmetic — the ALU only computes the address 0+R6. R1 is the data being written, and that's used in the MEM stage. Why this matters: the "needed at EX" rule was for operands. Store-data is needed one stage later.
  2. Timing. I2's MEM is cycle 5. I1's R1 is on MEM/WB at cycle 5 → forward MEM/WB straight into the store-data path at MEM (a separate mux, "MEM forwarding"). Why a different mux: the value's destination is the memory-write port, not the ALU input.
  3. Stalls = 0. Forwarding to MEM has an extra station of slack.

Verify: store consumer → 0 stalls (ex7_stalls). ✔


  1. LW → ADD. Load-use, distance 1 → 1 stall (Cell E). The loaded R1 forwards from MEM/WB after the bubble. Why: this is the one unavoidable stall in the loop.
  2. ADD → SW. ADD produces R1 in EX; SW needs it as store-data at MEM → forward MEM/WB to MEM (Cell H). 0 stalls.
  3. SW, ADDI independent of R1 (ADDI touches R2). No hazard. 0 stalls.
  4. Total per iteration = 1 stall.

Verify: total loop stalls = 1 (ex8_stalls). ✔ Performance impact of that 1 stall/iteration is analysed in 5.2.09-Pipeline-performance-analysis.


  1. The stall is LWADD (they're adjacent). If some independent instruction sits between them, the bubble is filled by useful work. Why: recall Example 2 — an independent instruction converts distance-1 into distance-2, and distance-2 needs no stall.
  2. ADDI R2,R2,4 is independent of R1 — but it writes R2, which SW ... 0(R2) and the next LW 0(R2) use. Reordering it changes addresses. Not free. The exam answer: compute the next address into a scratch register, OR unroll so an independent load fills the slot. Why: the twist tests whether you notice the R2 side-dependency (a WAR-flavoured concern) — you cannot blindly move it.
  3. Scheduled version (unrolled two elements, loads hoisted):
    LW   R1, 0(R2)
    LW   R4, 4(R2)     # independent load fills the LW->ADD gap
    ADD  R1, R1, R3    # R1 now ready, no stall
    ADD  R4, R4, R3
    SW   R1, 0(R2)
    SW   R4, 4(R2)
    ADDI R2, R2, 8
  4. Stalls now = 0 — the second load occupies the bubble slot for the first ADD.

Verify: scheduled loop stalls = 0 (ex9_stalls). ✔ This scheduling is exactly what out-of-order engines do in hardware — see 5.3.02-Superscalar-out-of-order-execution.


Recall Self-check

Distance-1 ALU→ALU with forwarding costs how many stalls? ::: 0 (forward from EX/MEM). A load feeding the very next instruction costs how many stalls? ::: 1 (then forward from MEM/WB). Two producers write R5; consumer reads R5 — which shelf wins? ::: EX/MEM (the more recent result). Producer writes R0 — forward or not? ::: Not — the guard blocks it. A SW's stored register is forwarded to which stage? ::: MEM (store-data port), not EX.