Eight words go into a row of 64 bits, three bits each. Then words that were never added ask whether they are in. Watch how the filter answers.
A set that can say maybe
Words set three bits each in a row of 64
static unsigned bit_index(uint32_t h, int i){ uint32_t a = h & 63u; uint32_t b = ((h >> 6) & 63u) | 1u; return (a + (uint32_t)i * b) & 63u;} void add(const char *s){ uint32_t h = fnv1a(s); for (int i = 0; i < K; i++) bits |= (uint64_t)1 << bit_index(h, i);} bool might_contain(const char *s){ uint32_t h = fnv1a(s); for (int i = 0; i < K; i++) if ((bits & ((uint64_t)1 << bit_index(h, i))) == 0) return false; return true;}int main(void){ const char *present[20] = { "filter", "sample", "window", "phase", "gain", "delay", "noise", "pulse", "chirp", "tone", "echo", "carrier", "band", "notch", "ripple", "decay", "spike", "drift", "bias", "clock" }; const char *absent[20] = { "apple", "river", "stone", "cloud", "tiger", "piano", "lemon", "bread", "chair", "glass", "plant", "rope", "sand", "wheel", "coin", "hill", "mask", "nest", "ink", "fog" }; for (int i = 0; i < WORDS; i++) add(present[i]); int set = 0; for (int b = 0; b < 64; b++) set += (int)((bits >> b) & 1u); int missed = 0, maybe = 0; for (int i = 0; i < WORDS; i++) if (!might_contain(present[i])) missed++; for (int i = 0; i < 20; i++) if (might_contain(absent[i])) maybe++; printf("%d words, %d of 64 bits set, %d missed, %d of 20 absent words say maybe\n", WORDS, set, missed, maybe); return 0;}A filter of 64 bits, all dark. Each word will set three of them, chosen by its hash.
- words added
- 0
- bits set
- 0
- absent words saying maybe
- 0 of 0
- formula
- 0.0 %
Output
Describe this picture
Two pictures stacked, over the code: lines 23 to 44 (bit_index, add and might_contain) and 46 to 73 (main) of bloom.c, with the line about to run marked. The top picture is the filter, uint64_t bits: 64 cells numbered 0 to 63, each 0 or 1, a set bit lit. Over them stands the call in play, such as add("filter"), with three arrows from it down to its three bits. Below is a plot of false positives, in per cent from 0 to 25, against words added, from 0 to 20. A line in the second colour is the formula (1 − e^(−3n/64))³; dots in the first colour are the share of 20 words never added that answered “probably”, one dot per number of words. The clip takes about 17 seconds. The eight words filter, sample, window, phase, gain, delay, noise and pulse are added one by one, each sending its arrows down and lighting its bits: filter lights 55, 24 and 57. Some bits are already lit when a later word reaches them. The bits are counted: 22 of 64 are set. Then might_contain("filter") sends its three arrows to 55, 24 and 57, all lit, and the answer beside it reads “probably”. Then might_contain("apple") checks 63, which is lit, then 42, which is dark; the check stops there and the answer reads “certainly not”. The other 19 words never added are tested without a beat of their own, and the output reads “8 words, 22 of 64 bits set, 0 missed, 0 of 20 absent words say maybe”. On the plot the dots for 1 to 7 words are there from the start, all at 0, and the dot for 8, also at 0, appears and is ringed with the output. Under the pictures are one sentence on the current step and the readouts “words added”, “bits set”, “absent words saying maybe” and “formula”, the approximation for the words added so far, which ends at 3.1 % for 8 words. After the clip, the slider “Words added” sets the number of words, from 1 to 20, and replays the run. From 13 words, “tiger”, never added, finds its bits 56, 59 and 62 all lit, and its answer, “probably (never added)”, is in the warning colour; at 20 words apple, tiger and hill all do. The transport plays and pauses, steps one beat back or forward, and scrubs; the number of words and a paused step are kept in the page address as bloom.words and bloom.step.
Bits instead of items
A Bloom filter answers one question about a set: is this item in it? It does not store the items. It keeps an array of m bits, all 0 at first, and k hash functions, each sending an item to one of the bits.
To add an item, set its k bits. To test an item, look at its k bits. If any of them is 0, the item was never added: adding it would have set that bit. If all k are 1, the item was probably added, but the bits might have been set by other items. The filter answers “certainly not” or “probably”, never a certain “yes”.
That is the trade. The filter here holds up to 20 words in 8 bytes, whatever their length, and in return it is sometimes wrong in one direction.
The code and its cost
#define K 3
static uint64_t bits = 0;
static unsigned bit_index(uint32_t h, int i)
{
uint32_t a = h & 63u;
uint32_t b = ((h >> 6) & 63u) | 1u;
return (a + (uint32_t)i * b) & 63u;
}
void add(const char *s)
{
uint32_t h = fnv1a(s);
for (int i = 0; i < K; i++)
bits |= (uint64_t)1 << bit_index(h, i);
}
bool might_contain(const char *s)
{
uint32_t h = fnv1a(s);
for (int i = 0; i < K; i++)
if ((bits & ((uint64_t)1 << bit_index(h, i))) == 0)
return false;
return true;
}
The 64 bits are one uint64_t, used as a set of bits as in Counting and bitmasks: |= sets a bit, & tests one. The three positions come from one FNV-1a hash: a is its low 6 bits, b the next 6 with the lowest bit forced to 1, and the positions are a, a + b and a + 2b, each mod 64. Because b is odd, the three are always different. For “filter”, FNV-1a gives 3353438327, so a = 55, b = 33, and the positions are 55, 24 and 57.
The cast in (uint64_t)1 << ... matters. A plain 1 is an int, 32 bits here and signed, and shifting it by 31 or more is undefined behaviour in C: at 31 the result, 2³¹, no longer fits in an int, and from 32 the shift is as wide as the type. 1u, an unsigned int, can be shifted by up to 31, and the 64-bit (uint64_t)1 used here by up to 63.
Adding and testing each take k steps, plus one hash, however many items the filter holds. main adds the first 8 of its 20 words, tests those 8, then tests 20 words it never added, and prints 8 words, 22 of 64 bits set, 0 missed, 0 of 20 absent words say maybe.
No false negatives
A word that was added always answers “probably”. Its k bits were set when it was added, and nothing ever clears a bit, so they are all still 1. The filter is never wrong about an item it holds: “missed” is 0 in every run from 1 to 20 words.
The other answer can be wrong. A word that was never added answers “probably” when all of its bits happen to be set by other words: a false positive. With 16 words in the filter, “tiger” finds its bits 56, 59 and 62 all lit and says “probably”.
How often it is wrong
With m bits, k hash functions and n items added, take one bit. Each of the kn bit settings misses it with probability 1 − 1/m, so if the settings fall independently and uniformly over the bits, the bit is still 0 afterwards with probability
A word never added says “probably” when all k of its bits are 1. If its k positions are independent of each other and of the bits already set, that happens with probability about
This is an approximation, and it rests on independence: every position equally likely and unrelated to every other. Real hashes only come close. Here the three positions are not even independent of each other, since they are a, a + b and a + 2b from one hash; a filter built for real use would derive them from better-separated hashes.
The traced runs give a check. For m = 64 and k = 3, the formula expects about 64(1 − e^(−0.375)) ≈ 20 bits set after 8 words; the run sets 22. It predicts 3.1 % false positives, 0.6 of 20 test words; the run measures 0 of 20. At 20 words it predicts 22.5 %, 4.5 of 20; the run measures 3 of 20, 15 %. With only 20 test words the measured share moves in steps of 5 %. If each test word were fooled independently with probability 0.225, the count out of 20 would have a standard deviation of about 1.9, so 3 against 4.5 is well within the scatter. On the plot every dot sits below the line, partly because this hash sets fewer bits than the formula expects: 34 at 20 words, against about 39.
Choosing k
More hash functions make each test harder to fool, but they also set more bits, so the array fills faster. For given m and n, the rate is least at about
and there each bit is 1 with probability about one half. With m = 64: for 8 words the best k is 8 ln 2 ≈ 5.5; for 20 words it is 3.2 ln 2 ≈ 2.2. The program’s k = 3 would be the best choice for about 15 words: (64/15) ln 2 ≈ 3.0.
No removal
An item cannot be taken out. Clearing its k bits could clear a bit another item needs, and that item would then answer “certainly not”: a false negative. A counting filter keeps a small counter per position instead of a bit, adding 1 on add and subtracting 1 on removal.
Worked example
The formula for m = 64, k = 3.
| Words added n | kn/m | (1 − e^(−kn/m))³ | measured, of 20 |
|---|---|---|---|
| 4 | 0.1875 | 0.005 | 0 |
| 8 | 0.375 | 0.031 | 0 |
| 12 | 0.5625 | 0.080 | 0 |
| 16 | 0.75 | 0.147 | 1 |
| 20 | 0.9375 | 0.225 | 3 |
Doubling the bits. With n = 16 and k = 3, 128 bits instead of 64 give kn/m = 0.375 again, so the rate falls from 0.147 to 0.031, the same as 8 words in 64 bits. For a fixed k, in the approximation, only the ratio n/m matters.
A million items at 1 %. Putting the best k back into the formula gives a rate of about (1/2)^k, and solving for the bits per item gives
For p = 0.01 that is 4.605 / 0.4805 ≈ 9.6 bits per item, about 9.6 million bits, or 1.2 MB, for a million items, with k = 9.6 ln 2 ≈ 6.6, so 7 hash functions. Storing a million 20-byte keys instead would take 20 MB.
Two checks. Can “certainly not” ever be wrong? (No: every bit an added word set stays set.) What happens to the rate when m doubles and n stays the same? (It falls: at n = 16 and k = 3, from 0.147 to 0.031.)
Where it’s used
Databases and key–value stores, which keep a filter per file on disk and skip reading the file when the filter says a key is certainly not there.
Web browsers, some of which have checked addresses against a filter of unsafe sites before asking a server.
Spell checkers and network routers, where a small filter in fast memory answers most questions and only the “probably” answers need a slower check.
Reference card
| Code or idea | Meaning |
|---|---|
| m bits, k hash functions | the filter; here m = 64, k = 3 |
| add | set the item’s k bits |
| test, any bit 0 | certainly not in the set |
| test, all bits 1 | probably in the set |
| false negatives | never |
| false positives | about (1 − e^(−kn/m))^k, if the bits are independent |
| best k | about (m/n) ln 2 |
| bits per item for rate p | about −ln p / (ln 2)² |
| removal | not possible; a counting filter keeps counters |
Platform: C11, gcc on x86-64 Linux: int 4 bytes, uint64_t 8, little-endian. The bit positions come from FNV-1a as this page computes it; addresses in the pictures are short stand-ins, by one rule for the whole course.