Branch Prediction Secrets: 6 Proven CPU Speed Wins Now
Sorted data runs 4x faster through the exact same loop.
20+ years shipping production systems from the metal up. Everything here is grounded in real deployments.
- ✓Basic Java loops and arrays
- ✓Rough idea of Big-O and benchmarking
- ✓A JDK to run JMH-style microbenchmarks
- Branch prediction: CPUs guess if/else outcomes to keep 14-19 stage pipelines fed; wrong guesses flush ~15-20 cycles (~5ns)
- CPU caches move 64-byte lines through L1/L2/L3 to hide ~100ns RAM latency; sequential access uses full lines, random access wastes them
- Sorted-vs-shuffled benchmark: identical loop runs 3-4x faster on sorted data because the predictor learns the pattern
- Production case: a 40-line filter hit 47% branch-miss rates plus false-sharing ping-pong, driving p99 from 90ms to 900ms at 10x volume
- Fix hierarchy: partition/sort data first (O(n) pass), branchless selects for cheap unpredictable cases, layout/tiling for cache misses
- Measure with JMH plus perf counters (branch-misses, cache-misses) — cache misses (~100ns) usually dominate mispredicts (~5ns)
Imagine a librarian fetching books for you. If you request books in shelf order (sorted data), they grab armfuls at once and fly — each trip carries 64 books (a cache line) and they guess your next request correctly (branch prediction). If you request random shelves (shuffled data), every request is a separate trip to the basement (RAM at 100ns), and their guesses about what you'll ask next are always wrong (mispredicts), forcing them to walk back each time. Same librarian, same books — but ordered requests finish 4x faster because prediction and bulk-carrying actually work.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Your loop does almost nothing — one comparison, one addition. Yet it runs 4x slower on shuffled data than sorted data. Same instructions, same count, wildly different speed.
The CPU isn't executing your code line by line. It predicts, prefetches, and caches ahead — and your data's shape decides whether those guesses pay off.
You'll learn how branch prediction and cache lines really work, the sorted-array demo that proves it, and the measurement discipline that separates real wins from folklore.
How Branch Prediction Feeds the Pipeline (and Starves It)
Your CPU doesn't run one instruction at a time — it keeps 14-19 stages busy simultaneously, with dozens of instructions in flight. At every if, it must guess which way to go before the condition is computed, or the whole pipeline starves.
Guess right (the common case for loops and rare error checks) and the cost is zero — execution flows uninterrupted. Guess wrong and the CPU flushes everything speculative: ~15-20 cycles (~5ns) of work discarded. One mispredict is nothing; 5 million per second is a bottleneck.
Predictors aren't coin flips — they're two-level history tables that learn patterns, even alternating ones. Loops, correlated branches, mostly-one-way checks: all nearly free. The killer is data-dependent 50/50 chaos no table can learn.
The Sorted-vs-Shuffled Demo That Converts Skeptics
The classic demo fits in 30 lines: loop 10M integers, count those above a threshold. On sorted data it flies; shuffled, it crawls — 3-4x slower with identical instructions. The only difference is predictability.
Sorted, the branch is 'no,no,no...yes,yes,yes' — the predictor learns the single transition and naps. Shuffled, it's coin-flip noise — the predictor guesses wrong half the time and the pipeline flushes constantly.
Run it yourself with the benchmark below. The numbers convert skeptics faster than any diagram: same code, same data, different order, 4x speed gap. Data shape is performance.
Cache Lines, False Sharing, and the 100ns Toll
Below the predictor sit the caches: L1 (~1ns, 32-64KB), L2 (~4ns), L3 (~15ns), then RAM (~100ns). Data moves in 64-byte lines — using one byte fetches 63 neighbors. Sequential scans consume whole lines (fast); strided jumps waste 7/8 of every fetch.
False sharing is the multicore trap: two threads writing different fields on the same line force the coherence protocol to shuttle it between cores at ~100ns per transfer. Your 'independent' counters serialize through the memory system.
And the hierarchy rule: one RAM miss (~100ns) costs as much as twenty mispredicts (~5ns). Layout fixes usually outrank branch fixes — profile cache-misses before celebrating a branchless rewrite.
Branchless Selects, Lookup Tables, and Partition-First
When the branch is genuinely unpredictable and both sides are cheap, go branchless: replace if (c) x = a; else x = b; with arithmetic or cmov-style selects the CPU executes without guessing. The cost is always doing both sides' work — worth it only when guessing loses more.
Lookup tables trade branches for memory: precompute outcomes for small input domains and index instead of deciding. The table must stay cache-resident or you've swapped a 5ns mispredict for a 100ns miss — a 20x loss disguised as cleverness.
Sort or partition first when scans repeat: one O(n) pass over static data pays for itself across thousands of predictable scans. The trading fix was exactly this — partition by type once, scan predictably forever.
Layout Wins: Tiling, SoA, and JIT Cooperation
Big arrays need tiling: process in cache-sized blocks so each line is reused while hot instead of streamed once and evicted. Hot-field/cold-field splits (Struct-of-Arrays) stop cold bytes from evicting hot lines. Loop interchange, padding, and alignment finish the job.
In Java specifically: prefer arrays of primitives over boxed collections in hot loops, keep hot methods monomorphic so the JIT inlines aggressively, and confirm compilation with -XX:+PrintCompilation — interpreted timings lie 10x about hardware effects.
Each transform has a cost (complexity, footprint, refactor risk). Apply them where counters prove the bottleneck, nowhere else.
The Measurement Discipline That Beats Folklore
The discipline in five steps. Profile with hardware counters (perf stat or async-profiler: branch-misses, cache-misses) on production-shaped data. Identify the dominant toll — 100ns misses outrank 5ns mispredicts, fix bigger first. Apply one transform. Re-measure with JMH (warmed, forked). Keep it only if the counters and the clock agree.
What not to do: optimize cold code, benchmark without warmup, test on sorted fixtures, or merge branchless rewrites without numbers. Folklore optimizations have negative value — they cost readability and sometimes speed.
The teams that win treat the CPU as a measurement-driven partner: predictable data, friendly layout, verified gains. Everything else is superstition with a compiler.
The 40-Line Loop That Ate 38% of Trading CPU
- Benchmark with production distributions, not tidy fixtures — sorted test data makes every branch predictor look perfect.
- A 2% profile on small inputs becomes the p99 bottleneck at 10x volume; load-test at production cardinality before declaring code fast.
- One O(n) partition pass can beat any amount of branchless cleverness — fix the data shape before the instruction shape.
| File | Command / Code | Purpose |
|---|---|---|
| public class BranchDemo { | The Sorted-vs-Shuffled Demo That Converts Skeptics | |
| Branchless.java | public class Branchless { | Branchless Selects, Lookup Tables, and Partition-First |
| measure.sh | perf stat -e branch-misses,cache-misses,L1-dcemissloads java -jar target/bench.j... | The Measurement Discipline That Beats Folklore |
Key takeaways
Common mistakes to avoid
4 patternsMicro-optimizing branches the predictor already handles
Fixing branches while ignoring false sharing
Benchmarking cold code and blaming the hardware
Branchless-everywhere zealotry
Interview Questions on This Topic
Why does sorting an array speed up a loop with an if inside?
Frequently Asked Questions
20+ years shipping production systems from the metal up. Everything here is grounded in real deployments.
That's Performance. Mark it forged?
3 min read · try the examples if you haven't