Introduction
Welcome back! In our previous lesson, we explored how CPU pipelining and instruction-level parallelism (ILP) act like a sophisticated assembly line to improve instruction throughput. We also identified the "sand in the gears" of this assembly line: hazards. Specifically, we noted that control hazards—caused by branches like if statements and loops—are a major performance bottleneck because they create uncertainty about which instruction to fetch next.
This lesson directly addresses that challenge. Our learning outcome is to describe how branch prediction affects pipeline efficiency. We will investigate the clever techniques CPUs use to "guess" the path of your code, a process called branch prediction. You'll learn why this guessing game is crucial for performance, what happens when the CPU guesses right or wrong, and how the predictability of your code can lead to surprising performance differences.
The High Cost of Uncertainty
As we saw, a pipelined processor fetches a new instruction every clock cycle. But what happens when it fetches a branch instruction, like if (x > 10)? The outcome of this comparison won't be known until the Execute stage, which is several steps down the pipeline. The CPU is now faced with a dilemma:
- Stall: It could simply wait, freezing the pipeline until the branch outcome is known. This creates "bubbles" (wasted cycles) and severely degrades performance.
- Predict: It could guess the outcome and start fetching and executing instructions from the predicted path. This is called speculative execution.
Modern CPUs choose to predict. To understand the problem and the penalty for waiting, let's watch a short introductory clip.
How Branch Prediction Works in CPUs - Computerphile
This clip from Computerphile uses a simple 'robot assembly line' analogy to explain why branches are disruptive to a pipeline.
Watch the first 1 minute and 34 seconds of this video. Focus on how a branch requires the pipeline to be 'flushed' and restarted, wasting valuable cycles.
This pipeline flush is the fundamental problem. Branch prediction is the solution designed to minimize how often it happens.
A Puzzling Performance Phenomenon
To see the real-world impact of branch prediction, consider the following C++ code snippet. It sums up elements of an array that are greater than or equal to 128. The surprising part is that if we std::sort the array first, the loop runs dramatically faster.
Why is processing a sorted array faster than processing an unsorted ...
This famous Stack Overflow post presents a C++ code example that perfectly illustrates the effect of branch prediction. It will form the basis for our exploration.
Read the initial question and the C++ code provided. You don't need to read the answers yet. Just familiarize yourself with the problem: why does sorting the array make the summing loop so much faster, even though the sum itself is the same?
This seems counter-intuitive. The loop does the same number of comparisons and additions regardless of the array's order. The key to this mystery lies in how predictable the if (data[c] >= 128) condition is in each case.
The Logic of Prediction
A branch predictor's job is to guess the outcome of a conditional branch.
- Correct Prediction: If the guess is correct, the instructions already in the pipeline are the right ones. The pipeline flows smoothly without any stalls. This is the best-case scenario.
- Misprediction: If the guess is wrong, the CPU must discard all the speculatively executed instructions and restart fetching from the correct path. This is a branch misprediction, and it incurs a misprediction penalty, which is the time wasted. This is essentially the same as the stall we would have had if we waited in the first place.
Let's look at the "railroad junction" analogy to solidify this concept.
Why is processing a sorted array faster than processing an unsorted ...
The top answer in the Stack Overflow post uses an excellent analogy to explain the mechanics and consequences of branch prediction.
Please read the section titled "What is Branch Prediction?" which uses a railroad analogy. Also, review the diagrams in the answer from 'Harsh Sharma' which visualize the pipeline with and without prediction, and in cases of correct and incorrect guesses. These diagrams will help formalize the concept of a pipeline flush.
As you can see, the goal is to maximize correct predictions. The efficiency of the pipeline is directly tied to the branch predictor's success rate.
How CPUs Predict the Future
So, how does the CPU make these crucial guesses? It uses a combination of hardware mechanisms that learn from the program's past behavior.
Static vs. Dynamic Prediction
The simplest methods are static, meaning the prediction follows a fixed rule. A common one is "predict backward branches as taken and forward branches as not taken." This works well for loops (which branch backward to repeat) and error-checks (which often branch forward to skip).
However, modern CPUs use far more sophisticated dynamic prediction, which adapts based on the runtime behavior of the code.
The Branch Target Buffer (BTB)
First, the CPU needs to predict where a branch will go if it's taken. It uses a small, fast cache called the Branch Target Buffer (BTB). The BTB stores the target addresses of recently executed taken branches, indexed by the branch instruction's own address. When the CPU fetches a branch, it looks it up in the BTB. If there's a hit, it instantly knows the likely target address without having to calculate it.

2-Bit Saturating Counters: Adding Hysteresis
The real magic is in predicting the direction (taken or not taken) of a conditional branch. A naive approach would be to simply predict the branch will do what it did last time (a 1-bit predictor). This works, but it has a flaw: it will always mispredict the last iteration of a loop and the first iteration of the next time the loop runs.
To solve this, CPUs use 2-bit saturating counters. This adds hysteresis, or resistance to change. A branch must be mispredicted twice in a row to flip the prediction.
Each branch in the code maps to its own 2-bit counter. The counter has four states:
- 00: Strongly Not Taken
- 01: Weakly Not Taken
- 10: Weakly Taken
- 11: Strongly Taken
The prediction is based on the most significant bit (0 = Not Taken, 1 = Taken). The counter updates after the actual outcome is known.
How Branch Prediction Works in CPUs - Computerphile
This video provides a fantastic, intuitive explanation of dynamic branch prediction, including an excellent physical demonstration of a 2-bit saturating counter.
Watch the segment from 01:34 to 11:59. Focus on: How the 'predicting robot' learns the target of an unconditional jump (the basic idea of a BTB). The introduction of a conditional branch and the problem with a simple 'last-time' predictor. The brilliant explanation of the 2-bit counter using a toy with faces to represent the four states. This clearly shows how hysteresis works.
This 2-bit counter mechanism is the key to our puzzle.
Solving the Puzzle
Let's return to the sorted vs. unsorted array example.
-
Sorted Array:
data = [0, 1, 2, ..., 127, 128, 129, ..., 255]
The branchif (data[c] >= 128)will befalsefor the first half of the array andtruefor the second half.
The pattern of outcomes isN, N, N, ..., N, T, T, T, ..., T.- The 2-bit predictor quickly learns the
Npattern and enters the "Strongly Not Taken" state. It correctly predictsNfor nearly half the loop. - When
data[c]becomes 128, it mispredicts once (predict N, actual T). The counter moves to "Weakly Not Taken". - On the next element, it mispredicts again (
predict N, actual T). The counter flips to "Weakly Taken". - From here on, it correctly predicts
T, and the counter moves to "Strongly Taken" for the rest of the array.
With thousands of elements, there are only a couple of mispredictions in the entire loop. The prediction accuracy is >99%.
- The 2-bit predictor quickly learns the
-
Unsorted Array:
data = [226, 185, 125, 158, ...]
The branch outcome is effectively random:T, T, N, T, N, N, T, ...
The branch predictor cannot learn a pattern. Its guesses are no better than a coin flip, leading to a ~50% misprediction rate. The pipeline is constantly being flushed, destroying performance.
The following resource provides a benchmark graph that perfectly visualizes this effect.
C++ cache locality and branch predictability
This video from the mCoding channel provides another C++ example and shows a graph of performance versus branch predictability.
Watch from 07:03 to 09:59. Pay close attention to the graph at the end. It shows that performance is highest when the branch is highly predictable (always true or always false) and worst when it's 50/50, confirming our analysis.
Advanced Predictors and Branchless Code
Modern branch predictors are even more complex. They often use pattern-based predictors that look at the history of the last several branches (e.g., a "gshare" predictor) to find correlations and repeating patterns. The most advanced CPUs use tournament predictors that run multiple different prediction strategies in parallel and use a meta-predictor to choose the best one for the current code.
Sometimes, if a branch is known to be unpredictable, a programmer or compiler can eliminate it entirely using branchless code. This can be done with bitwise operations or conditional move (cmov) instructions, which compute both outcomes and then select the correct one based on the condition. This avoids the risk of a misprediction penalty, but it comes at the cost of executing more instructions.
The Stack Overflow post you looked at has excellent benchmarks showing that branchless code is slower than a well-predicted branch but much faster than a mispredicted branch.
Conclusion
In this lesson, we demystified the surprising performance impact of code structure on modern CPUs. You've learned that the smooth operation of a CPU's pipeline is not guaranteed and is highly dependent on the predictability of branches.
Key Takeaways:
- Control hazards from branches force the CPU to either stall or predict, and predicting is the high-performance option.
- Branch prediction is a hardware mechanism that guesses the outcome of a branch to keep the pipeline full via speculative execution.
- A correct prediction results in zero performance loss. A misprediction causes a costly pipeline flush.
- Pipeline efficiency is therefore directly proportional to branch prediction accuracy.
- Dynamic predictors, like 2-bit saturating counters, learn from past behavior to improve accuracy. They work extremely well for code with predictable patterns (e.g., loops over sorted data).
- Code with unpredictable, data-dependent branches (e.g., loops over random data) thwarts the branch predictor, leading to frequent mispredictions and poor performance.
Preview of the Next Lesson
Now that you understand two major hardware-level performance factors—cache locality (from a previous module) and branch prediction—we can start applying this knowledge to optimize code. In the next lesson, we will cover specific C++ code transformations: loop interchange and loop unrolling. You will learn how to restructure loops to improve both cache utilization and instruction scheduling, making your code more friendly to the underlying hardware.