From Transistor to Browser

The Machine · Chapter 5.b
Optional read: How does a CPU go faster?

Updated Oct 12, 2026
Summarize
Transcript

How does a CPU go faster? Ours runs three steps per instruction, one at a time. Four instructions take twelve steps.

Easy, I thought: just run the clock faster. But each step is gates settling, and they won't settle any quicker.

Look at the steps again: While one instruction executes, the fetch part just sits there. So fetch the next one right away. Cycle one: fetch the first. Cycle two: decode it, and fetch the second. Cycle three: three instructions in flight, one in each step. From then on, one finishes every cycle. Four take six cycles, not twelve. The catch? If one needs an answer that isn't ready, it waits a cycle.

Faster, and no step got quicker! Overlapping the steps is a pipeline, and once it's full, one instruction finishes every cycle.

Below, run instructions that don't need each other, then a chain that does. Where do the gaps come from?

The hosts' voices are synthetic, made with Breeze TTS 2 by BreezeBlue.

  1. The browser
  2. The network
  3. Software and languages
  4. The CPU
  5. Memory
  6. Arithmetic
  7. Logic gates
  8. The switch
Figure 5.b.1: Where we are in the climb from transistor to browser: still on the processor. A timing model shows how overlapping instruction stages can make a core faster.

The processor of Chapter 5 runs one instruction all the way through, fetch then decode then execute, before it starts the next. If the stages have the resources they need, one instruction can execute while the next decodes and the one after that fetches. Overlapping stages this way is a pipeline. The accumulator CPU cannot simply overlap every operation: LOAD, ADD, and STORE also use memory during execute and can compete with fetch.

To isolate timing, this chapter uses a separate machine with several named registers and three one-cycle stages. Its instruction records say which registers are read and written; they are not the CPU’s encoded bytes. Fetch and execute are assumed to have independent resources, so we can focus on waiting for an operand and guessing a branch. The code schedules these records and computes their values sequentially; it does not call or pipeline CPU.step().

5.b.1 Overlapping the stages

If a new instruction starts each cycle and every instruction takes three stages, then once the pipeline fills, three instructions are always in flight and one finishes every cycle. Four instructions that would take twelve cycles one at a time finish in six. Figure 5.b.2 lays the stages out on a timeline.

pipeline-ganttpipeline-gantt
Figure 5.b.2: Four independent instructions in the pipeline. Each one’s fetch, decode, and execute run a cycle behind the instruction above it, so the stages overlap. At cycle three, three instructions are in flight at once.

The interactive version needs the in-browser runtime.

Scheduling the pipeline is bookkeeping over cycle numbers: place each instruction one cycle after the last, unless it must wait.

Example 5.b-1. Scheduling instructions through the pipeline

def schedule(program: list) -> tuple:
    """Place each instruction's three stages on a timeline, in order, stalling an
    instruction until every register it reads has been written back. Returns the
    per-instruction stage cycles and the total number of cycles."""
    execute = []          # the cycle each instruction reaches its execute stage
    written = {}          # register -> index of the last instruction that writes it
    for index, ins in enumerate(program):
        earliest = execute[index - 1] + 1 if index else _STAGES - 11
        for src in ins.srcs:
            producer = written.get(src)
            if producer is not None:
                earliest = max(earliest, execute[producer] + 2)2
        execute.append(earliest)
        written[ins.dst] = index
    stages = [{"fetch": e - 2, "decode": e - 1, "execute": e} for e in execute]
    total = execute[-1] + 1 if execute else 03
    return stages, total

5.b.2 When an instruction must wait

The stall in callout 2 is the price of a data hazard. If an instruction needs a value the instruction just ahead of it has not finished computing, it cannot proceed; it has to wait until that value exists. A bubble opens in the pipeline, and everything behind it slips a cycle. Figure 5.b.3 shows one.

hazardhazard
Figure 5.b.3: A data hazard. The second instruction, r2, reads the value the first instruction’s execute is still computing, so it cannot decode until the next cycle. A stall fills the gap.

A pipeline is only allowed to overlap instructions because the result is the same as running them one at a time. Here the schedule models only the timing; the computed result is simply the sequential one, so the program does not prove the equivalence. What it does check is the hazard rule, which is exactly what lets a real overlapped pipeline keep that same result: no instruction reads an operand before the instruction that produces it has computed it.

Example 5.b-2. Checking the schedule is safe

def hazard_safe(program: list, stages: list) -> bool:
    """True if every instruction reads its registers only after the instruction
    that writes each one has reached its execute stage, so no operand is read
    before it exists."""
    written = {}
    for index, ins in enumerate(program):
        for src in ins.srcs:
            producer = written.get(src)
            if producer is not None and \
                    stages[index]["decode"] <= stages[producer]["execute"]:1
                return False
        written[ins.dst] = index
    return True

A real pipeline softens this with forwarding: the instant an instruction computes a value in its execute stage, that value is routed straight to the instruction behind it, rather than making it wait for the write-back. Many stalls disappear, but the shape of the hazard, a value needed before it exists, stays the same.

5.b.3 Guessing the next instruction

A data hazard makes the pipeline wait; a branch makes it guess. A jump does not know where it goes until it executes, but by then the pipeline has already fetched the instructions sitting behind it. So the processor bets on the outcome and fetches that way. A branch predictor makes the bet from past behaviour: a two-bit counter with four states:

State Meaning Prediction
0 strongly not-taken not-taken
1 weakly not-taken not-taken
2 weakly taken taken
3 strongly taken taken

Each taken outcome increases the counter by one, each not-taken outcome decreases it by one, stopping at 3 or 0. From either strong state, two contrary outcomes change the prediction; from a weak state, one is enough. The model starts at 0. After warming up on a loop that runs many times and leaves once, it misses mostly on the way out.

Example 5.b-3. Predicting a branch

def predict(outcomes: list) -> int:
    """Count misses with a two-bit counter starting strongly not-taken (0).

    States 0 and 1 predict not-taken; 2 and 3 predict taken. Two contrary
    outcomes flip the prediction from a strong state, one from a weak state.
    """
    state = 0                                # 0,1 predict not-taken; 2,3 taken
    misses = 0
    for taken in outcomes:
        if (state >= 2) != taken:1
            misses += 1
        state = min(state + 1, 3) if taken else max(state - 1, 0)2
    return misses

When the guess is wrong, the instructions fetched behind the branch were the wrong ones and are thrown away. Figure 5.b.4 shows the cost.

mispredictionmisprediction
Figure 5.b.4: A mispredicted branch. The guess sent the pipeline down the wrong path, so the two instructions fetched behind the branch are flushed, cycles of work thrown away. Deeper pipelines fetch further ahead, so a wrong guess costs even more.

Return to the accumulator CPU with those rules in mind. Its arithmetic instructions repeatedly read and write the same accumulator, so they form dependent chains. Overlapping them would require pipeline storage, hazard control, and either stalls or extra memory access paths when fetch and execute compete. We have studied the scheduling problem, leaving that CPU unchanged. Even with those resources, a pipeline still waits for slow memory. The next chapter asks how a cache reduces that wait.

Exercises