TRT
Sign in
Operating SystemsUpdated

Branch Prediction and Exposing Parallel Work

Branch prediction keeps a modern CPU full by guessing outcomes from history; mispredicts cost a pipeline flush, so predictable data layout beats clever conditionals.

The Runtime Theory Team8 min read#branch-prediction#parallelism#performance

Branches make program control depend on data, and data is usually not known until the instruction is deep inside the machine. A modern CPU predicts the likely direction so it can keep fetching, decoding and even executing instructions before the condition resolves. A correct prediction costs nothing you can see. A misprediction costs a pipeline flush — on current x86 cores, roughly 15–20 cycles of thrown-away work plus refilling the pipe, which at 4 GHz is about 4–5 nanoseconds per miss.

What the predictor actually keeps

The branch target buffer predicts where a branch jumps, remembered from recently seen PCs; a pattern history table indexed by global history (the outcomes of the last ~10–15 branches) holds a two-bit saturating counter per entry, which predicts the direction. The machine is extremely good at one thing: repetition. A loop that runs 1,000 iterations takes the same edge 1,000 times and is predicted perfectly after the first couple of trips. A branch whose outcome is 50/50 random — a mask over encrypted data, a bitmap test of uniform bits — is unpredictable in principle, not just in practice.

c
// branchy: one unpredictable test per element
for (size_t i = 0; i < n; i++)
    if (a[i] < 0) b[j++] = a[i];
 
// branchless: same work, no control dependence on data
for (size_t i = 0; i < n; i++) {
    int mask = -(a[i] < 0);
    b[j] = a[i]; j += mask & 1;
}

The branchy version's cost is n × mispredict_rate × 17 cycles. At 50% mispredictions and 10⁸ elements that is ~8.5 × 10⁸ wasted cycles — around 0.2 seconds on a 4 GHz core, for a loop whose useful work is one comparison per element.

Removing a branch is a trade, not a win

The branchless rewrite buys predictable flow but pays with extra instructions, and it can block optimizations the branch made possible. Three failure modes show up constantly:

  • The branch was usually not taken anyway. A filter where 99.5% of elements pass mispredicts ~0.5% of the time; the branchless version adds two instructions to every element to save a cost already near zero.
  • Branchless code can prevent vectorization. A conditional that the compiler could turn into a masked AVX-512 operation sometimes ends up as scalar arithmetic because the rewrite hides the pattern.
  • Correctness gets subtler. j += mask & 1 reads a[i] unconditionally; if the body could fault or trap (dividing, dereferencing), the branchless form executes it for elements the branch would have skipped.

So the question is never "how many branches are in this function" but "how predictable is each one on the data I actually ship".

Measure the mispredict rate on your workload

Guessing from source code is unreliable; the hardware will tell you in one command:

bash
$ perf stat -e branches,branch-misses,cycles ./filter input.bin
 
 Performance counter stats for './filter input.bin':
   100,001,042  branches
    49,998,731  branch-misses    # 50.00% of all branches
 3,148,220,914  cycles

branch-misses / branches is the number to watch. A loop sitting above 5–10% is a candidate for data reordering (sort or group by predicate so the branch becomes runs of the same outcome), a mask-based rewrite, or a different algorithm that never asks the question. objdump -d on your binary confirms which form the compiler actually emitted — an if sometimes compiles to cmov on its own, and a "branchless" rewrite sometimes compiles back into a branch.

Parallel work and branches interact

Exposing parallelism (SIMD, multiple cores) multiplies both sides of the ledger: vector lanes each carry a predicate, and a mispredicted branch stalls the whole front end rather than one instruction stream. Widest wins come from work that is simultaneously data-parallel and branch-light — a gather/scatter with a mask, or a sort that converts random predicates into runs. That is why the fast paths in image processing and analytics are usually histograms, masks and sorted runs rather than clever conditionals.

Key takeaways

  • The predictor wins on repetition and loses on data-dependent randomness; a 50/50 branch is a guaranteed ~17-cycle tax per evaluation.
  • Cost is mispredict_rate × penalty × evaluations — a rarely-taken branch is nearly free, so branchless rewrites must earn their extra instructions.
  • Always verify with perf stat -e branch-misses and objdump -d on real input; the compiler often already did the rewrite you were about to hand-write.
  • When you do parallelize, prefer layouts that make predicates contiguous (masks, sorted runs) so the machine never has to guess.

Production cost model and latency numbers

To apply Branch Prediction and Exposing Parallel Work effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Branch Prediction and Exposing Parallel Work, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save