#[derive(Debug, Clone)]
pub struct FastHasher {
coeffs_a: Vec<u64>,
coeffs_b: Vec<u64>,
num_hashes: usize,
}
const MERSENNE_PRIME: u64 = (1_u64 << 61) - 1;
const PHI: u64 = 0x9e37_79b9_7f4a_7c15;
impl FastHasher {
pub fn new(num_hashes: usize, seed: u64) -> Self {
let num_hashes = num_hashes.min(crate::config::MAX_SIGNATURE_SIZE);
if num_hashes == 0 {
return Self {
coeffs_a: Vec::new(),
coeffs_b: Vec::new(),
num_hashes: 0,
};
}
let mut coeffs_a = Vec::with_capacity(num_hashes);
let mut coeffs_b = Vec::with_capacity(num_hashes);
let mut state = seed.wrapping_add(PHI);
for _ in 0..num_hashes {
state = splitmix64(state);
coeffs_a.push(state | 1);
state = splitmix64(state);
coeffs_b.push(state);
}
Self {
coeffs_a,
coeffs_b,
num_hashes,
}
}
#[allow(dead_code)]
pub fn hash_shingle(&self, shingle: u64) -> Vec<u32> {
let mut result = Vec::with_capacity(self.num_hashes);
for i in 0..self.num_hashes {
let hash = self.hash_single(shingle, i);
result.push(hash);
}
result
}
pub fn update_signature(&self, signature: &mut [u32], shingle: u64) {
const CHUNK_SIZE: usize = 8;
let limit = signature.len().min(self.num_hashes);
for chunk_start in (0..limit).step_by(CHUNK_SIZE) {
let chunk_end = (chunk_start + CHUNK_SIZE).min(limit);
for i in chunk_start..chunk_end {
let hash = self.hash_single(shingle, i);
if hash < signature[i] {
signature[i] = hash;
}
}
}
}
#[inline]
fn hash_single(&self, shingle: u64, idx: usize) -> u32 {
let a = self.coeffs_a[idx];
let b = self.coeffs_b[idx];
let product = u128::from(a).wrapping_mul(u128::from(shingle));
let sum = product.wrapping_add(u128::from(b));
let reduced = mod_mersenne(sum);
reduced as u32
}
#[allow(dead_code)]
pub fn update_signature_batch(&self, signature: &mut [u32], shingles: &[u64]) {
for shingle in shingles {
self.update_signature(signature, *shingle);
}
}
#[must_use]
pub const fn num_hashes(&self) -> usize {
self.num_hashes
}
}
#[inline]
fn mod_mersenne(mut x: u128) -> u64 {
const MASK: u128 = (1_u128 << 61) - 1;
let p = u128::from(MERSENNE_PRIME);
while (x >> 61) != 0 {
let low = x & MASK;
let high = x >> 61;
x = low + high;
}
if x >= p {
(x - p) as u64
} else {
x as u64
}
}
#[inline]
const fn splitmix64(state: u64) -> u64 {
let mut z = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
#[must_use]
pub fn hash_bytes(data: &[u8]) -> u64 {
const C1: u64 = 0x87c3_7b91_1142_53d5;
const C2: u64 = 0x4cf5_ad43_2745_937f;
const SEED: u64 = 0x9e37_79b9_7f4a_7c15;
let mut h = SEED;
let chunks = data.chunks_exact(8);
let remainder = chunks.remainder();
for chunk in chunks {
let mut k = u64::from_le_bytes([
chunk[0], chunk[1], chunk[2], chunk[3],
chunk[4], chunk[5], chunk[6], chunk[7],
]);
k = k.wrapping_mul(C1);
k = k.rotate_left(31);
k = k.wrapping_mul(C2);
h ^= k;
h = h.rotate_left(27);
h = h.wrapping_mul(5).wrapping_add(0x52ce_adbe_e7ef_7e45);
}
if !remainder.is_empty() {
let mut k = 0_u64;
for (i, &b) in remainder.iter().enumerate() {
k ^= u64::from(b) << (i * 8);
}
k = k.wrapping_mul(C1);
k = k.rotate_left(31);
k = k.wrapping_mul(C2);
h ^= k;
}
h ^= data.len() as u64;
h ^= h >> 33;
h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
h ^= h >> 33;
h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
h ^= h >> 33;
h
}
#[must_use]
#[allow(dead_code)]
pub fn hash_str(s: &str) -> u64 {
hash_bytes(s.as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hasher_creates_correct_size() {
let hasher = FastHasher::new(128, 42);
assert_eq!(hasher.num_hashes(), 128);
assert_eq!(hasher.coeffs_a.len(), 128);
assert_eq!(hasher.coeffs_b.len(), 128);
}
#[test]
fn new_clamps_hostile_num_hashes() {
let hasher = FastHasher::new(usize::MAX, 42);
assert_eq!(hasher.num_hashes(), crate::config::MAX_SIGNATURE_SIZE);
assert_eq!(hasher.coeffs_a.len(), crate::config::MAX_SIGNATURE_SIZE);
}
#[test]
fn hash_single_deterministic() {
let hasher = FastHasher::new(64, 12345);
let h1 = hasher.hash_single(0xdead_beef, 0);
let h2 = hasher.hash_single(0xdead_beef, 0);
assert_eq!(h1, h2);
}
#[test]
fn different_shingles_different_hashes() {
let hasher = FastHasher::new(64, 42);
let sig1 = hasher.hash_shingle(1);
let sig2 = hasher.hash_shingle(2);
assert_ne!(sig1, sig2);
}
#[test]
fn update_signature_works() {
let hasher = FastHasher::new(64, 42);
let mut sig = vec![u32::MAX; 64];
hasher.update_signature(&mut sig, 12345);
assert!(sig.iter().any(|&x| x < u32::MAX));
}
#[test]
fn signature_minimum_property() {
let hasher = FastHasher::new(32, 42);
let mut sig = vec![u32::MAX; 32];
hasher.update_signature(&mut sig, 100);
let first_sig = sig.clone();
hasher.update_signature(&mut sig, 200);
for i in 0..32 {
assert!(sig[i] <= first_sig[i]);
}
}
#[test]
fn hash_bytes_deterministic() {
let data = b"hello world";
let h1 = hash_bytes(data);
let h2 = hash_bytes(data);
assert_eq!(h1, h2);
}
#[test]
fn hash_bytes_different_input_different_output() {
let h1 = hash_bytes(b"hello");
let h2 = hash_bytes(b"world");
assert_ne!(h1, h2);
}
#[test]
fn hash_str_same_as_bytes() {
let s = "hello world";
assert_eq!(hash_str(s), hash_bytes(s.as_bytes()));
}
#[test]
fn splitmix64_produces_varied_output() {
let s1 = splitmix64(1);
let s2 = splitmix64(2);
assert_ne!(s1, s2);
}
#[test]
fn mod_mersenne_reduction() {
let p = u128::from(MERSENNE_PRIME);
assert_eq!(mod_mersenne(12345), 12345);
assert_eq!(mod_mersenne(p), 0);
assert_eq!(mod_mersenne(p + 1), 1);
}
#[test]
fn batch_update_matches_individual() {
let hasher = FastHasher::new(32, 42);
let shingles = vec![1_u64, 2, 3, 4, 5];
let mut sig_batch = vec![u32::MAX; 32];
hasher.update_signature_batch(&mut sig_batch, &shingles);
let mut sig_individual = vec![u32::MAX; 32];
for shingle in &shingles {
hasher.update_signature(&mut sig_individual, *shingle);
}
assert_eq!(sig_batch, sig_individual);
}
#[test]
fn hash_distribution_uniform() {
let hasher = FastHasher::new(64, 42);
let mut bins = [0_u32; 16];
for i in 0..10000 {
let hash = hasher.hash_single(i, 0);
let bin = (hash >> 28) as usize % 16; bins[bin] += 1;
}
let expected = 10000 / 16;
for count in &bins {
assert!(
*count >= expected / 2 && *count <= expected * 3 / 2,
"bin count {} is outside expected range around {}",
count,
expected
);
}
}
}