use crate::{BUCKET_SIZE, alt_index_of_fp, fnv1a64, mix};
const MAX_KICKS: usize = 500;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FingerprintWidth {
Eight,
Twelve,
Sixteen,
}
impl FingerprintWidth {
pub fn bits(self) -> u32 {
match self {
FingerprintWidth::Eight => 8,
FingerprintWidth::Twelve => 12,
FingerprintWidth::Sixteen => 16,
}
}
fn mask(self) -> u16 {
match self {
FingerprintWidth::Eight => 0x00ff,
FingerprintWidth::Twelve => 0x0fff,
FingerprintWidth::Sixteen => 0xffff,
}
}
}
pub struct VariableFpCuckooFilter {
width: FingerprintWidth,
buckets: Vec<[u16; BUCKET_SIZE]>,
mask: usize,
count: usize,
rng_state: u64,
}
impl VariableFpCuckooFilter {
pub fn new(expected_entries: usize, width: FingerprintWidth) -> Self {
let needed = (expected_entries.max(1) * 105 / 100) / BUCKET_SIZE + 1;
let num_buckets = needed.max(2).next_power_of_two();
Self {
width,
buckets: vec![[0u16; BUCKET_SIZE]; num_buckets],
mask: num_buckets - 1,
count: 0,
rng_state: 0x9e3779b97f4a7c15,
}
}
pub fn width(&self) -> FingerprintWidth {
self.width
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn bucket_count(&self) -> usize {
self.buckets.len()
}
pub fn insert(&mut self, key: &str) -> bool {
let (fp, i1, i2) = self.indices(key);
if self.try_place(i1, fp) || self.try_place(i2, fp) {
self.count += 1;
return true;
}
let mut bucket_idx = if self.rand_bit() { i1 } else { i2 };
let mut victim = fp;
for _ in 0..MAX_KICKS {
let slot = (self.next_random() as usize) % BUCKET_SIZE;
std::mem::swap(&mut victim, &mut self.buckets[bucket_idx][slot]);
bucket_idx ^= alt_index_of_fp_u16(victim) & self.mask;
if self.try_place(bucket_idx, victim) {
self.count += 1;
return true;
}
}
false
}
pub fn contains(&self, key: &str) -> bool {
let (fp, i1, i2) = self.indices(key);
self.bucket_has(i1, fp) || self.bucket_has(i2, fp)
}
pub fn delete(&mut self, key: &str) -> bool {
let (fp, i1, i2) = self.indices(key);
if self.bucket_remove(i1, fp) || self.bucket_remove(i2, fp) {
self.count -= 1;
return true;
}
false
}
fn indices(&self, key: &str) -> (u16, usize, usize) {
let h = mix(fnv1a64(key.as_bytes()));
let raw = (h as u16) & self.width.mask();
let fp = if raw == 0 { 1 } else { raw };
let i1 = (h >> 16) as usize & self.mask;
let i2 = (i1 ^ alt_index_of_fp_u16(fp)) & self.mask;
(fp, i1, i2)
}
fn try_place(&mut self, i: usize, fp: u16) -> bool {
for slot in &mut self.buckets[i] {
if *slot == 0 {
*slot = fp;
return true;
}
}
false
}
fn bucket_has(&self, i: usize, fp: u16) -> bool {
self.buckets[i].contains(&fp)
}
fn bucket_remove(&mut self, i: usize, fp: u16) -> bool {
for slot in &mut self.buckets[i] {
if *slot == fp {
*slot = 0;
return true;
}
}
false
}
fn rand_bit(&mut self) -> bool {
self.next_random() & 1 != 0
}
fn next_random(&mut self) -> u64 {
self.rng_state = self
.rng_state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.rng_state
}
}
fn alt_index_of_fp_u16(fp: u16) -> usize {
if fp <= u8::MAX as u16 {
alt_index_of_fp(fp as u8)
} else {
(fp as u64).wrapping_mul(0x5bd1e9955_u64) as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_eight_bit() {
let mut cf = VariableFpCuckooFilter::new(1000, FingerprintWidth::Eight);
for i in 0..500u32 {
assert!(cf.insert(&format!("k{i}")));
}
for i in 0..500u32 {
assert!(cf.contains(&format!("k{i}")));
}
for i in 0..500u32 {
assert!(cf.delete(&format!("k{i}")));
}
assert_eq!(cf.len(), 0);
}
#[test]
fn round_trip_twelve_bit() {
let mut cf = VariableFpCuckooFilter::new(1000, FingerprintWidth::Twelve);
for i in 0..500u32 {
assert!(cf.insert(&format!("k{i}")));
}
for i in 0..500u32 {
assert!(cf.contains(&format!("k{i}")));
}
assert_eq!(cf.len(), 500);
}
#[test]
fn round_trip_sixteen_bit() {
let mut cf = VariableFpCuckooFilter::new(1000, FingerprintWidth::Sixteen);
for i in 0..500u32 {
assert!(cf.insert(&format!("k{i}")));
}
for i in 0..500u32 {
assert!(cf.contains(&format!("k{i}")));
}
}
#[test]
fn wider_fingerprint_lowers_fpr() {
let n = 5_000usize;
let probes = 10_000usize;
let mut narrow = VariableFpCuckooFilter::new(n, FingerprintWidth::Eight);
let mut wide = VariableFpCuckooFilter::new(n, FingerprintWidth::Sixteen);
for i in 0..n {
narrow.insert(&format!("present{i}"));
wide.insert(&format!("present{i}"));
}
let mut narrow_fp = 0usize;
let mut wide_fp = 0usize;
for i in 0..probes {
let k = format!("absent{i}");
if narrow.contains(&k) {
narrow_fp += 1;
}
if wide.contains(&k) {
wide_fp += 1;
}
}
assert!(
wide_fp < narrow_fp,
"wide_fp={wide_fp} should be < narrow_fp={narrow_fp}"
);
}
#[test]
fn empty_filter_rejects_everything() {
let cf = VariableFpCuckooFilter::new(100, FingerprintWidth::Twelve);
assert!(!cf.contains("never-inserted"));
assert!(cf.is_empty());
}
#[test]
fn width_accessor_reports_configured_value() {
let cf = VariableFpCuckooFilter::new(100, FingerprintWidth::Twelve);
assert_eq!(cf.width(), FingerprintWidth::Twelve);
assert_eq!(cf.width().bits(), 12);
}
#[test]
fn delete_unknown_is_false() {
let mut cf = VariableFpCuckooFilter::new(100, FingerprintWidth::Sixteen);
assert!(!cf.delete("never-inserted"));
}
#[test]
fn bucket_count_is_power_of_two() {
let cf = VariableFpCuckooFilter::new(1000, FingerprintWidth::Twelve);
let n = cf.bucket_count();
assert!(n.is_power_of_two(), "{n} not power of two");
}
}