CMP-310 · Lesson 3 of 12 · 22 min

The pipelined datapath: forwarding, stalls and exceptions

CMP-229 built a single-cycle processor and then cut it into five stages to make a pipeline. That first pipeline showed the idea: while one instruction executes, the next is decoded and the one after that is fetched, so in the best case one instruction finishes every cycle. This lesson opens the pipeline up and looks at the hardware that makes it work correctly at speed: the pipeline registers that carry each instruction's state, the forwarding unit that passes results directly between stages, the hazard detection unit that inserts stalls when forwarding cannot help, and the mechanisms that let a pipelined processor stop cleanly when an instruction faults. These details decide whether a real pipeline achieves a CPI near 1 or wastes a third of its cycles.

Maren's Core A is exactly this kind of machine: a classic five-stage, single-issue, in-order RISC-V pipeline. Before comparing it with Core B's dual-issue design, Jonah needs to understand where Core A's measured CPI of 1.40 comes from, and how much of it the compiler could recover by scheduling instructions better.

The five stages and their pipeline registers

The classic pipeline has five stages. Instruction fetch, IF, reads the instruction at the program counter and computes PC + 4. Instruction decode, ID, decodes the instruction, reads up to two source registers and builds the immediate. Execute, EX, performs the ALU operation, computes a memory address or evaluates a branch condition. Memory access, MEM, reads or writes data memory for loads and stores. Write back, WB, writes the result into the register file.

Between each pair of stages sits a pipeline register, named for the stages it separates: IF/ID, ID/EX, EX/MEM and MEM/WB. On every clock edge, each register captures everything the next stage needs about the instruction that just left the previous stage: operand values, the destination register number, the immediate, and the control signals that the decoder generated. Control is decided once, in ID, and then travels down the pipeline with its instruction, so that WB knows to write register x6 three cycles after decode chose to. Seeing the pipeline registers as the place where each instruction's identity lives makes forwarding and exceptions much easier to reason about.

A simplified five-stage datapath drawn left to right with blocks for instruction memory, register file, ALU and data memory, separated by four vertical pipeline registers labeled IF/ID, ID/EX, EX/MEM and MEM/WB. Two forwarding paths run backward from the EX/MEM and MEM/WB registers to multiplexers in front of the ALU inputs. A hazard detection unit sits above the ID stage with a line to the program counter and IF/ID register.
The five-stage pipeline with pipeline registers, the forwarding paths into the ALU and the hazard detection unit that stalls fetch and decode.

Data hazards and the forwarding unit

A data hazard arises when an instruction needs a value that an earlier instruction has not yet written to the register file. In the sequence add x6, x5, x7 followed by sub x8, x6, x9, the add computes x6 at the end of its EX stage but does not write it until WB, two cycles later, while the sub wants x6 at the start of its own EX stage, one cycle after the add's. Waiting for the write would cost two stall cycles every time a result is used immediately, which in typical code is very often.

Forwarding, also called bypassing, removes most of those stalls. The value the sub needs already exists: it sits in the EX/MEM pipeline register at the moment the sub enters EX. Multiplexers in front of the ALU inputs can choose that value instead of the stale one read from the register file. A forwarding unit compares the source register numbers of the instruction in EX with the destination register numbers of the instructions in MEM and WB and steers the multiplexers. Its conditions, for the first ALU input, read as follows.

\[\text{if } \mathit{EX/MEM.RegWrite} \wedge \mathit{EX/MEM.rd} \neq 0 \wedge \mathit{EX/MEM.rd} = \mathit{ID/EX.rs1}: \quad \text{ForwardA} \gets \text{EX/MEM}\]
\[\text{else if } \mathit{MEM/WB.RegWrite} \wedge \mathit{MEM/WB.rd} \neq 0 \wedge \mathit{MEM/WB.rd} = \mathit{ID/EX.rs1}: \quad \text{ForwardA} \gets \text{MEM/WB}\]

Three details matter. The check rd ≠ 0 prevents forwarding a value to an instruction that reads x0, which must always read as zero. The EX/MEM condition is tested first, because when two earlier instructions both write the same register, the more recent one holds the correct value. And the register file itself is designed to write in the first half of a cycle and read in the second, so an instruction in ID can read a value being written by an instruction in WB in the same cycle, covering a dependence three instructions apart without any forwarding path.

The load-use hazard and the hazard detection unit

Forwarding cannot fix every dependence. A load produces its value at the end of MEM, one stage later than an ALU instruction. If the very next instruction uses the loaded register in EX, the value does not yet exist when it is needed, and no wire can send data backward in time. The pipeline must stall for one cycle. A hazard detection unit in ID checks whether the instruction in EX is a load whose destination matches a source of the instruction in ID. If so, it holds the program counter and the IF/ID register for a cycle, so that fetch and decode repeat, and it inserts a bubble by setting all the control signals in ID/EX to zero, which turns that slot into a no-operation as it flows down the pipeline.

\[\text{stall if } \mathit{ID/EX.MemRead} \wedge \big(\mathit{ID/EX.rd} = \mathit{IF/ID.rs1} \vee \mathit{ID/EX.rd} = \mathit{IF/ID.rs2}\big)\]

Compilers reduce load-use stalls by scheduling: moving an independent instruction between a load and its first use. The optimization is cheap and often effective, because loop bodies usually contain independent work. On Core A, Jonah finds that the classifier's inner loop loads a filter coefficient and immediately multiplies by it; reordering the loop so that the next coefficient is loaded one iteration early removes the stall.

A pipeline diagram with clock cycles 1 to 9 across the top. A load instruction occupies IF, ID, EX, MEM and WB in cycles 1 to 5. The dependent add behind it is fetched in cycle 2, held in ID during cycles 3 and 4 with a bubble shown in EX during cycle 4, then executes in cycle 5. An arrow shows the loaded value forwarded from the end of MEM in cycle 4 to the start of EX in cycle 5.
A load-use hazard. Even with forwarding, the dependent instruction waits one cycle, and a bubble moves down the pipeline in its place.

Worked problem

Counting stalls with and without forwarding

Jonah traces six instructions from the filter loop: lw x5, 0(x10); add x6, x5, x7; sub x8, x6, x9; lw x11, 4(x10); sw x8, 8(x10); and x12, x11, x8. Assume the register file writes in the first half of a cycle and reads in the second. How many stall cycles occur without forwarding, and how many with full forwarding?

Show solution
  1. Without forwarding, a consumer can read a value in ID only in the producer's WB cycle, so a dependence on the previous instruction costs 2 stalls.
  2. The add waits for x5 (2 stalls); the sub waits for x6 (2 stalls); the sw waits for x8, now two instructions back (1 stall); the final and finds x11 ready in time.
  3. \[ \text{stalls}_{\text{no forwarding}} = 2 + 2 + 1 = 5\]
  4. With forwarding, ALU results reach the next instruction with no stall; only the load-use pair lw x5 then add costs 1 stall. The second load is followed by the sw, which hides its latency from the and.
  5. \[ \text{stalls}_{\text{forwarding}} = 1\]

Answer: Forwarding removes four of the five stalls. The remaining one is a load-use stall, which only instruction scheduling can remove.

The program below performs the same count mechanically. For each instruction it finds the earliest cycle in which the instruction can enter EX, given when its source registers become available, and reports the stalls. It is a small version of the cycle-level simulators architects use to evaluate pipelines.

Cycle counts for the six-instruction sequence with and without forwarding.
# Five-stage pipeline timing: count cycles with and without forwarding
# Each instruction: (text, dest, sources, is_load)
prog = [("lw   x5, 0(x10)",  "x5", ["x10"], True),
        ("add  x6, x5, x7",  "x6", ["x5", "x7"], False),
        ("sub  x8, x6, x9",  "x8", ["x6", "x9"], False),
        ("lw   x11, 4(x10)", "x11", ["x10"], True),
        ("sw   x8, 8(x10)",  None, ["x8", "x10"], False),
        ("and  x12, x11, x8", "x12", ["x11", "x8"], False)]

def run(forwarding):
    ready = {}            # register -> first cycle its value can be used in EX
    ex_prev = 0           # cycle the previous instruction was in EX
    for i, (text, rd, srcs, is_load) in enumerate(prog):
        ex = max([ex_prev + 1, i + 3] + [ready.get(r, 0) for r in srcs])
        stalls = ex - max(ex_prev + 1, i + 3)
        if rd:
            if forwarding:
                ready[rd] = ex + (2 if is_load else 1)   # load data after MEM, ALU result after EX
            else:
                ready[rd] = ex + 3                        # written in WB, read in ID the same cycle
        print(f"  {text:18s} EX in cycle {ex:2d}  stalls {stalls}")
        ex_prev = ex
    total = ex_prev + 2       # MEM and WB of the last instruction
    return total

for fw in (False, True):
    print("forwarding" if fw else "no forwarding")
    t = run(fw)
    print(f"  total cycles {t}, CPI over {len(prog)} instructions = {(t - 4) / len(prog):.2f}")
Output
no forwarding
  lw   x5, 0(x10)    EX in cycle  3  stalls 0
  add  x6, x5, x7    EX in cycle  6  stalls 2
  sub  x8, x6, x9    EX in cycle  9  stalls 2
  lw   x11, 4(x10)   EX in cycle 10  stalls 0
  sw   x8, 8(x10)    EX in cycle 12  stalls 1
  and  x12, x11, x8  EX in cycle 13  stalls 0
  total cycles 15, CPI over 6 instructions = 1.83
forwarding
  lw   x5, 0(x10)    EX in cycle  3  stalls 0
  add  x6, x5, x7    EX in cycle  5  stalls 1
  sub  x8, x6, x9    EX in cycle  6  stalls 0
  lw   x11, 4(x10)   EX in cycle  7  stalls 0
  sw   x8, 8(x10)    EX in cycle  8  stalls 0
  and  x12, x11, x8  EX in cycle  9  stalls 0
  total cycles 11, CPI over 6 instructions = 1.17

The sequence takes 15 cycles without forwarding and 11 with it. Subtracting the four cycles needed to fill the pipeline, the CPI over these six instructions falls from 1.83 to 1.17. Over a long program the fill cost disappears, and the remaining excess over 1.0 comes from load-use stalls, branches and cache misses.

Structural hazards

A structural hazard occurs when two instructions need the same hardware resource in the same cycle. The classic five-stage pipeline avoids most of them by design: separate instruction and data caches let IF and MEM access memory in the same cycle, and the register file has two read ports and one write port, enough for one instruction per stage. Structural hazards reappear when a unit is not fully pipelined. Core A's integer divider, for example, takes 34 cycles and cannot accept a new divide until the previous one finishes, so two divides close together stall the second. The hazard detection unit handles such cases by checking a busy flag. In practice designers accept rare structural hazards when the hardware to remove them would cost more than the stalls.

Control hazards, briefly

Branches create control hazards: the pipeline must fetch the next instruction before it knows whether the branch is taken. In the basic pipeline, the branch outcome is known in EX, so if the pipeline keeps fetching sequentially and the branch is taken, the two instructions fetched behind it must be flushed, costing two cycles. Moving the comparison into ID cuts the penalty to one cycle at the cost of a comparator and forwarding paths in decode. Lesson 4 treats control hazards properly, with branch prediction and speculation, because they become the dominant cost as pipelines grow deeper and wider.

Exceptions and precise interrupts

An exception is an unexpected event that changes control flow: an illegal instruction, a misaligned access, a page fault, an environment call, or an external interrupt from a timer or the radar's data-ready line. A pipeline holds up to five instructions in flight at once, so when one of them causes an exception the processor must decide what state to present to the handler. An exception is precise if all instructions before the faulting one have completed and none after it has changed any architectural state. Precise exceptions let the handler fix the problem, for example by loading a missing page, and resume the program exactly where it stopped. RISC-V requires precise exceptions.

The five-stage pipeline achieves precision by recording exceptions in the pipeline registers instead of acting on them immediately. An instruction that faults in ID or EX carries a flag down the pipeline; when it reaches the point of commitment, conventionally the end of MEM, the processor flushes it and every younger instruction, writes the faulting instruction's address into the exception program counter register, records the cause, and fetches from the handler's address. Because older instructions have all passed that point and younger ones have not written anything, the state is precise. If two instructions fault in the same cycle, the older one wins, because it is further down the pipeline. Lesson 6 shows how out-of-order processors keep the same promise with a reorder buffer.

How deep should a pipeline be?

Splitting work into more stages shortens each stage and raises the clock rate, but each stage adds the delay of a pipeline register and its clock skew, which does not shrink. Hazards also cost more cycles in deeper pipelines, because more instructions sit between the point where a value is produced and the point where it is needed, and a mispredicted branch flushes more work. Performance therefore rises with depth at first and then falls. Studies of real workloads have put the optimum for general-purpose code somewhere around 15 to 20 stages for high-end designs, while power-conscious embedded cores like the NS-5 candidates use 3 to 8 stages, because every pipeline register costs energy on every cycle and the clock rate is limited by power long before it is limited by logic depth.

\[t_{\text{cycle}} \approx \frac{t_{\text{logic}}}{k} + t_{\text{latch}}, \qquad CPI \approx 1 + h \, k\]

In this rough model, k is the number of stages, t_logic the total logic delay, t_latch the per-stage overhead and h a hazard rate that grows the stall cost in proportion to depth. Multiplying the two gives a time per instruction that is minimized at a finite k, which is the quantitative version of the argument above.

Common mistakes

  • MistakeForgetting the rd ≠ 0 check in forwarding logic.

    Insteadx0 always reads as zero; an instruction that writes x0 must never forward its discarded result.

  • MistakeTesting the MEM/WB forwarding condition before EX/MEM.

    InsteadCheck EX/MEM first; when two earlier instructions write the same register, the more recent value is correct.

  • MistakeBelieving forwarding removes all data stalls.

    InsteadA load followed immediately by a use still needs one stall in the five-stage pipeline.

  • MistakeHandling an exception as soon as it is detected in an early stage.

    InsteadRecord it in the pipeline registers and act at the commit point, so older instructions complete and younger ones are flushed.

  • MistakeAssuming a deeper pipeline is always faster.

    InsteadLatch overhead and growing hazard penalties mean performance peaks at a finite depth.

Summary and key terms

  • Pipeline registers carry each instruction's data and control signals between stages.
  • Forwarding passes results from EX/MEM and MEM/WB to the ALU inputs, removing most data stalls.
  • A load followed by an immediate use needs one stall; the hazard detection unit inserts a bubble.
  • Compilers schedule independent instructions after loads to hide load-use latency.
  • Structural hazards arise from shared or unpipelined resources such as a divider.
  • Precise exceptions are achieved by acting on faults at the commit point and flushing younger instructions.
  • Pipeline depth trades clock rate against latch overhead and hazard penalties.
Pipeline register
A register between stages that holds an instruction's data and control for the next stage.
Forwarding
Routing a result from a later pipeline stage directly to an earlier stage that needs it; also called bypassing.
Load-use hazard
A dependence of an instruction on a load immediately before it, requiring a stall.
Bubble
An empty slot inserted into the pipeline that does no work.
Structural hazard
A conflict when two instructions need the same hardware resource at once.
Precise exception
An exception after which all earlier instructions have completed and no later instruction has changed state.
Exception program counter
A register that records the address of the instruction that caused an exception.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. In the sequence lw x1, 0(x2); add x3, x1, x4, how many stall cycles occur with forwarding in the five-stage pipeline?

    Show answer

    One: the loaded value is available only after MEM, one cycle too late for the add's EX stage.

  2. Why must the forwarding unit check EX/MEM before MEM/WB?

    Show answer

    If both earlier instructions write the same register, the one in EX/MEM is more recent and holds the value the program expects.

  3. A program has 25 percent loads, and 40 percent of loads are followed immediately by a use. What CPI do load-use stalls add?

    Show answer

    0.25 × 0.40 × 1 = 0.10 cycles per instruction.

  4. What does the processor do when an instruction in EX raises an illegal-instruction exception?

    Show answer

    It marks the instruction; at the commit point it flushes it and all younger instructions, saves its PC and cause, and jumps to the handler.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz