CodeRigor

FREE LESSON

Branchless code and bit tricks

cmov, setcc and sbb; when a branch beats a select and when it loses badly; and the bit-manipulation instructions behind std::popcount, countl_zero, rotl and pext.

A branch costs almost nothing when the CPU guesses its direction right, and 15–20 cycles when it guesses wrong: the pipeline has already fetched and started executing dozens of instructions down the wrong path, and throws them all away. Branchless code replaces "jump if" with "compute both, select one" — trading a possible misprediction for a certain small cost. This skill is about knowing which side of that trade you're on, reading the select instructions the compiler emits, and the bit-manipulation instructions that make a lot of branchy code unnecessary.

1. Branch prediction in one picture

The front end guesses; the back end finds out much later
Fetch follows the predicted target every cycle. The branch only resolves in execute, a dozen stages later; if the guess was wrong, everything fetched since is flushed and fetch restarts — about 15–20 cycles on Zen 3.
The front end guesses; the back end finds out much later
Fetch follows the predicted target every cycle. The branch only resolves in execute, a dozen stages later; if the guess was wrong, everything fetched since is flushed and fetch restarts — about 15–20 cycles on Zen 3.

The front end can't wait for a comparison to finish before fetching the next instruction, so it predicts. Predictors are very good at patterns — always-taken, alternating, loop exits, correlation with recent branches. They are hopeless on data that's genuinely random. A branch taken 50% of the time at random mispredicts about half the time.

C++
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>

// asm volatile keeps a real conditional jump: without it GCC would turn the
// loop branch-free (and vectorise it) on its own.
[[gnu::noinline]] long sum_big(const std::vector<int>& v) {
    long s = 0;
    for (int x : v)
        if (x >= 128) { s += x; asm volatile("" ::: "memory"); }
    return s;
}

double ms(const std::vector<int>& v) {
    auto t0 = std::chrono::steady_clock::now();
    long r = 0;
    for (int rep = 0; rep < 20; ++rep) r += sum_big(v);
    auto t1 = std::chrono::steady_clock::now();
    if (r == 42) std::puts("");  // use r
    return std::chrono::duration<double, std::milli>(t1 - t0).count();
}

int main() {
    std::vector<int> v(1 << 20);
    std::mt19937 rng(1);
    for (int& x : v) x = static_cast<int>(rng() % 256);
    double random = ms(v);
    std::sort(v.begin(), v.end());
    double sorted = ms(v);
    std::printf("random: %.1f ms   sorted: %.1f ms\n", random, sorted);
}

Same work, same data, sorted versus shuffled — typically 5–10× apart on this machine. Sorted data makes the branch "not taken 128 times, then taken 128 times", which the predictor learns.

2. The select instructions

Instruction Does Typical C++
setcc r8 r8 = condition ? 1 : 0 return a < b;
cmovcc r, r/m if (condition) r = r/m — but it always reads its source x = c ? a : b;
sbb r, r r = r - r - CF = CF ? -1 : 0 mask = (a < b) ? -1 : 0 for unsigned
adc r, 0 r += CF counting unsigned comparisons
C++
extern "C" int max_i(int a, int b) { return a > b ? a : b; }
extern "C" int below_mask(unsigned a, unsigned b) { return a < b ? -1 : 0; }
Assembly
max_i:                 below_mask:
    cmp   edi, esi         cmp  edi, esi     ; CF = (a < b) unsigned
    mov   eax, esi         sbb  eax, eax     ; eax = -CF
    cmovge eax, edi        ret
    ret
sbb eax, eax turns the carry flag into a mask
cmp edi, esi sets CF when a < b (unsigned). sbb eax, eax computes eax − eax − CF: all ones if CF = 1, zero otherwise. (Low byte shown; the other 24 bits match.)
sbb eax, eax turns the carry flag into a mask
cmp edi, esi sets CF when a < b (unsigned). sbb eax, eax computes eax − eax − CF: all ones if CF = 1, zero otherwise. (Low byte shown; the other 24 bits match.)

cmov is not magic. It has a data dependency on both inputs and the flags, so the result can't be used until the comparison and both operands are ready. A correctly predicted branch has no such dependency — execution continues speculatively. That's why:

  • Unpredictable condition → branchless wins. No misprediction to pay.
  • Predictable condition → the branch often wins, especially when the condition sits on a long dependency chain or one side is expensive (cmov must compute both sides).

Compilers guess. GCC and Clang turn small ?: and if bodies into cmov when both sides are cheap and side-effect free; they keep branches when a side has a store, a call, or work you'd rather skip. When the choice matters, measure, and check the listing.

3. Writing it yourself

Arithmetic selects work with any compiler and every type:

C++
#include <cstdint>
// mask is all ones or all zeros
inline std::uint32_t select(bool c, std::uint32_t a, std::uint32_t b) {
    std::uint32_t mask = -static_cast<std::uint32_t>(c);   // c ? 0xffffffff : 0
    return (a & mask) | (b & ~mask);
}
select(c, a, b): two ANDs and an OR
start / 2
With c = true the mask is all ones, a & mask keeps a and b & ~mask keeps nothing. Step to c = false and the roles swap. (Low byte shown.)
select(c, a, b): two ANDs and an OR
With c = true the mask is all ones, a & mask keeps a and b & ~mask keeps nothing. Step to c = false and the roles swap. (Low byte shown.)

Watch out for signed overflow in the classic tricks. abs(x) as (x ^ (x >> 31)) - (x >> 31) overflows for INT_MIN; a - b to compare two ints overflows when they have opposite signs. Do the arithmetic in unsigned, where wrapping is defined, and convert at the end.

C++
#include <climits>
#include <cstdint>
#include <cstdio>

// |a - b| as an unsigned value: exact for every pair of ints, no UB.
std::uint32_t abs_diff(std::int32_t a, std::int32_t b) {
    std::uint32_t ua = static_cast<std::uint32_t>(a), ub = static_cast<std::uint32_t>(b);
    return a < b ? ub - ua : ua - ub;
}

int main() {
    std::printf("%u %u %u\n", abs_diff(3, 10), abs_diff(INT_MIN, INT_MAX), abs_diff(-5, -5));
}

4. Branchless binary search

A textbook binary search has one unpredictable branch per level: whether to go left or right depends on the data. A branch-free version keeps a base pointer and a remaining length, and moves the base with a select:

C++
#include <cstddef>
// Index of the first element >= key in sorted a[0, n).
inline long lower_bound_branchless(const int* a, long n, int key) {
    if (n == 0) return 0;
    const int* base = a;
    while (n > 1) {
        long half = n / 2;
        base = (base[half] < key) ? base + half : base;   // cmov
        n -= half;                                        // NOT n = half: odd n
    }
    return (base - a) + (*base < key);
}
lower_bound_branchless(a, 8, 9)
start / 5
Each step halves the remaining length and selects the new base with a cmov — the same number of steps for every key, so nothing to mispredict.
lower_bound_branchless(a, 8, 9)
Each step halves the remaining length and selects the new base with a cmov — the same number of steps for every key, so nothing to mispredict.

The loop runs exactly ⌈log₂ n⌉ times regardless of the data, so its own branch is perfectly predicted. On arrays that fit in cache this is typically 1.5–3× faster than std::lower_bound on random queries; on huge arrays, cache misses dominate both and prefetching matters more (see the microarchitecture skill).

5. The bit-manipulation instructions

C++20's <bit> header maps directly onto single instructions — if the target has them:

<bit> x86 (BMI1/ABM/BMI2, -march=x86-64-v3) baseline x86-64
std::popcount(x) popcnt a call to __popcountdi2 or a bit-twiddling sequence
std::countl_zero(x) lzcnt bsr + fix-up for 0
std::countr_zero(x) tzcnt bsf (rep bsf) + fix-up for 0
std::rotl(x, k) rol rol
std::byteswap(x) bswap bswap
std::bit_width(x) lzcnt + sub bsr …

Plain g++ -O2 targets the 2003-era x86-64 baseline, which has no popcnt. Either compile for a newer level (-march=x86-64-v2 has popcnt; -v3 adds lzcnt, tzcnt, BMI1/2, AVX2) or mark individual functions with __attribute__((target("popcnt"))) and dispatch at run time.

The std:: functions are defined for every input, including zero: std::countl_zero(0u) == 32. The GCC builtins they replace are not: __builtin_clz(0) and __builtin_ctz(0) are undefined behaviour (the old bsr/bsf instructions leave the destination undefined for zero input). Prefer <bit>.

Useful identities, all branch-free:

Expression Value
x & (x - 1) clear the lowest set bit (blsr)
x & -x isolate the lowest set bit (blsi)
x ^ (x - 1) mask up to and including the lowest set bit (blsmsk)
std::bit_ceil(x) smallest power of two ≥ x (UB-free only if it fits!)
1ull << std::bit_width(x - 1) the same, by hand — careful at x = 0, 1 and > 2⁶³
The lowest-set-bit identities
For x = 0101_1000: subtracting 1 flips the lowest set bit and every 0 below it; -x (two's complement) keeps the lowest set bit and flips everything above. One AND then isolates or clears it.
The lowest-set-bit identities
For x = 0101_1000: subtracting 1 flips the lowest set bit and every 0 below it; -x (two's complement) keeps the lowest set bit and flips everything above. One AND then isolates or clears it.

6. pext and pdep

BMI2's pext gathers the bits of x selected by a mask into the low end; pdep scatters low bits into the mask positions:

pext gathers, pdep scatters
The mask selects bits 7, 6, 3 and 1. pext packs x's bits at those positions into the low end: 1001. pdep puts 1001 back into the same positions: 1000_0010.
pext gathers, pdep scatters
The mask selects bits 7, 6, 3 and 1. pext packs x's bits at those positions into the low end: 1001. pdep puts 1001 back into the same positions: 1000_0010.

They're one instruction on Intel since Haswell and on AMD since Zen 3 (on Zen 1/2 they were microcoded and slow — a good example of "measure on your target"). A portable version loops over the mask's set bits, using m & (m - 1) to step:

C++
#include <cstdint>
#include <cstdio>
#include <immintrin.h>

std::uint64_t pext_portable(std::uint64_t x, std::uint64_t mask) {
    std::uint64_t out = 0;
    for (std::uint64_t bit = 1; mask != 0; bit <<= 1) {
        if (x & mask & -mask) out |= bit;   // lowest remaining mask bit
        mask &= mask - 1;                    // drop it
    }
    return out;
}

__attribute__((target("bmi2"))) std::uint64_t pext_hw(std::uint64_t x, std::uint64_t mask) {
    return _pext_u64(x, mask);
}

int main() {
    std::uint64_t x = 0b1011'0110, m = 0b1100'1010;
    std::printf("%llu %llu\n", (unsigned long long)pext_portable(x, m), (unsigned long long)pext_hw(x, m));
}

(Calling a target("bmi2") function on a CPU without BMI2 crashes with an illegal instruction; real code checks __builtin_cpu_supports("bmi2") first.)

7. Checklist

  • Is the condition predictable? Then a branch is probably fine. Random? Go branchless.
  • Does the listing actually contain cmov/setcc? The compiler may have already done it — or refused because a side has a store or a call.
  • Are the edge cases defined? INT_MIN, shift counts ≥ the width, clz(0).
  • Is the bit instruction available on the target? Check the flags.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Accessible intro to branch misprediction and branchless rewrites (cmov, masks, SIMD) with timings.

Deep dive: when branchless code wins, when it loses, and how to measure misprediction cost.

Live demo where a cmov version of clamp is slower than the branchy one — read the profile, not the folklore.

Bit hacks lecture: swapping, masks, min/max without branches, popcount, n-queens with bitboards.

How compilers recognise a bit-counting loop and replace it with popcnt.

Shifts, rotates and bit-manipulation instructions on x64 (Windows/MASM examples).

C++20 <bit>: popcount, countl_zero, rotl, has_single_bit and friends — portable bit tricks.

The ABM and BMI x86 extensions (popcnt, lzcnt, tzcnt, andn, pext/pdep…) and when compilers use them.