Photo by Brian Kostiuk on Unsplash
Branch Prediction: How CPUs Guess the Future
Every time your code hits an if statement, a loop condition, or a switch case, the processor faces a problem: it doesn’t know which instruction to fetch next until the condition is evaluated. But waiting for that evaluation would stall the pipeline and waste precious cycles. The solution is branch prediction—the CPU makes an educated guess about which path the code will take and speculatively executes ahead. When it guesses right, performance stays high. When it guesses wrong, the pipeline flushes and performance tanks.
Branch prediction is one of the most important microarchitectural techniques in modern processors, yet it operates almost entirely invisibly to developers. Understanding how it works helps explain performance cliffs in tight loops, why sorted data sometimes processes faster than unsorted data, and why certain code patterns are faster than they have any right to be.
How Branch Predictors Work
Modern CPUs use a combination of static and dynamic prediction. Static prediction follows simple heuristics: backward branches (like loops) are predicted taken, forward branches (like error handling) are predicted not taken. These rules work surprisingly well for common patterns but fail on anything more complex.
Dynamic prediction tracks the history of each branch instruction. The simplest form uses a branch history table (BHT), essentially a cache indexed by the instruction address. Each entry contains a saturating counter: if the branch is taken, the counter increments; if not taken, it decrements. A branch is predicted taken when the counter is in the upper half of its range.
But real-world branches often depend on patterns that span multiple instructions. Modern predictors use two-level adaptive schemes that consider not just whether the last execution was taken, but the sequence of recent outcomes. A global history register tracks the last N branch outcomes as a bit vector, and this history indexes into a pattern history table. This lets the predictor learn complex patterns like “taken, not taken, taken, not taken” and predict the next outcome accordingly.
High-end processors like those in server and desktop CPUs go further with tournament predictors that run multiple prediction algorithms in parallel and dynamically choose whichever performed best recently. They also use tagged geometric (TAGE) predictors with multiple history lengths, allowing them to capture both short-term and long-term patterns simultaneously.
Why It Matters for Performance
A mispredicted branch costs anywhere from 10 to 20 cycles on modern CPUs—the entire speculative work must be discarded and the pipeline refilled from the correct path. In a tight loop processing millions of items, even a 5% misprediction rate can cut throughput nearly in half.
This explains the classic optimization puzzle where sorting an array before processing it makes the code faster, even though sorting itself takes time. If you’re filtering data with an if statement inside the loop, random data causes the branch to behave unpredictably. Sorted data, by contrast, creates long runs of taken or not-taken outcomes that the predictor handles perfectly. The time saved by eliminating mispredictions can exceed the cost of the sort.
Branch prediction also interacts with other CPU features. Speculative execution allows the processor to continue working ahead of branches, but only if the prediction is correct. Out-of-order execution can hide some branch costs by finding independent work to do, but a misprediction still forces a pipeline flush. Hyperthreading helps by switching to another thread during a stall, but it doesn’t eliminate the cost—it just spreads it around.
Writing Branch-Friendly Code
Some code patterns are easier for predictors to handle. Loops with consistent iteration counts are nearly perfect—the branch at the loop end is taken N-1 times and not taken once. Branches that depend on the same condition repeatedly (like checking a configuration flag) are also easy, since the outcome rarely changes.
The hardest patterns involve data-dependent branches on random or pseudorandom data. If every iteration of a loop has a 50% chance of taking either path, no predictor can do better than guessing. In these cases, branchless code using arithmetic or bitwise operations can be faster, even if it does more total work. Modern compilers emit conditional move instructions (CMOV on x86) to avoid branches for simple cases, but programmers sometimes need to manually restructure hot loops.
Profile-guided optimization (PGO) helps by giving the compiler real branch statistics. With profiling data, the compiler can reorder code to place the common path inline and the rare path out of line, reducing instruction cache pressure and improving prediction accuracy for static predictors.
Security and Speculation
Branch prediction became a household term—at least in systems engineering circles—with the Spectre vulnerability. Spectre exploits the fact that speculative execution happens before permission checks. An attacker can train the branch predictor to mispredict a bounds check, causing the CPU to speculatively access out-of-bounds memory. Although the results are discarded when the misprediction is discovered, they leave traces in the cache that can be measured through timing side channels.
Mitigations include inserting speculation barriers, flushing branch predictor state on context switches, and using retpoline techniques to prevent indirect branch prediction in sensitive contexts. These fixes carry performance costs, sometimes significant ones, which is why they’re applied selectively to security-critical code paths.
Branch prediction remains a fundamental technique for extracting instruction-level parallelism. As CPU clock speeds plateau, effective speculation becomes even more critical to performance—and understanding its behavior becomes more valuable for developers writing performance-sensitive code.