Opening: when the pipeline does not know which instruction comes next

Lesson 4 solved DATA conflicts between overlapping instructions in the pipeline β€” but there is another kind of conflict, more dangerous for THROUGHPUT: a conditional branch (BEQ, BNE) leaves the CPU not knowing WHICH instruction runs next until that branch has actually been computed (at the EX stage, i.e. 2–3 cycles AFTER IF has already fetched the following instruction). This is a control hazard β€” and the way modern CPUs solve it (guess first, then run speculatively) accidentally opened the most serious hardware security hole in history: Spectre.


πŸ“š Prerequisites
Read Lesson 4 (the 5-stage pipeline β€” this lesson's control hazard happens RIGHT AT the IF stage of that same pipeline).

1. Control hazards & why prediction is necessary

With a BEQ, the CPU only knows the next instruction's address FOR CERTAIN once it has computed the comparison at the EX stage (in Lesson 4's model, EX is the 3rd of the 5 stages IF-ID-EX-MEM-WB). But the pipeline has already had to FETCH (IF) the next 2 instructions in the 2 cycles immediately after β€” so without guessing ahead, the CPU has to STOP fetching (stall) until it knows the outcome, losing 1–3 cycles per branch (depending on which stage a given CPU resolves branches at). For a program dense with branches β€” every if/else, every loop β€” that cost adds up enormously if left unhandled.

⚠️ Pitfall: tidying the source code does not remove branch conflicts
Shortening the source (say collapsing several nested ifs into one expression) does NOT remove the branch instructions from the MACHINE CODE produced β€” the compiler still has to emit BEQ/BNE for every logical branch point, however "tidy" the source looks. A control hazard follows from the control structure (the branching logic), not the source length β€” a for loop written on one line produces exactly as many branch instructions as one written out over ten.

2. Static & dynamic branch predictors: 1-bit and 2-bit saturating FSMs

Two abbreviations run through this whole section. FSM stands for finite state machine: a mechanism with only a few defined states, plus a fixed rule saying which state to move to when a given event happens in a given state. A traffic light is a three-state FSM. The branch predictors below are FSMs with 2 or 4 states, and their "event" is the actual outcome of the branch that just resolved.

BHT stands for Branch History Table: instead of using ONE predictor for the whole program, the CPU keeps a table indexed by instruction address, so every branch instruction gets its OWN predictor. That is necessary because different branches have different habits: a loop-exit branch is almost always "taken", while an error-check branch is almost always "not taken" β€” mixing them into one counter makes both predict worse.

The simplest approach β€” static prediction (always guess Not-taken, or always guess Taken) β€” is only about 50% right on a random branch. Dynamic prediction remembers the HISTORY of that particular branch to guess more intelligently, exploiting how strongly repetitive real loop branches are.

branch_predictor_1bit.js (excerpt from the shared cpu-core.js engine)
// 1-bit predictor: remembers ONLY the MOST RECENT outcome, and predicts the same.
function makeBranchPredictor1Bit() {
  let state = 0; // 0 = predict Not-taken (N), 1 = predict Taken (T)
  return {
    predict: () => (state === 1 ? 'T' : 'N'),
    update: (actual) => { state = actual === 'T' ? 1 : 0; },
  };
}
// Verified on the nested-loop sequence (TTTTN repeated 3 times, 15 branches):
// the 1-bit predictor gets only 9/15 = 60% right.

The 2-bit saturating predictor (a saturating counter) uses 4 states instead of 2 β€” a SINGLE wrong guess only nudges the state one notch along rather than flipping the prediction immediately, so it tolerates small amounts of noise far better:

branch_predictor_2bit.js (excerpt from the shared cpu-core.js engine)
// States: 0=Strongly-not-taken 1=Weakly-not-taken 2=Weakly-taken 3=Strongly-taken.
// Predict T once state >= 2. One wrong guess only nudges the state one notch, so a
// single blip does NOT flip the prediction.
function makeBranchPredictor2Bit() {
  let state = 0;
  return {
    predict: () => (state >= 2 ? 'T' : 'N'),
    update: (actual) => {
      state = actual === 'T' ? Math.min(3, state + 1) : Math.max(0, state - 1);
    },
  };
}

// BHT (Branch History Table): every branch ADDRESS gets its OWN 2-bit predictor,
// indexed by pc - different branches have different habits.
function makeBranchHistoryTable() {
  const table = new Map();
  const entryFor = (pc) => {
    if (!table.has(pc)) table.set(pc, makeBranchPredictor2Bit());
    return table.get(pc);
  };
  return {
    predict: (pc) => entryFor(pc).predict(),
    update: (pc, actual) => entryFor(pc).update(actual),
    size: () => table.size,
  };
}
// Verified on the SAME 15-branch sequence: the 2-bit predictor gets 10/15 = 66.7%
// right - better than the 1-bit one.
⚠️ The main pitfall: a 1-bit predictor oscillates constantly on nested loops
With an OUTER loop containing an INNER loop (an extremely common shape: the inner loop's exit branch is Taken 4 times then Not-taken once), the 1-bit predictor "forgets" its state IMMEDIATELY after the first Not-taken β€” producing 2 consecutive mispredictions per outer iteration: one on leaving the inner loop (predicting Taken but actually Not-taken), and one RIGHT AFTER on re-entering it (predicting Not-taken but actually Taken). Verified for real: the 1-bit predictor is only 60% right on this sequence, while the 2-bit one β€” whose saturating behaviour absorbs a single blip without changing its prediction β€” reaches 66.7%.

3. Effective CPI & the cost of a misprediction

Every WRONG guess forces the pipeline to flush the instructions it fetched down the wrong path and refetch along the right one β€” costing PenaltyCycles entirely wasted cycles. The effective CPI adds exactly that cost on top of the ideal CPI:

$$CPI_{eff} = CPI_{ideal} + \text{BranchFrequency} \times \text{MispredictionRate} \times \text{PenaltyCycles}$$

A realistic case: a program where 20% of instructions are branches, the predictor achieves a 10% misprediction rate, each misprediction costs 3 cycles, and the ideal CPI is $1$ β€” verified for real: $CPI_{eff} = 1 + 0.2 \times 0.1 \times 3 = \mathbf{1.06}$. In other words, branch mispredictions alone make the CPU 6% slower than ideal, even though only 10% of guesses are wrong β€” because effective CPI multiplies ALL THREE factors together (branch frequency Γ— miss rate Γ— penalty cycles).

effective_cpi.js (excerpt from the shared cpu-core.js engine)
function effectiveCPI(cpiIdeal, branchFrequency, mispredictionRate, penaltyCycles) {
  return cpiIdeal + branchFrequency * mispredictionRate * penaltyCycles;
}
// Verified: effectiveCPI(1, 0.2, 0.1, 3) === 1.06
// Verified: effectiveCPI(1, 0, 0.1, 3) === 1 (no branches at all -> the ideal CPI)
⚠️ Do not underestimate the UNPREDICTABLE branch
The $CPI_{eff}$ formula assumes ONE average misprediction rate for the WHOLE program β€” but in reality some branches are extremely easy to predict (a loop-stop condition over thousands of iterations, wrong only on the final one) while others are effectively unpredictable (random data, branching on input with no pattern β€” a binary search over random data, say). Using a SINGLE average miss rate for the whole program can completely mask the "hotspots" where local effective CPI is far above the average β€” real optimisation needs the miss rate measured PER BRANCH.

4. Speculative execution & the Spectre vulnerability

πŸ“ What you need to know about caches beforehand β€” just one thing
This section uses the word cache constantly, and caches are only taught properly in Lesson 7. To read on, you need exactly one thing: a cache is a very small, very fast memory sitting RIGHT INSIDE the chip, holding copies of recently used memory cells β€” so that reading them again does not require a trip out to slow external RAM.

The only consequence this section needs: reading an address that is ALREADY in the cache is noticeably faster than reading one that is not. That time difference is measurable β€” and it is precisely what Spectre uses to see data it should never have been able to see.

Having GUESSED a branch direction, a modern CPU does not merely fetch instructions β€” it runs them ahead of time (speculative execution) along the predicted path, including LOAD instructions that pull data from memory into the cache. If the guess was RIGHT, the results are committed normally. If it was WRONG, the CPU restores architectural state (registers, memory) exactly as if those instructions had never run β€” but ONE trace is NOT erased: the cache state. Data fetched speculatively REMAINS IN the cache even though the architectural result was discarded.

Spectre (disclosed in 2018) exploits exactly that trace: the attacker trains the branch predictor to force the victim CPU to SPECULATIVELY EXECUTE code that reads secret data (out of an array's bounds, say, past a bounds-check the CPU has not yet confirmed), then uses cache access timing (a cache-timing side-channel attack β€” measuring read latency to tell whether an address is cached) to INFER that secret value, even though the architectural result of the speculative computation was fully discarded. This is NOT an isolated software bug β€” it exploits the very speculation MECHANISM that every modern high-performance CPU needs in order to be fast.

spectre_leak_sketch.txt (an exploit sketch β€” conceptual illustration only)
// 1. Train the predictor: call the function many times with a VALID index
//    -> the predictor comes to "trust" that the bounds-check branch is TRUE
// 2. Call it again with an OUT-OF-BOUNDS index (past the real array)
//    -> the CPU PREDICTS the check is still TRUE and SPECULATIVELY reads the
//       out-of-bounds data into a register, using it to index a second array
//    -> the secret value is now part of an ADDRESS that entered the cache
// 3. When the CPU notices the misprediction it RESTORES architectural state
//    (registers and memory)
//    -> BUT the cache still holds the trace (that address is now cached)
// 4. The attacker TIMES a read of each candidate address
//    -> whichever address is fast (i.e. cached) reveals part of the secret
⚠️ Pitfall: believing Spectre can be fully patched in software at no performance cost
Software mitigations (inserting a "speculation barrier" before sensitive bounds-checks, or partially disabling branch prediction) trade DIRECTLY against performance, because they force the CPU to WAIT exactly where speculation was paying off most. No software patch removes this class of vulnerability COMPLETELY without a performance cost, because the root cause is in the HARDWARE itself (speculation plus a shared cache) β€” a genuine fix requires microarchitectural change (cache isolation per security domain, or new CPU generations with redesigned speculation), not just a firmware or OS patch.

5. Hands-on: simulating a 2-bit branch predictor

Pick a control structure below to run BOTH the 1-bit and the 2-bit predictor at once (using the same verified engine) β€” a green cell is a correct guess, a red one is a misprediction, so you can compare the two predictors' real hit rates on the SAME branch sequence:

🎯 1-bit vs 2-bit branch prediction simulator

1-bit predictor

2-bit saturating predictor

Effective CPI calculator

β€”

Summary

  • βœ… Control hazard: the CPU only knows a branch direction for certain after EX, yet has already had to fetch the next instruction β€” costing 1–3 cycles per branch without prediction.
  • βœ… Verified: the 1-bit predictor is 60% right on nested loops (oscillating at each loop boundary); the 2-bit saturating one reaches 66.7% (absorbing a single blip).
  • βœ… A BHT keeps SEPARATE history per branch address β€” different branches have different habits and cannot share one predictor.
  • βœ… Verified: $CPI_{eff} = CPI_{ideal} + \text{BranchFreq} \times \text{MispredictRate} \times \text{Penalty}$ β€” 20% branches, 10% mispredicted, a 3-cycle penalty gives 1.06, i.e. 6% slower than ideal.
  • βœ… Spectre exploits the CACHE trace left behind after speculative execution is discarded β€” no software patch removes it completely without a performance cost.

Review quiz

Question 1

Why does a CPU need a branch predictor, instead of simply waiting for the condition to be computed before fetching the next instruction?

Question 2

Verified: on a 15-branch nested-loop sequence (TTTTN repeated 3 times) the 1-bit predictor gets 9/15 (60%) and the 2-bit predictor 10/15 (66.7%). Why is the 2-bit one better?

Question 3

Verified: effectiveCPI(1, 0.2, 0.1, 3) = 1.06 (ideal CPI 1, 20% branch instructions, 10% mispredicted, a 3-cycle penalty per miss). If the misprediction rate fell to 0, what would the effective CPI be?

Question 4

What does the Spectre vulnerability exploit to leak secret data, even though the architectural result of the speculative computation was fully discarded?

Download the practice code for this lesson

The CPUJS JavaScript file β€” a mini computer-architecture library used across all 12 lessons. Lesson 5 adds makeBranchPredictor1Bit(), makeBranchPredictor2Bit(), makeBranchHistoryTable() and effectiveCPI() β€” the FSM branch predictors, the BHT, and the effective-CPI formula, with a self-test that checks every number quoted in this lesson (run node cpu-core.js; nothing to install):

Download cpu-core.js

πŸ“– References

Related lessons in this series

Lesson 4: CPU Pipelining & Data Hazards Lesson 6: Instruction-Level Parallelism & Out-of-Order Execution (Tomasulo) Back to the Computer Architecture roadmap

Comments