DSA DSA In Depth
0%
lesson bloom-filtercuckoo-filterxor-filter

Bloom Filters, Counting Bloom Filters, Cuckoo Filters & Xor Filters

7 min read Module 2.1: Hash Tables, Modern Probing & Probabilistic Sketches Updated Oct 23, 2026
📄 Cheat Sheet
What Are Bloom Filters? Probabilistic Membership & False Positive Math
Spanning Tree What Are Bloom Filters? Probabilistic Membership & False Positive Math Watch on YouTube ↗

Srini Devadas derives the false positive probability (1 - e^{-kn/m})^k, solves for the optimal number of hash functions k = (m/n) ln 2, and analyzes space-error tradeoffs.

0:00 / --:--

Deriving the Optimal Bloom Filter Parameters ($m, n, k, \varepsilon$)

A standard Bloom filter consists of a bit array $B[0..m-1]$ initialized to $0$ and $k$ hash functions $h_1, \dots, h_k: \mathcal{U} \to \{0, \dots, m-1\}$. To insert $x$, set $B[h_i(x)] \gets 1$ for all $1 \le i \le k$. To query $q$, return PossiblyInSet iff $\bigwedge_{i=1}^k B[h_i(q)] == 1$; if any bit is $0$, $q$ is definitely not in the set.

After inserting $n$ elements, the probability that a specific bit remains $0$ is:

$$p_0 = \left(1 - \frac{1}{m}\right)^{kn} \approx e^{-kn/m}$$

The probability of a false positive for a query $q \notin S$ is the probability that all $k$ probed bits are $1$:

$$\varepsilon = (1 - p_0)^k \approx \left(1 - e^{-kn/m}\right)^k$$

Taking the derivative with respect to $k$ shows that $\varepsilon$ is minimized when exactly half the bits in the array are $1$ ($p_0 = 1/2$), giving the optimal hash count and bit budget:

$$k^* = \frac{m}{n} \ln 2 \approx 0.693 \frac{m}{n}, \qquad \frac{m}{n} = \frac{-\ln \varepsilon}{(\ln 2)^2} \approx 1.4427 \log_2\left(\frac{1}{\varepsilon}\right)\text{ bits/key}$$

Cuckoo Filters (Partial-Key Cuckoo Hashing) & Static Xor Filters

  • Why You Cannot Naive-Clear Bits in a Bloom Filter: Clearing $B[h_i(x)] \gets 0$ on delete can clear a bit shared with another live key $y$, introducing illegal false negatives! Counting Bloom Filters replace each bit with a 4-bit saturating counter, but this $4\times$ memory bloat ($38.4$ bits/key at $\varepsilon = 1\%$) is prohibitive.
  • Cuckoo Filters (Fan et al., 2014): Store only a short $f$-bit fingerprint $\text{fp}(x)$ inside a 4-slot-per-bucket Cuckoo table ($alpha = 0.95$). To relocate an existing fingerprint during Cuckoo eviction without having the original key $x$, Cuckoo Filters use Partial-Key Cuckoo Hashing:

    $$i_1 = h_1(x), \qquad i_2 = i_1 \oplus h_2(\text{fp}(x))$$

    Because XOR is its own inverse ($i_1 = i_2 \oplus h_2(\text{fp}(x))$), the alternate bucket $i_2$ can be computed purely from the current bucket index $i_1$ and the stored fingerprint $\text{fp}(x)$! Deletions simply remove one copy of $\text{fp}(x)$ from bucket $i_1$ or $i_2$.
  • Xor Filters (Graf & Lemire, 2020): For immutable sets (such as LSM SSTables), an Xor Filter constructs an array $B$ of $m = \lfloor 1.23 n \rfloor + 32$ $f$-bit fingerprints via 3-hypergraph peeling so that for every key $x \in S$, $B[h_0(x)] \oplus B[h_1(x)] \oplus B[h_2(x)] == \text{fp}(x)$, achieving false positive rate $2^{-f}$ using only $1.23 f$ bits per key.
Filter Architecture Bits per Key ($ arepsilon = 1\%$) Supports Deletion? Cache Line Reads per Query Information-Theoretic Overhead
Information-Theoretic Lower Bound $6.64$ bits N/A N/A $1.00\times \log_2(1/\varepsilon)$
Standard Bloom Filter ($k=7$) $9.58$ bits No (False negatives) Up to $k=7$ lines (1 if Blocked) $1.44\times \log_2(1/\varepsilon)$
4-Bit Counting Bloom Filter $38.3$ bits Yes (Decrement 4b counters) Up to $k=7$ lines $5.77\times \log_2(1/\varepsilon)$
Cuckoo Filter ($b=4, \alpha=0.95$) $~9.5$ bits Yes ($O(1)$ fingerprint erase) Strict $\le 2$ cache lines $((log_2(1/\varepsilon) + 2) / 0.95)$
Static Xor Filter ($f$-bit) $8.6$ bits ($f=7$) No (Immutable SSTables) Strict $3$ parallel loads $1.23\times \log_2(1/\varepsilon)$

Formal Specification: Kirsch-Mitzenmacher Double Hashing & XOR Dual-Bucket Identity

Kirsch-Mitzenmacher Optimization (2006): Computing $k$ independent hashes is unnecessary. Given just two 32-bit halves $(h_a(x), h_b(x))$ of a single 64-bit hash, generating $k$ indices via:

$$g_i(x) = (h_a(x) + i \cdot h_b(x)) \bmod m \quad \text{for } i = 0, \dots, k-1$$

achieves the exact same asymptotic false positive probability $(1 - e^{-kn/m})^k$ as $k$ mutually independent hash functions!

Cuckoo Filter Involutory Bucket Invariant: Setting $i_2 = i_1 \oplus h(\text{fp}(x))$ ensures that for whichever bucket $i \in \{i_1, i_2\}$ currently holds $\text{fp}(x)$, its sibling bucket is always $i_{\text{alt}} = i \oplus h(\text{fp}(x))$.

Visualizing Blocked Bloom Filters vs Cuckoo Fingerprint Duality

1. Cache-Line Blocked Bloom Filter (512-bit / 64-Byte L1 Block selected by h_high(x)): 0 1 0 1 0 0 1 0 All k=7 probes stay inside ONE 64B cache line! 1 DRAM access instead of k=7 random misses. 2. Cuckoo Filter Partial-Key Eviction (No full key stored, only 8-bit fp): Bucket i1 = h1(x) = 0b011010 | Fingerprint fp = tag(x) = 0x7A | h2(fp) = 0b101101 Alternate Bucket i2 = i1 ^ h2(fp) = 0b110111 ⇔ i1 = i2 ^ h2(fp) = 0b011010 (Self-Inverse!)
flowchart LR subgraph CuckooFilterOps["Cuckoo Filter Insert / Lookup / Delete"] X["Key x"] --> FP["fp = nonzero_tag(x) i1 = h1(x) i2 = i1 ^ h2(fp)"] FP --> Q{"Operation?"} Q -- "Lookup(x)" --> L["Check if fp ∈ Bucket[i1] OR fp ∈ Bucket[i2]"] Q -- "Delete(x)" --> D["Remove 1 copy of fp from Bucket[i1] or Bucket[i2]"] Q -- "Insert(x)" --> I["Place fp in Bucket[i1] or [i2] Else evict random slot e via i = i ^ h2(e)"] end
Bloom Filter k-Hash Bit Array & Cuckoo Fingerprint Kicks
0% False Negatives · ~1.44 log₂(1/ε) bits

Trace k=3 independent hash probes setting bits in an m=12 bit vector (and observe how overlapping bits cause a false positive), or inspect Cuckoo Filter XOR alternate-bucket displacement.

Step 1 / 11
Custom Input:
Timeline

Invariant & State

p ≈ (1 − e^(−kn/m))^k | Bits set: 3/12 (25%) | Est. FPR ≈ 1.6%
🌳 Live 2D Hierarchical Tree Topology — Bloom Filter k-Hash Bit Array & Cuckoo Fingerprint Kicks Visited/Active Pivot/Root Resolved
0 [0] h_1 1 [1] 0 [2] 0 [3] 0 [4] h_2 1 [5] 0 [6] 0 [7] 0 [8] h_3 1 [9] 0 [10] 0 [11]
[0]0
[1]1h_1
[2]0
[3]0
[4]0
[5]1h_2
[6]0
[7]0
[8]0
[9]1h_3
[10]0
[11]0
Operation
ADD "tcp"
Probed Indices
[1, 5, 9]
Bits Set (m=12)
3 / 12
Verdict
Inserted
[Step 1] insert("tcp"): h₁=1, h₂=5, h₃=9 -> set bits [1, 5, 9] = 1.

Hardware Reality: Cache-Line Blocked Bloom Filters & AVX2 Register Masks

A textbook Bloom filter spanning $128\text{ MB}$ with $k = 7$ hash functions performs up to 7 independent DRAM cache misses (~450 ns) on every positive query. Production storage engines (RocksDB, Apache Parquet, DuckDB) use Cache-Line Blocked (or Register-Blocked) Bloom Filters (Putze et al.): the first hash selects a single 64-byte (512-bit) cache line, and all $k$ bits are set/tested inside that single 512-bit block! Even better, Parquet's Split Block Bloom Filter tests $k=8$ bits across eight 32-bit lanes in a single AVX2 _mm256_testc_si256 instruction (1 cache miss + 3 CPU cycles total).

Production C++20 Implementation: Cache-Line Blocked Bloom Filter & Cuckoo Filter

// C++20: 64-Byte Cache-Line Blocked Bloom Filter (1 Cache Miss per Query)
#include <vector>
#include <array>
#include <cstdint>

class BlockedBloomFilter {
    struct alignas(64) CacheLineBlock {
        std::array<uint64_t, 8> words{}; // 512 bits = 64 bytes (1 L1 cache line)
    };

    std::vector<CacheLineBlock> blocks_;
    uint32_t k_;

    static uint64_t mix64(uint64_t x) noexcept {
        x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
        x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
        return x ^ (x >> 31);
    }

public:
    explicit BlockedBloomFilter(size_t numElements, uint32_t bitsPerKey = 10)
        : blocks_((numElements * bitsPerKey + 511) / 512),
          k_(static_cast<uint32_t>(bitsPerKey * 0.693147)) {
        if (blocks_.empty()) blocks_.resize(1);
        if (k_ < 1) k_ = 1;
    }

    void insert(uint64_t key) noexcept {
        uint64_t h = mix64(key);
        size_t blkIdx = ((h >> 32) * blocks_.size()) >> 32; // Lemire FastRange block select
        uint32_t h1 = static_cast<uint32_t>(h);
        uint32_t h2 = static_cast<uint32_t>(mix64(h));

        auto& blk = blocks_[blkIdx];
        for (uint32_t i = 0; i < k_; ++i) {
            uint32_t bit = (h1 + i * h2) & 511U;
            blk.words[bit >> 6] |= (1ULL << (bit & 63U));
        }
    }

    bool possiblyContains(uint64_t key) const noexcept {
        uint64_t h = mix64(key);
        size_t blkIdx = ((h >> 32) * blocks_.size()) >> 32;
        uint32_t h1 = static_cast<uint32_t>(h);
        uint32_t h2 = static_cast<uint32_t>(mix64(h));

        const auto& blk = blocks_[blkIdx];
        for (uint32_t i = 0; i < k_; ++i) {
            uint32_t bit = (h1 + i * h2) & 511U;
            if ((blk.words[bit >> 6] & (1ULL << (bit & 63U))) == 0) {
                return false; // 100% Guaranteed Negative!
            }
        }
        return true;
    }
};

Failure Modes & Adversarial Pitfalls

  • Deleting a Non-Existent Key from a Cuckoo Filter or Counting Bloom Filter: Calling delete(x) on a key $x$ that was never inserted into the filter will corrupt the filter by deleting a colliding key's fingerprint (or decrementing a shared counter to $0$), causing **false negatives** for legitimate keys! Only delete keys confirmed to exist in the backing store.
  • Zero Fingerprint Trap in Cuckoo Filters: If fp == 0 is used as the EMPTY slot sentinel in a Cuckoo Filter, the fingerprint function must remap 0 to 1 (fp = raw_fp ? raw_fp : 1), and $h_2(\text{fp})$ must be hashed before XORing so that h2(fp) != 0 (otherwise $i_1 \oplus 0 = i_1$, collapsing both candidate buckets into one).
Check yourself Question 1 of 4
Concept Check Optimal Hash Function Count in Standard Bloom Filters recall

You allocate a standard Bloom filter with $m$ bits to store $n$ expected distinct keys ($m/n$ bits per key).

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

How do Cache-Blocked Bloom Filters (Putze et al.) and Static Xor Filters (Graf & Lemire) address this hardware bottleneck, and what is their trade-off?

Key Takeaways

A Bloom filter achieves false positive rate ε using m/n = 1.44 log₂(1/ε) bits per element (~9.6 bits/key for 1% FPR) with k = (m/n) ln 2 hash functions and zero false negatives.
Kirsch-Mitzenmacher double hashing generates all k probe indices as (h1(x) + i·h2(x)) mod m from a single 64-bit hash with zero loss in asymptotic accuracy.
Cache-Line Blocked Bloom Filters confine all k bit probes to a single 64-byte L1 cache line, reducing query cost from k DRAM misses to 1 DRAM miss.
Cuckoo Filters support O(1) deletion via partial-key XOR relocation (i2 = i1 ^ h2(fp)), while static Xor Filters achieve 1.23 log₂(1/ε) bits per key with 3 parallel memory reads.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue