Branchless Rust: removing an if makes a filter almost 4x faster
Serhii Potapov, in a blog post dated August 2, 2026, describes optimizing a hot path after a career spent mostly in domain programming, where he says correctness matters far more than performance. The task: filter a slice of numbers and keep only the elements above a given threshold, which he calls a typical problem that database engines solve all day long. The obvious Rust implementation, input.iter().copied().filter(|&x| x > threshold).collect(), is idiomatic and easy to read, so he benchmarks it with the criterion crate against one million random f64 values uniformly spread over 0.0 to 100.0, using five thresholds chosen so the filter keeps 1%, 25%, 50%, 75% or 99% of the elements; the 50.0 threshold, for instance, keeps about half.
The benchmark produces a puzzle. The row that keeps 50% of the elements is the slowest of the five, even though the row that keeps 99% copies almost twice as much data and still runs 2.6 times faster. Since the amount of input is identical in every row, the amount of output copied does not explain the gap.
Potapov's first suspect is memory allocation. collect() does not know the output size in advance, so the Vec has to grow and reallocate as it fills. Preallocating with Vec::with_capacity(input.len()) and pushing matched elements in a plain loop brings the 50%-selectivity time down to 3.87 ms, only about 2% faster than the original. Reallocation is real, he concludes, but it was never the bottleneck.
The actual cause sits inside the CPU. A modern processor pipelines instructions, fetching and decoding the next ones while the current one executes, and it does not stop at a branch like if x > threshold to wait for the comparison to resolve. Instead a branch predictor guesses which way execution will go and runs ahead speculatively; Potapov compares it to a barista who starts making a regular customer's usual order the moment they walk in. A wrong guess forces the CPU to discard that speculative work and restart from the branch, which costs about 15 to 20 cycles on a typical modern x86 core, against roughly one cycle for the comparison itself. At 1% or 99% selectivity the outcome is almost always the same, so the predictor is right almost every time and the branch is nearly free. At 50% selectivity on random data there is no pattern to learn: the predictor is reduced to guessing, wrong on about half a million of the one million elements, and at 15 to 20 cycles per flush that adds up to roughly 2 ms of pure penalty on a 4 GHz core, about the size of the gap between the 50% and 99% rows.
To confirm the theory, Potapov sorts the same input outside the timed section and reruns the 50%-selectivity case with the identical function and threshold. It comes out 4.5 times faster. A sorted array lets the branch go "skip" for the entire first half and "keep" for the entire second half, a pattern even a simple predictor learns after one miss. He links a Stack Overflow question with 27,000 upvotes asking essentially the same thing: why processing a sorted array is faster than processing an unsorted one. Sorting itself is not a real fix, he notes, since sorting costs more than the filtering itself and the original order is usually still needed afterward, but the experiment isolates the actual cause.
The fix is to remove the unpredictable branch rather than sort around it. In the branchless version, Potapov allocates an output vector the same length as the input, then loops over every element, writing it unconditionally to out[n] and advancing the cursor n by (x > threshold) as usize, which evaluates to 1 when the element clears the threshold and 0 otherwise; when it does not, the next iteration simply overwrites the same slot. After the loop, out.truncate(n) drops the unused tail. The comparison is still computed, but its result is now used as a number that moves a cursor rather than as a decision about where the program goes next; in the generated assembly it becomes a seta instruction that just produces 0 or 1, so there is no fork left to mispredict. He notes that out[n] = x still performs a bounds check and the loop's own continuation condition is itself a branch, but both go the same way a million times in a row, so the predictor handles them for free. Only the branch on unpredictable data needed to disappear.
The payoff shows up exactly where the branching versions struggled: the worst case, 50% selectivity, becomes almost 4 times faster, and the branchless implementation's running time stays essentially flat across all five selectivity levels instead of peaking in the middle. The trade shows up at the other end, though. At 1% selectivity the original idiomatic version still wins, because an almost-always-correctly-predicted branch is nearly free, while the branchless version always pays the cost of writing all one million elements no matter how few are kept. As Potapov frames it, branchless code is not a general speedup: it swaps a worse best case for a better worst case.
Potapov's own recommendation is not to reach for this technique by default. Branchless code is harder to read and easier to get wrong, and compilers already automate a lot of the same optimization on their own, so most of the time the answer is no. He reserves it for the specific case where a profiler has pointed at a hot loop that contains a branch on unpredictable data. The post links to the branchless-rust-benchmarks repository so the numbers can be reproduced, along with the Stack Overflow question on sorted arrays, a Wikipedia article on branch predictors, a blog post by Daniel Lemire on mispredicted branches, and a CppCon 2021 talk on branchless programming in C++ by Fedor Pikus.
Key facts
- Serhii Potapov's blog post, dated August 2, 2026, benchmarks a Rust filter over one million random f64 values and finds the plain iterator implementation slowest at 50% selectivity, even though the 99% case copies almost twice as much data yet runs 2.6 times faster.
- Preallocating the output vector with Vec::with_capacity barely helps: at 50% selectivity it lands at 3.87 ms, only about 2% faster than the original, which rules out reallocation as the real bottleneck.
- The real cause is CPU branch misprediction: on random data the predictor is reduced to a coin flip at 50% selectivity, producing about half a million mispredictions that cost roughly 2 ms on a 4 GHz core; sorting the same data first, outside the timed section, makes the identical function 4.5 times faster.
- Rewriting the filter as branchless code, writing every element unconditionally and advancing the cursor only through (x > threshold) as usize, makes the worst case (50% selectivity) almost 4 times faster and keeps the running time flat across all five selectivity levels.
- The trade cuts the other way at 1% selectivity, where the original branching version still wins because an almost-always-correct branch is nearly free while the branchless version always pays for writing all one million elements; Potapov reserves the technique for hot loops a profiler has actually flagged.
Why it matters
The post is a clean demonstration of a gap that trips up even careful engineers: the amount of work a piece of code does on paper (copy half the elements versus almost all of them) does not predict how fast it actually runs on real hardware. Potapov's first, most natural fix, preallocating the output vector to avoid reallocation, barely moved the needle: about 2% faster at 50% selectivity, exactly the kind of near-miss that can send an optimization effort down the wrong path. The real cost turns out to live one level below the code, in the CPU's branch predictor, and it appears specifically on data with no pattern to learn, the case Potapov frames as "a typical problem that database engines solve all day long." The general lesson, that a cheap branch becomes expensive the moment it turns unpredictable and that this can turn an innocent-looking if into the slowest part of a hot loop, applies well beyond this one filter function.
Who it affects
Rust developers and, more broadly, anyone writing performance-sensitive filtering or scanning loops over large, unpredictable datasets, the kind of code Potapov compares to what database engines run constantly. It is explicitly less relevant to the domain and business-logic programming he says he has spent most of his career doing, where correctness matters far more than raw speed and this level of micro-optimization would rarely be justified. Engineers who profile their own hot paths and find a branch on unpredictable data get the most direct payoff; everyone else can still take the general branch-prediction explanation, which the post ties to established outside references including a Daniel Lemire post, a CppCon 2021 talk by Fedor Pikus, and a Stack Overflow question with 27,000 upvotes on sorted-versus-unsorted array performance. The benchmark code is public in the branchless-rust-benchmarks repository, so any Rust developer can rerun the comparison on their own hardware rather than take the laptop numbers on faith.
How to use it
There is nothing to buy or sign up for here: the value is the technique and the reproducible benchmark, not a product. The article walks through three concrete Rust implementations in order: the idiomatic input.iter().filter().collect() version, a preallocated Vec::with_capacity loop, and the branchless version that writes every element to out[n] and advances n by (x > threshold) as usize before truncating to n at the end, so a developer can copy whichever fits and adapt the threshold comparison to their own filter condition. Potapov's own advice on when to reach for the branchless form is narrow: only after a profiler has pointed at a specific hot loop that contains a branch on data with no predictable pattern, not as a default habit, since compilers already perform a lot of equivalent optimization automatically and branchless code is harder to read and easier to get wrong. The benchmarks themselves are built with the criterion crate and published in the branchless-rust-benchmarks repository, so the exact numbers can be reproduced rather than taken on trust.
How solid is it
The measurements come from one person's own benchmarks on one laptop, an Intel i7-10875H, run with the widely used criterion crate; the code to reproduce them is public in the branchless-rust-benchmarks repository, but the post does not state the Rust compiler version, build flags, or operating system used. Only one exact timing figure appears in the running text, 3.87 ms for the preallocated version at 50% selectivity; every other comparison is given as a ratio (2.6 times, 4.5 times, almost 4 times faster) rather than an absolute number, referring to result tables that read as images not captured in the extracted text. The underlying phenomenon itself is not new: that branch mispredictions cost roughly 15 to 20 cycles on modern x86 cores, against about one cycle for the comparison itself, is documented in the sources the post links: a Stack Overflow question with 27,000 upvotes, a Wikipedia article on branch predictors, a Daniel Lemire post, and a Fedor Pikus CppCon 2021 talk. The comparison is limited to single-threaded scalar Rust; no SIMD, auto-vectorization, multithreading, or GPU approach is discussed. On Hacker News the story drew a modest 61 points and 13 comments over about 70 hours.
Risks and caveats
The technique is explicitly a trade, not a free win: the branchless version loses to the original at low selectivity (1% kept) because it always pays for writing all one million elements regardless of how many actually clear the threshold, so applying it outside a genuinely unpredictable hot loop can make code slower as well as harder to read. Potapov is direct that most of the time the answer is not to go branchless, since compilers already automate much of the same optimization and hand-rolled branchless code is easier to get subtly wrong. The absolute timings are specific to one laptop's CPU with no stated compiler version or build flags, so they may not transfer directly to other hardware or toolchains, and the article never names the real production hot path that originally motivated the exploration, so a reader has to judge independently whether their own workload actually matches the unpredictable-branch pattern this fix addresses.
“A branch is cheap. A mispredicted branch is not.”
— Serhii Potapov, in the post's conclusion