σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Cache locality skill›Module 3 · The wider machine›Topic 3.6·~55 minAdvanced

Branch prediction

Skill positionModule 3 of 3 · Topic 6 of 7
3.6 Branch prediction
—Lab score— / 100Total XP—Streak—

6 min readCode examples: 3

  1. 01Why the processor guesses
  2. 02Predictors that remember
  3. 03What a miss costs, and why sorting helps
  4. 04What you will build

A processor does not wait to find out which way an if goes: it guesses and starts working. A right guess is free and a wrong one throws the work away, about 15 cycles lost. How often it guesses right depends on the data: the same loop over the same SPY returns costs 8.3 times fewer cycles once the returns are sorted.

01

Why the processor guesses

A processor works like an assembly line: while one instruction runs, the next ones are already being fetched and decoded. An if breaks the line, because the next instruction depends on its answer, and the answer is not ready yet. So the processor makes a branch prediction and runs ahead on it. If the guess was right nothing is lost. If it was wrong the work done on the wrong path is thrown away and the line restarts, which costs about 15 cycles on a modern core, as much as a visit to the L2 cache. We write the outcome of a branch as True when it is taken. In a trading loop a branch is, for instance, if daily_return > 0, and the stream of its outcomes is simply one True or False per day.

main.pyPython
def taken_sequence(values, threshold):    return [bool(value > threshold) for value in values]def always_taken_mispredicts(outcomes):    return sum(1 for outcome in outcomes if not outcome)returns = [0.004, -0.012, 0.007, 0.001, -0.003, 0.009]outcomes = taken_sequence(returns, 0)print(outcomes)print(always_taken_mispredicts(outcomes), "misses out of", len(outcomes))
[True, False, True, True, False, True]
2 misses out of 6

Of six days, four had a positive return, so the branch is taken four times and not taken twice. The simplest predictor, always taken, is wrong exactly on the two False outcomes. It is right as often as the branch is taken, and it ignores the history.

02

Predictors that remember

Real processors use the history. The 1-bit predictor remembers what the branch did last time and guesses the same again. It is good when outcomes come in runs, but it pays twice for every stray outcome: once when the stray one arrives and once when the run resumes. The 2-bit predictor fixes that with a counter from 0 to 3 that stops at both ends. It guesses "taken" at 2 and 3, moves one step toward each outcome, and so it needs two outcomes in a row against its guess to change its mind. We start the 1-bit guess at "not taken" and the counter at 1.

main.pyPython
def one_bit_mispredicts(outcomes):    guess = False    wrong = 0    for outcome in outcomes:        if guess != outcome:            wrong += 1        guess = outcome    return wrongdef two_bit_mispredicts(outcomes):    counter = 1    wrong = 0    for outcome in outcomes:        if (counter >= 2) != outcome:            wrong += 1        counter = min(3, counter + 1) if outcome else max(0, counter - 1)1    return wrongruns = [True] * 4 + [False] * 4 + [True] * 4stray = [True] * 6 + [False] + [True] * 6for name, stream in (("runs", runs), ("one stray", stray)):    print(name, "| 1-bit", one_bit_mispredicts(stream), "| 2-bit", two_bit_mispredicts(stream))
runs | 1-bit 3 | 2-bit 5
one stray | 1-bit 3 | 2-bit 2
  1. 1The counter moves one step toward the outcome and stops at 0 and 3: this is what makes it saturating.

On the runs of four, both predictors miss three times (the first guess, and the two turns). With one stray False among taken outcomes the 1-bit predictor misses three times and the 2-bit predictor only once, because the counter drops from 3 to 2 and still guesses "taken". That is why real processors prefer counters. Keep in mind the other side: when outcomes have no pattern, no predictor can do better than guessing the more frequent answer.

Prediction is about the data, not the code

The same if is cheap on predictable data and costly on random data. Do not rewrite a branch to be "faster" before you know how its outcomes look in the real data; measure the misses first.

03

What a miss costs, and why sorting helps

To turn misses into time we add them up: every branch costs a base of one cycle and every miss adds a penalty of 15. Now the experiment that this lecture is named for: take the first 4,000 daily returns of SPY and run the branch return > 0 twice, over the returns in date order and over the same returns sorted. Sorting changes nothing about the numbers, only their order, but it changes the outcomes from a coin flip into two long runs: all the False first and all the True after.

main.pyPython
def cycles(n_branches, mispredicts, penalty=PENALTY, base=BASE_CYCLES):    return base * n_branches + penalty * mispredictsdef sorted_stream(values, threshold):    return taken_sequence(sorted(values), threshold)returns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:4000]for name, stream in (("unsorted", taken_sequence(returns, 0)), ("sorted", sorted_stream(returns, 0))):    wrong = two_bit_mispredicts(stream)    print(name, "| always", always_taken_mispredicts(stream), "| 1-bit", one_bit_mispredicts(stream), "| 2-bit", wrong, "|", cycles(len(stream), wrong), "cycles")
unsorted | always 1800 | 1-bit 2035 | 2-bit 1950 | 33250 cycles
sorted | always 1800 | 1-bit 1 | 2-bit 2 | 4030 cycles

In date order 2,200 of the 4,000 returns are positive, so the branch is taken 55% of the time, and it jumps from one answer to the other almost without a pattern: always-taken misses 1,800 times, the 1-bit predictor 2,035 and the 2-bit predictor 1,950. The predictors with a memory are even worse than the plain guess, because they follow every flip. Sorted, the outcomes are 1,800 False followed by 2,200 True, and the 2-bit predictor misses only 2 times. The cost falls from 33,250 to 4,030 cycles, 8.3 times fewer, with the very same data and the very same loop.

DefinitionMisprediction penalty

The cycles lost when the processor guessed a branch wrong: it discards the work done on the wrong path and refills the pipeline, about 15 cycles on a modern core.

The same trick in NumPy and in C++

A vectorised comparison such as returns > 0 has no branch per element, so it does not suffer from this at all. In C++ a loop with an if over random data is the classic case where sorting the data first, or writing the condition as arithmetic, makes it several times faster.

What the real data says4,000 SPY daily returns, branch return > 0, penalty 15 cycles
Always taken, date order1,800misses of 4,000
2-bit, date order1,950misses, worse than always taken
2-bit, sorted2misses: one run, then the other
Gain from sorting8.3×fewer cycles (33,250 → 4,030)
Cycles of the 2-bit predictor
Date order33250
Sorted4030
04

What you will build

The lab follows the lecture: the outcomes of a branch, the three predictors, the cost in cycles, the sorted stream, and a study of the SPY returns in date order and sorted.

  • Function · questionBuilds on
  • taken_sequence(values, threshold)What does the branch do for each value?Plain arithmetic
  • always_taken_mispredicts(outcomes)How often is "always taken" wrong?Plain arithmetic
  • one_bit_mispredicts(outcomes)And a predictor that remembers the last outcome?Plain arithmetic
  • two_bit_mispredicts(outcomes)And a saturating counter?Plain arithmetic
  • cycles(n_branches, mispredicts)What does it all cost?Plain arithmetic
  • sorted_stream(values, threshold)What if the values were sorted first?taken_sequence
  • study(bars)How much does sorting save on SPY?sorted_streamtwo_bit_mispredictscycles

Key takeaways

  • The processor guesses every `if`. A right guess is free; a wrong one costs about 15 cycles, and a predictor with a memory can only beat the plain guess when outcomes come in runs.
  • A counter beats a single bit. The 2-bit counter ignores one stray outcome where the 1-bit predictor pays twice.
  • Order changes the cost. The same 4,000 returns cost 33,250 cycles in date order and 4,030 sorted, because sorting turns a coin flip into two runs.
Quick check
A branch has the outcomes True ×4, False ×4, True ×4. The 1-bit predictor starts at "not taken". How many times is it wrong?
Pick an answer.

Next topic: the capstone puts the whole skill together. You are given a table of ticks and a workload that reads it, and you re-lay it out as rows, columns or blocks, count the cache misses of each layout with the cache you built, price them with the latency model of the first topic, and prove which layout wins.

Lab · what's graded

7 tasks · 100 pts
  • Branch outcomes5 pts
  • Always taken10 pts
  • A 1-bit predictor15 pts
  • A 2-bit saturating counter20 pts
  • The cost in cycles10 pts
  • Sorted values10 pts
  • The study on the SPY table30 pts
Explorer · lab-cl-12
▾ lab-cl-12
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-cl-12$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-cl-12 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes