use std::hash::{Hash, Hasher};
use std::sync::Arc;
#[derive(Default)]
struct FxBloomHasher(u64);
const FX_SEED: u64 = 0xCBF2_9CE4_8422_2325;
const FX_MULT: u64 = 0x517C_C1B7_2722_0A95;
impl FxBloomHasher {
fn with_seed(seed: u64) -> Self {
Self(seed ^ FX_SEED)
}
}
impl Hasher for FxBloomHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
let mut chunks = bytes.chunks_exact(8);
for c in &mut chunks {
let n = u64::from_le_bytes([
c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7],
]);
self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(FX_MULT);
}
for &b in chunks.remainder() {
self.0 = (self.0.rotate_left(5) ^ b as u64).wrapping_mul(FX_MULT);
}
}
#[inline]
fn write_u64(&mut self, n: u64) {
self.0 = (self.0.rotate_left(5) ^ n).wrapping_mul(FX_MULT);
}
#[inline]
fn write_u32(&mut self, n: u32) { self.write_u64(n as u64); }
#[inline]
fn write_u16(&mut self, n: u16) { self.write_u64(n as u64); }
#[inline]
fn write_u8(&mut self, n: u8) { self.write_u64(n as u64); }
#[inline]
fn write_i64(&mut self, n: i64) { self.write_u64(n as u64); }
#[inline]
fn write_i32(&mut self, n: i32) { self.write_u64(n as u64); }
#[inline]
fn write_isize(&mut self, n: isize) { self.write_u64(n as u64); }
#[inline]
fn write_usize(&mut self, n: usize) { self.write_u64(n as u64); }
#[inline]
fn finish(&self) -> u64 { self.0 }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Bloom64(pub u64);
impl Bloom64 {
pub const ZERO: Self = Self(0);
pub const SUGGESTED_CAPACITY: usize = 8;
#[inline]
pub(crate) fn fast_hash<K: Hash + ?Sized>(key: &K, seed: u64) -> u64 {
let mut h = FxBloomHasher::with_seed(seed);
key.hash(&mut h);
h.finish()
}
#[inline]
fn indices<K: Hash + ?Sized>(key: &K) -> [u8; 4] {
let h = Self::fast_hash(key, 0x9E37_79B9_7F4A_7C15);
[
(h & 0x3F) as u8,
((h >> 16) & 0x3F) as u8,
((h >> 32) & 0x3F) as u8,
((h >> 48) & 0x3F) as u8,
]
}
pub fn insert<K: Hash + ?Sized>(&mut self, key: &K) {
for bit in Self::indices(key) {
self.0 |= 1u64 << bit;
}
}
pub fn might_contain<K: Hash + ?Sized>(&self, key: &K) -> bool {
let bits = Self::indices(key);
for bit in bits {
if (self.0 >> bit) & 1 == 0 {
return false;
}
}
true
}
pub fn from_keys<'a, K, I>(keys: I) -> Self
where K: Hash + 'a, I: IntoIterator<Item = &'a K>,
{
let mut b = Self::ZERO;
for k in keys { b.insert(k); }
b
}
pub fn popcount(&self) -> u32 { self.0.count_ones() }
pub fn estimated_fpr(n: usize) -> f64 {
let m = 64.0;
let k = 4.0;
let p_zero = (-k * n as f64 / m).exp();
(1.0 - p_zero).powf(k)
}
}
#[derive(Debug, Clone)]
pub struct BloomPointer<T> {
bloom: Bloom64,
target: Arc<T>,
}
impl<T> BloomPointer<T> {
pub const SIGNATURE: subetha_core::AxisMask = subetha_core::AxisMask::from_axes(
&[subetha_core::Axis::ContentPrefix],
);
pub fn new(target: Arc<T>, bloom: Bloom64) -> Self {
Self { bloom, target }
}
pub fn from_keys<K, I>(target: Arc<T>, keys: I) -> Self
where K: Hash, I: IntoIterator<Item = K>,
{
let mut b = Bloom64::ZERO;
for k in keys { b.insert(&k); }
Self { bloom: b, target }
}
#[inline]
pub fn bloom(&self) -> Bloom64 { self.bloom }
#[inline]
pub fn target(&self) -> &Arc<T> { &self.target }
#[inline]
pub fn might_contain<K: Hash + ?Sized>(&self, key: &K) -> bool {
self.bloom.might_contain(key)
}
pub fn set_bloom(&mut self, b: Bloom64) { self.bloom = b; }
}
#[derive(Debug, Clone)]
pub struct BloomCascade<T> {
coarse: Bloom64,
fine: BloomFine,
target: Arc<T>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BloomFine {
bits: [u64; 4],
}
impl BloomFine {
pub const ZERO: Self = Self { bits: [0; 4] };
pub const SUGGESTED_CAPACITY: usize = 64;
#[inline]
fn indices<K: Hash + ?Sized>(key: &K) -> [u8; 8] {
let h1 = Bloom64::fast_hash(key, 0x9E37_79B9_7F4A_7C15);
let h2 = Bloom64::fast_hash(key, 0xBB67_AE85_84CA_A73B);
[
(h1 & 0xFF) as u8,
((h1 >> 16) & 0xFF) as u8,
((h1 >> 32) & 0xFF) as u8,
((h1 >> 48) & 0xFF) as u8,
(h2 & 0xFF) as u8,
((h2 >> 16) & 0xFF) as u8,
((h2 >> 32) & 0xFF) as u8,
((h2 >> 48) & 0xFF) as u8,
]
}
pub fn insert<K: Hash + ?Sized>(&mut self, key: &K) {
for bit in Self::indices(key) {
self.bits[(bit / 64) as usize] |= 1u64 << (bit % 64);
}
}
pub fn might_contain<K: Hash + ?Sized>(&self, key: &K) -> bool {
for bit in Self::indices(key) {
let word = self.bits[(bit / 64) as usize];
if (word >> (bit % 64)) & 1 == 0 { return false; }
}
true
}
pub fn from_keys<'a, K, I>(keys: I) -> Self
where K: Hash + 'a, I: IntoIterator<Item = &'a K>,
{
let mut b = Self::ZERO;
for k in keys { b.insert(k); }
b
}
pub fn popcount(&self) -> u32 {
self.bits.iter().map(|w| w.count_ones()).sum()
}
}
impl<T> BloomCascade<T> {
pub fn new(target: Arc<T>, coarse: Bloom64, fine: BloomFine) -> Self {
Self { coarse, fine, target }
}
pub fn from_keys<K, I>(target: Arc<T>, keys: I) -> Self
where K: Hash, I: IntoIterator<Item = K>,
{
let mut coarse = Bloom64::ZERO;
let mut fine = BloomFine::ZERO;
for k in keys {
coarse.insert(&k);
fine.insert(&k);
}
Self { coarse, fine, target }
}
pub fn target(&self) -> &Arc<T> { &self.target }
pub fn coarse(&self) -> Bloom64 { self.coarse }
pub fn fine(&self) -> &BloomFine { &self.fine }
pub fn cascade_check<K: Hash + ?Sized>(&self, key: &K) -> CascadeOutcome {
if !self.coarse.might_contain(key) {
return CascadeOutcome::RejectedAtCoarse;
}
if !self.fine.might_contain(key) {
return CascadeOutcome::RejectedAtFine;
}
CascadeOutcome::MightContain
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CascadeOutcome {
RejectedAtCoarse,
RejectedAtFine,
MightContain,
}
impl CascadeOutcome {
pub fn might_contain(self) -> bool { matches!(self, Self::MightContain) }
pub fn rejected(self) -> bool { !self.might_contain() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bloom64_insert_then_query() {
let mut b = Bloom64::ZERO;
b.insert(&42u64);
b.insert(&"hello");
assert!(b.might_contain(&42u64));
assert!(b.might_contain(&"hello"));
let mut rejects = 0;
for k in 1000..1100u64 {
if !b.might_contain(&k) { rejects += 1; }
}
assert!(rejects > 80,
"fresh Bloom64 with 2 entries should reject most random keys; got {rejects}/100");
}
#[test]
fn bloom64_no_false_negative() {
let mut b = Bloom64::ZERO;
for k in 0..16u64 { b.insert(&k); }
for k in 0..16u64 {
assert!(b.might_contain(&k),
"Bloom must never give false negative; missed {k}");
}
}
#[test]
fn bloom_pointer_basic_usage() {
let target = Arc::new(vec![1u64, 2, 3, 4, 5]);
let keys: Vec<u64> = target.iter().copied().collect();
let bp = BloomPointer::from_keys(target.clone(), keys);
for k in 1..=5u64 {
assert!(bp.might_contain(&k));
}
let mut rejects = 0;
for k in 100..200u64 {
if !bp.might_contain(&k) { rejects += 1; }
}
assert!(rejects > 80,
"BloomPointer with 5 entries should reject most random; got {rejects}/100");
}
#[test]
fn bloom_pointer_size_is_16_bytes() {
assert_eq!(std::mem::size_of::<BloomPointer<u64>>(), 16);
}
#[test]
fn bloom_fine_holds_more_keys_than_coarse() {
let mut coarse = Bloom64::ZERO;
let mut fine = BloomFine::ZERO;
for k in 0..32u64 {
coarse.insert(&k);
fine.insert(&k);
}
let mut coarse_rejects = 0;
let mut fine_rejects = 0;
for k in 1000..1100u64 {
if !coarse.might_contain(&k) { coarse_rejects += 1; }
if !fine.might_contain(&k) { fine_rejects += 1; }
}
assert!(fine_rejects >= coarse_rejects,
"fine filter must reject at least as much as coarse: \
coarse={coarse_rejects} fine={fine_rejects}");
}
#[test]
fn bloom_cascade_layered_rejection() {
let target: Arc<Vec<u64>> = Arc::new((0..32u64).collect());
let keys: Vec<u64> = target.iter().copied().collect();
let bc = BloomCascade::from_keys(target.clone(), keys);
for k in 0..32u64 {
assert!(bc.cascade_check(&k).might_contain(),
"inserted key {k} must not be rejected");
}
let mut coarse_rej = 0;
let mut fine_rej = 0;
let mut survive = 0;
for k in 1000..1100u64 {
match bc.cascade_check(&k) {
CascadeOutcome::RejectedAtCoarse => coarse_rej += 1,
CascadeOutcome::RejectedAtFine => fine_rej += 1,
CascadeOutcome::MightContain => survive += 1,
}
}
assert!(coarse_rej + fine_rej >= 90,
"cascade should reject most random queries; \
coarse_rej={coarse_rej} fine_rej={fine_rej} survive={survive}");
}
#[test]
fn estimated_fpr_grows_with_load() {
let fpr1 = Bloom64::estimated_fpr(1);
let fpr8 = Bloom64::estimated_fpr(8);
let fpr16 = Bloom64::estimated_fpr(16);
let fpr32 = Bloom64::estimated_fpr(32);
assert!(fpr1 < fpr8);
assert!(fpr8 < fpr16);
assert!(fpr16 < fpr32);
let fpr8_actual = Bloom64::estimated_fpr(8);
assert!(fpr8_actual < 0.05,
"8-key FPR should be < 5%, got {fpr8_actual}");
assert!(fpr16 > 0.10 && fpr16 < 0.25,
"16-key FPR should be in [10%, 25%], got {fpr16}");
}
#[test]
fn bloom_cascade_outer_inner_information() {
let mut coarse = Bloom64::ZERO;
let mut fine = BloomFine::ZERO;
for k in 0..10u64 {
coarse.insert(&k);
fine.insert(&k);
}
for k in 100..200u64 {
coarse.insert(&k);
}
let bc = BloomCascade {
coarse, fine,
target: Arc::new(()),
};
let outcome = bc.cascade_check(&150u64);
assert_ne!(outcome, CascadeOutcome::RejectedAtCoarse,
"150 was inserted into coarse so must pass coarse");
}
}