Skip to main content

dedup/
fast_hash.rs

1//! Fast hash computation for MinHash signatures.
2//!
3//! Provides efficient hash functions for computing MinHash signatures.
4//! Uses 128-bit arithmetic for modular reduction to avoid overflow
5//! and provides good performance for shingle hashing.
6
7/// A fast hasher for computing multiple hash values efficiently.
8///
9/// This struct uses a family of permutation hash functions suitable for MinHash.
10/// The hash functions have the form: `h_i(x) = (a_i * x + b_i) mod p`
11/// where `p` is a large prime and `a_i`, `b_i` are random odd coefficients.
12#[derive(Debug, Clone)]
13pub struct FastHasher {
14    /// Coefficients 'a' for linear hash functions.
15    coeffs_a: Vec<u64>,
16    /// Coefficients 'b' for linear hash functions.
17    coeffs_b: Vec<u64>,
18    /// Number of hash functions.
19    num_hashes: usize,
20}
21
22// Large Mersenne prime: 2^61 - 1
23const MERSENNE_PRIME: u64 = (1_u64 << 61) - 1;
24
25// Golden ratio constant for hash mixing
26const PHI: u64 = 0x9e37_79b9_7f4a_7c15;
27
28impl FastHasher {
29    /// Create a new hasher with the given number of hash functions.
30    ///
31    /// A count of 0 returns a no-op hasher with empty coefficients. Counts
32    /// above [`crate::config::MAX_SIGNATURE_SIZE`] are clamped to it: the
33    /// coefficients are two `u64` vectors of this length, so an unbounded
34    /// count (for example `usize::MAX` from a hostile or mistaken caller)
35    /// would abort the process on allocation failure.
36    pub fn new(num_hashes: usize, seed: u64) -> Self {
37        let num_hashes = num_hashes.min(crate::config::MAX_SIGNATURE_SIZE);
38        if num_hashes == 0 {
39            return Self {
40                coeffs_a: Vec::new(),
41                coeffs_b: Vec::new(),
42                num_hashes: 0,
43            };
44        }
45
46        let mut coeffs_a = Vec::with_capacity(num_hashes);
47        let mut coeffs_b = Vec::with_capacity(num_hashes);
48
49        // Generate pseudo-random coefficients using splitmix64
50        let mut state = seed.wrapping_add(PHI);
51        for _ in 0..num_hashes {
52            state = splitmix64(state);
53            // Ensure 'a' is odd and non-zero for good mixing
54            coeffs_a.push(state | 1);
55            state = splitmix64(state);
56            coeffs_b.push(state);
57        }
58
59        Self {
60            coeffs_a,
61            coeffs_b,
62            num_hashes,
63        }
64    }
65
66    /// Compute MinHash signature for a single shingle value.
67    ///
68    /// Returns a vector of hash values, one per hash function.
69    /// For MinHash, we take the minimum across all shingles.
70    #[allow(dead_code)]
71    pub fn hash_shingle(&self, shingle: u64) -> Vec<u32> {
72        let mut result = Vec::with_capacity(self.num_hashes);
73
74        for i in 0..self.num_hashes {
75            let hash = self.hash_single(shingle, i);
76            result.push(hash);
77        }
78
79        result
80    }
81
82    /// Update a signature in-place with a new shingle using MINimum update.
83    ///
84    /// For each hash function, updates `signature[i] = min(signature[i], hash_i(shingle))`.
85    pub fn update_signature(&self, signature: &mut [u32], shingle: u64) {
86        const CHUNK_SIZE: usize = 8;
87
88        // Bound the iteration to the shorter of signature and num_hashes
89        // to prevent OOB if caller passes a shorter slice.
90        let limit = signature.len().min(self.num_hashes);
91
92        // Process in chunks for better cache locality
93        for chunk_start in (0..limit).step_by(CHUNK_SIZE) {
94            let chunk_end = (chunk_start + CHUNK_SIZE).min(limit);
95
96            for i in chunk_start..chunk_end {
97                let hash = self.hash_single(shingle, i);
98                if hash < signature[i] {
99                    signature[i] = hash;
100                }
101            }
102        }
103    }
104
105    /// Compute hash for a single function index.
106    #[inline]
107    fn hash_single(&self, shingle: u64, idx: usize) -> u32 {
108        // h(x) = (a * x + b) mod p
109        // Use 128-bit arithmetic to avoid overflow, then reduce mod p
110        let a = self.coeffs_a[idx];
111        let b = self.coeffs_b[idx];
112
113        let product = u128::from(a).wrapping_mul(u128::from(shingle));
114        let sum = product.wrapping_add(u128::from(b));
115
116        // Reduce modulo Mersenne prime: x mod (2^61 - 1)
117        let reduced = mod_mersenne(sum);
118
119        // Convert to u32 for the signature
120        reduced as u32
121    }
122
123    /// Batch hash multiple shingles and update signature.
124    ///
125    /// This is more cache-efficient than calling `update_signature` repeatedly.
126    #[allow(dead_code)]
127    pub fn update_signature_batch(&self, signature: &mut [u32], shingles: &[u64]) {
128        for shingle in shingles {
129            self.update_signature(signature, *shingle);
130        }
131    }
132
133    /// Get the number of hash functions.
134    #[must_use]
135    pub const fn num_hashes(&self) -> usize {
136        self.num_hashes
137    }
138}
139
140/// Reduce a 128-bit value modulo the Mersenne prime 2^61 - 1.
141/// Uses iterative reduction to ensure correctness for arbitrary 128-bit inputs.
142#[inline]
143fn mod_mersenne(mut x: u128) -> u64 {
144    // For p = 2^61 - 1 we can reduce by folding the high bits into the low
145    // bits: x -> (x_low + (x >> 61)). For large 128-bit values this may
146    // need to be repeated until the value fits in 61 bits.
147    const MASK: u128 = (1_u128 << 61) - 1;
148    let p = u128::from(MERSENNE_PRIME);
149
150    // Iteratively fold high bits until no bits remain above 61
151    while (x >> 61) != 0 {
152        let low = x & MASK;
153        let high = x >> 61;
154        x = low + high;
155    }
156
157    // One final correction
158    if x >= p {
159        (x - p) as u64
160    } else {
161        x as u64
162    }
163}
164
165/// SplitMix64 pseudo-random number generator.
166#[inline]
167const fn splitmix64(state: u64) -> u64 {
168    let mut z = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
169    z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
170    z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
171    z ^ (z >> 31)
172}
173
174/// Compute a fast, non-cryptographic hash of a byte slice.
175///
176/// Uses the 64-bit variant of MurmurHash3 for good distribution.
177#[must_use]
178pub fn hash_bytes(data: &[u8]) -> u64 {
179    const C1: u64 = 0x87c3_7b91_1142_53d5;
180    const C2: u64 = 0x4cf5_ad43_2745_937f;
181    const SEED: u64 = 0x9e37_79b9_7f4a_7c15;
182
183    let mut h = SEED;
184
185    // Process 8 bytes at a time
186    let chunks = data.chunks_exact(8);
187    let remainder = chunks.remainder();
188
189    for chunk in chunks {
190        let mut k = u64::from_le_bytes([
191            chunk[0], chunk[1], chunk[2], chunk[3],
192            chunk[4], chunk[5], chunk[6], chunk[7],
193        ]);
194        k = k.wrapping_mul(C1);
195        k = k.rotate_left(31);
196        k = k.wrapping_mul(C2);
197
198        h ^= k;
199        h = h.rotate_left(27);
200        h = h.wrapping_mul(5).wrapping_add(0x52ce_adbe_e7ef_7e45);
201    }
202
203    // Process remaining bytes
204    if !remainder.is_empty() {
205        let mut k = 0_u64;
206        for (i, &b) in remainder.iter().enumerate() {
207            k ^= u64::from(b) << (i * 8);
208        }
209        k = k.wrapping_mul(C1);
210        k = k.rotate_left(31);
211        k = k.wrapping_mul(C2);
212        h ^= k;
213    }
214
215    // Finalization
216    h ^= data.len() as u64;
217    h ^= h >> 33;
218    h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
219    h ^= h >> 33;
220    h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
221    h ^= h >> 33;
222
223    h
224}
225
226/// Compute hash of a string slice.
227#[must_use]
228#[allow(dead_code)]
229pub fn hash_str(s: &str) -> u64 {
230    hash_bytes(s.as_bytes())
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn hasher_creates_correct_size() {
239        let hasher = FastHasher::new(128, 42);
240        assert_eq!(hasher.num_hashes(), 128);
241        assert_eq!(hasher.coeffs_a.len(), 128);
242        assert_eq!(hasher.coeffs_b.len(), 128);
243    }
244
245    /// Regression: `FastHasher::new` allocated its coefficient vectors with
246    /// the caller-supplied count, so `FastHasher::new(usize::MAX, seed)` died
247    /// on a capacity-overflow panic / allocation abort. The count is now
248    /// clamped to `MAX_SIGNATURE_SIZE`, keeping construction total for any
249    /// input while leaving realistic counts untouched.
250    #[test]
251    fn new_clamps_hostile_num_hashes() {
252        let hasher = FastHasher::new(usize::MAX, 42);
253        assert_eq!(hasher.num_hashes(), crate::config::MAX_SIGNATURE_SIZE);
254        assert_eq!(hasher.coeffs_a.len(), crate::config::MAX_SIGNATURE_SIZE);
255    }
256
257    #[test]
258    fn hash_single_deterministic() {
259        let hasher = FastHasher::new(64, 12345);
260        let h1 = hasher.hash_single(0xdead_beef, 0);
261        let h2 = hasher.hash_single(0xdead_beef, 0);
262        assert_eq!(h1, h2);
263    }
264
265    #[test]
266    fn different_shingles_different_hashes() {
267        let hasher = FastHasher::new(64, 42);
268        let sig1 = hasher.hash_shingle(1);
269        let sig2 = hasher.hash_shingle(2);
270        
271        // Very unlikely to be identical across all hash functions
272        assert_ne!(sig1, sig2);
273    }
274
275    #[test]
276    fn update_signature_works() {
277        let hasher = FastHasher::new(64, 42);
278        let mut sig = vec![u32::MAX; 64];
279        
280        hasher.update_signature(&mut sig, 12345);
281        
282        // Signature should have been updated (minimized)
283        assert!(sig.iter().any(|&x| x < u32::MAX));
284    }
285
286    #[test]
287    fn signature_minimum_property() {
288        let hasher = FastHasher::new(32, 42);
289        let mut sig = vec![u32::MAX; 32];
290        
291        // First shingle sets initial values
292        hasher.update_signature(&mut sig, 100);
293        let first_sig = sig.clone();
294        
295        // Second shingle can only decrease values
296        hasher.update_signature(&mut sig, 200);
297        
298        for i in 0..32 {
299            assert!(sig[i] <= first_sig[i]);
300        }
301    }
302
303    #[test]
304    fn hash_bytes_deterministic() {
305        let data = b"hello world";
306        let h1 = hash_bytes(data);
307        let h2 = hash_bytes(data);
308        assert_eq!(h1, h2);
309    }
310
311    #[test]
312    fn hash_bytes_different_input_different_output() {
313        let h1 = hash_bytes(b"hello");
314        let h2 = hash_bytes(b"world");
315        assert_ne!(h1, h2);
316    }
317
318    #[test]
319    fn hash_str_same_as_bytes() {
320        let s = "hello world";
321        assert_eq!(hash_str(s), hash_bytes(s.as_bytes()));
322    }
323
324    #[test]
325    fn splitmix64_produces_varied_output() {
326        let s1 = splitmix64(1);
327        let s2 = splitmix64(2);
328        assert_ne!(s1, s2);
329    }
330
331    #[test]
332    fn mod_mersenne_reduction() {
333        // Test that mod_mersenne works correctly
334        let p = u128::from(MERSENNE_PRIME);
335        
336        // x mod p should be x for x < p
337        assert_eq!(mod_mersenne(12345), 12345);
338        
339        // p mod p should be 0
340        assert_eq!(mod_mersenne(p), 0);
341        
342        // (p + 1) mod p should be 1
343        assert_eq!(mod_mersenne(p + 1), 1);
344    }
345
346    #[test]
347    fn batch_update_matches_individual() {
348        let hasher = FastHasher::new(32, 42);
349        let shingles = vec![1_u64, 2, 3, 4, 5];
350        
351        let mut sig_batch = vec![u32::MAX; 32];
352        hasher.update_signature_batch(&mut sig_batch, &shingles);
353        
354        let mut sig_individual = vec![u32::MAX; 32];
355        for shingle in &shingles {
356            hasher.update_signature(&mut sig_individual, *shingle);
357        }
358        
359        assert_eq!(sig_batch, sig_individual);
360    }
361
362    #[test]
363    fn hash_distribution_uniform() {
364        // Test that hash values are reasonably distributed
365        let hasher = FastHasher::new(64, 42);
366        let mut bins = [0_u32; 16];
367        
368        for i in 0..10000 {
369            let hash = hasher.hash_single(i, 0);
370            let bin = (hash >> 28) as usize % 16; // Use high bits
371            bins[bin] += 1;
372        }
373        
374        // Each bin should have roughly 10000/16 = 625 items
375        // Allow 50% variance for statistical fluctuation
376        let expected = 10000 / 16;
377        for count in &bins {
378            assert!(
379                *count >= expected / 2 && *count <= expected * 3 / 2,
380                "bin count {} is outside expected range around {}",
381                count,
382                expected
383            );
384        }
385    }
386}