use std::hash::{BuildHasher, Hash};
const CELL_BITS: u32 = 4;
const CELLS_PER_WORD: usize = 16;
const CELL_MAX: u64 = 0xF;
#[derive(Debug, Clone)]
pub struct CountingBloomFilter<S = std::hash::RandomState> {
words: Vec<u64>,
cells: usize,
hashes: usize,
hasher_builder: S,
}
impl CountingBloomFilter<std::hash::RandomState> {
pub fn with_capacity(expected_items: usize, fp_rate: f64) -> Self {
Self::with_capacity_and_hasher(expected_items, fp_rate, std::hash::RandomState::new())
}
}
impl<S: BuildHasher> CountingBloomFilter<S> {
pub fn with_capacity_and_hasher(
expected_items: usize,
fp_rate: f64,
hasher_builder: S,
) -> Self {
let n = expected_items.max(1) as f64;
let p = fp_rate.clamp(f64::MIN_POSITIVE, 1.0 - f64::EPSILON);
let ln2 = std::f64::consts::LN_2;
let cells = (-n * p.ln() / (ln2 * ln2)).ceil() as usize;
let cells = cells.max(64);
let hashes = (((cells as f64 / n) * ln2).round() as usize).clamp(1, 64);
let words = cells.div_ceil(CELLS_PER_WORD);
Self {
words: vec![0u64; words],
cells,
hashes,
hasher_builder,
}
}
pub fn cell_len(&self) -> usize {
self.cells
}
pub fn hash_count(&self) -> usize {
self.hashes
}
fn h1_h2(&self, item: &impl Hash) -> (u32, u32) {
let h = self.hasher_builder.hash_one(item);
let h1 = (h >> 32) as u32;
let h2 = (h as u32) | 1;
(h1, h2)
}
fn cell_index(&self, h1: u32, h2: u32, i: usize) -> usize {
(h1.wrapping_add((i as u32).wrapping_mul(h2)) as usize) % self.cells
}
fn cell_get(&self, cell: usize) -> u64 {
let word = cell / CELLS_PER_WORD;
let shift = (cell % CELLS_PER_WORD) as u32 * CELL_BITS;
(self.words[word] >> shift) & CELL_MAX
}
fn cell_set(&mut self, cell: usize, value: u64) {
let word = cell / CELLS_PER_WORD;
let shift = (cell % CELLS_PER_WORD) as u32 * CELL_BITS;
self.words[word] =
(self.words[word] & !(CELL_MAX << shift)) | ((value & CELL_MAX) << shift);
}
pub fn insert(&mut self, item: impl Hash) -> bool {
let (h1, h2) = self.h1_h2(&item);
let mut newly = false;
for i in 0..self.hashes {
let cell = self.cell_index(h1, h2, i);
let v = self.cell_get(cell);
if v == 0 {
newly = true;
}
if v < CELL_MAX {
self.cell_set(cell, v + 1);
}
}
newly
}
pub fn remove(&mut self, item: &impl Hash) -> bool {
let (h1, h2) = self.h1_h2(item);
for i in 0..self.hashes {
if self.cell_get(self.cell_index(h1, h2, i)) == 0 {
return false;
}
}
for i in 0..self.hashes {
let cell = self.cell_index(h1, h2, i);
let v = self.cell_get(cell);
if v < CELL_MAX {
self.cell_set(cell, v - 1);
}
}
true
}
pub fn contains(&self, item: &impl Hash) -> bool {
let (h1, h2) = self.h1_h2(item);
for i in 0..self.hashes {
if self.cell_get(self.cell_index(h1, h2, i)) == 0 {
return false;
}
}
true
}
pub fn clear(&mut self) {
for w in &mut self.words {
*w = 0;
}
}
pub fn is_empty(&self) -> bool {
self.words.iter().all(|&w| w == 0)
}
pub fn saturation(&self) -> f64 {
let mut set = 0usize;
for cell in 0..self.cells {
if self.cell_get(cell) != 0 {
set += 1;
}
}
set as f64 / self.cells as f64
}
}
impl<S: BuildHasher> crate::correlate::Mergeable for CountingBloomFilter<S> {
fn merge(&mut self, other: Self) {
assert_eq!(
(self.cells, self.hashes),
(other.cells, other.hashes),
"CountingBloomFilter::merge requires matching cell count and hash count",
);
for cell in 0..self.cells {
let word = cell / CELLS_PER_WORD;
let shift = (cell % CELLS_PER_WORD) as u32 * CELL_BITS;
let a = (self.words[word] >> shift) & CELL_MAX;
let b = (other.words[word] >> shift) & CELL_MAX;
let merged = (a + b).min(CELL_MAX);
self.words[word] = (self.words[word] & !(CELL_MAX << shift)) | (merged << shift);
}
}
}
#[cfg(test)]
mod tests {
use std::hash::{BuildHasherDefault, DefaultHasher};
use super::*;
use crate::correlate::Mergeable;
type FixedHasher = BuildHasherDefault<DefaultHasher>;
fn fixed(expected: usize, fp: f64) -> CountingBloomFilter<FixedHasher> {
CountingBloomFilter::with_capacity_and_hasher(expected, fp, FixedHasher::default())
}
#[test]
fn no_false_negatives() {
let mut f = fixed(10_000, 0.01);
for i in 0..10_000u32 {
f.insert(i);
}
for i in 0..10_000u32 {
assert!(f.contains(&i), "inserted item {i} must be contained");
}
}
#[test]
fn insert_remove_round_trip() {
let mut f = fixed(1_000, 0.01);
for i in 0..100u32 {
f.insert(i);
}
assert!(f.remove(&42u32));
assert!(!f.contains(&42u32), "removed item is gone");
for i in (0..100u32).filter(|i| *i != 42) {
assert!(f.contains(&i), "item {i} unaffected by remove");
}
}
#[test]
fn remove_of_absent_item_is_noop() {
let mut f = fixed(1_000, 0.01);
f.insert(1u32);
assert!(!f.remove(&999_999u32), "absent item reports false");
assert!(f.contains(&1u32), "present item not corrupted");
}
#[test]
fn double_insert_needs_double_remove() {
let mut f = fixed(1_000, 0.01);
f.insert(7u32);
f.insert(7u32);
assert!(f.remove(&7u32));
assert!(f.contains(&7u32), "one of two insertions remains");
assert!(f.remove(&7u32));
assert!(!f.contains(&7u32));
}
#[test]
fn saturated_cells_are_sticky() {
let mut f = fixed(1_000, 0.01);
for _ in 0..20 {
f.insert(3u32);
}
for _ in 0..20 {
f.remove(&3u32);
}
assert!(f.contains(&3u32), "sticky saturation never clears");
}
#[test]
fn insert_returns_probably_new() {
let mut f = fixed(1_000, 0.01);
assert!(f.insert(1u32), "first insert is new");
assert!(!f.insert(1u32), "second insert of same item is not");
}
#[test]
fn fp_rate_within_expectations() {
let mut f = fixed(10_000, 0.01);
for i in 0..10_000u32 {
f.insert(i);
}
let mut fps = 0usize;
for i in 10_000..20_000u32 {
if f.contains(&i) {
fps += 1;
}
}
assert!(fps < 300, "false positives {fps} exceed 3x target");
}
#[test]
fn clear_and_is_empty() {
let mut f = fixed(100, 0.01);
assert!(f.is_empty());
f.insert(1u32);
assert!(!f.is_empty());
assert!(f.saturation() > 0.0);
f.clear();
assert!(f.is_empty());
assert_eq!(f.saturation(), 0.0);
}
#[test]
fn merge_unions_counts() {
let mut a = fixed(1_000, 0.01);
let mut b = fixed(1_000, 0.01);
a.insert(1u32);
b.insert(2u32);
b.insert(1u32);
a.merge(b);
assert!(a.contains(&1u32));
assert!(a.contains(&2u32));
assert!(a.remove(&1u32));
assert!(a.contains(&1u32));
assert!(a.remove(&1u32));
assert!(!a.contains(&1u32));
}
#[test]
fn merge_is_commutative() {
let mk = |items: &[u32]| {
let mut f = fixed(1_000, 0.01);
for i in items {
f.insert(*i);
}
f
};
let mut ab = mk(&[1, 2, 3]);
ab.merge(mk(&[3, 4]));
let mut ba = mk(&[3, 4]);
ba.merge(mk(&[1, 2, 3]));
assert_eq!(ab.words, ba.words, "cellwise saturating add commutes");
}
#[test]
#[should_panic(expected = "matching cell count and hash count")]
fn merge_panics_on_size_mismatch() {
let mut a = fixed(1_000, 0.01);
let b = fixed(50_000, 0.01);
a.merge(b);
}
}