Back to log

The If-Statement Retirement Plan

How I keep collapsing pages of slow, branchy decision logic into one register of bits and a Hamming distance. A love letter to approximate membership queries.

An office farewell party for a giant if-statement: movers wheel out a filing cabinet stuffed with paper while coworkers clap, a sheet cake reading GOOD LUCK and a gold retirement watch sit on a table, and in sharp focus on an empty desk in the foreground sits a single Las Vegas poker chip that replaced the whole department.

Somewhere in every production system there is a function nobody loves. It is four hundred lines of if-statements. It checks a deny list, then a beta flag, then a tenant tier, then a regex somebody wrote in 2019, then a country table, and then, if the stars align, it lets the request through. It is called something dignified like evaluate_policy. It is the slowest thing in the hot path and everyone has agreed not to look at it.

I have a retirement plan for that function. It fits in your L1 cache, it never takes a branch, and it answers in nanoseconds. This post is about the pattern, because I keep reaching for it, in edge routers, in decision services, in places I probably should not admit, and it keeps working.


The bouncer with a perfect memory for strangers

Start with the building block. An approximate membership query filter, an AMQ, is a tiny probabilistic structure that answers one question: is this thing in the set? A Bloom filter is the famous one; cuckoo, quotient, XOR, and binary fuse filters are the modern family. They all make the same strange bargain.

An AMQ can be wrong in exactly one direction. If it says no, that is the truth, guaranteed, always. If it says yes, it is almost certainly right, with a tunable sliver of maybe. It is a bouncer who has never once forgotten a banned face, but occasionally waves in a stranger out of enthusiasm.

That asymmetry sounds like a defect. It is the entire product. Because the no is certain, the filter can stand in front of anything expensive, a disk, a network hop, a database, a four-hundred-line function, and absorb almost all of the traffic that was never going to matter. The expensive thing only wakes up for the maybes. And the filter doing this work is a few bits per element. A million banned keys become a structure smaller than the photo on your badge.

I wrote about the structures themselves in an earlier post here. Today is about the part I find genuinely fun: what happens when you stop using one filter as a gate and start using several as a language.


The leap: any decision is a membership question wearing a costume

Here is the reframe that retires the if-statements. Look at that policy function again. Every branch in it is secretly asking whether the input belongs to some set. Is this key on the deny list. Is this tenant in the beta cohort. Is this route in the premium table. Does this pattern belong to the family of strings that regex was really checking for.

Membership. Membership. Membership. The function is not logic, it is a stack of set-membership tests with ceremony in between.

So strip the ceremony. Build one filter per question. At decision time, probe all of them with the same key, in parallel, and collect the answers as bits in a single machine word. Deny bit, beta bit, premium bit, suspicious bit. Your entire policy state for this request is now one register.

Then the last trick, and it is my favorite. Each outcome your system supports, allow, block, reroute, throttle, escalate, is just a bit pattern, a codeword. The decision is whichever codeword your answer register is closest to. Closest, as in Hamming distance, as in XOR and a popcount, two instructions the hardware does for breakfast.

No branches. Not fewer branches. None. The four hundred lines became: hash, probe, gather, XOR, popcount, done. The logic did not get faster. The logic left. What remains is arithmetic.


Why this is unreasonably fast, in three physics facts

The speed is not cleverness, it is geography.

First, branches are expensive in a way that never shows up in code review. A mispredicted branch flushes the pipeline, and a policy function made of data-dependent branches on adversarial traffic mispredicts constantly. The branchless version has nothing to mispredict. The CPU sees a straight road and floors it.

Second, the filters are small enough to live in L1 cache, and L1 is roughly a hundred times closer than RAM. A rule engine that chases pointers through hash maps is a museum of cache misses. A fistful of filters is a poker chip sitting on the register file.

Third, the probes do not care about each other, so they vectorize. One key, eight questions, answered as a batch. The hardware has been begging for workloads shaped like this since the first SIMD unit shipped, and we keep feeding it linked lists instead.

Stack those three and the before-and-after graphs stop looking like an optimization and start looking like a typo. That is not because I am clever. It is because the if-statement version was paying three separate physics taxes on every single call, and this shape pays none of them.


The part that makes it a control plane, not a party trick

Here is what I did not appreciate when I first fell for this pattern. The filters are data. Not code. Data.

That means the policy can change without a deploy. Marketing moves the beta cohort, security adds ten thousand keys to the deny list, the ops team reroutes a tenant: rebuild the affected filter offline, ship a few kilobytes, swap a pointer. The decision engine never restarts, never recompiles, never even notices. You have separated the plane that decides from the plane that describes what to decide, which is the same separation networking people fought a decade for, shrunk down to a register.

And the false positives, the bouncer’s occasional enthusiasm, get handled the same way they always should be: point the uncertainty in the safe direction, and verify on the slow path. A false positive on the deny filter means one legitimate request takes the expensive check it would have taken anyway in the old world. The certain answers, the vast majority, never pay at all. You are not accepting errors. You are choosing where the rare doubt lands, and billing it to the path that could always afford it.


What travels

  • Hunt for membership questions wearing costumes. Deny lists and feature flags are obvious. Regexes, tier checks, and routing tables are the same animal, shaved.
  • One register can hold your whole policy. Probe filters in parallel, gather bits, match to the nearest codeword. XOR and popcount are the entire runtime.
  • Make config into data with a rebuild step. Filters swap in like textures in a game engine. Deploys are for logic; this is not logic anymore.
  • Aim the false positives at the safe side, and keep a slow path that verifies. The bouncer waves in a stranger; the stranger still shows ID at the bar.

The four-hundred-line function is out there right now, in your codebase, taking branches, missing cache, evaluating a regex per request like it is being paid by the cycle. Throw it a party. Get it a cake. It worked hard.

Then replace it with a poker chip.


Hash, probe, gather, XOR, popcount. Five verbs. No branches. No regrets.

Verify what’s in this post:

  • The Magic of AMQ, earlier on this blog, covers the filter structures themselves.
  • Bloom (1970) started it; cuckoo, quotient, XOR, and binary fuse filters are the modern family, all public literature.
  • The one-directional error guarantee, no false negatives ever, is the defining property of the whole AMQ class.

Jonathan Corners · voxell.ai · sentimark.ai

Something in this log entry sparking an argument, partnership idea, or infrastructure war story?

Open a channel