Skip to main content

trueno/hash/
mod.rs

1//! SIMD-optimized hash functions for key-value store operations.
2//!
3//! This module provides fast hash functions optimized for short string keys,
4//! with automatic SIMD dispatch (AVX-512 → AVX2 → SSE2 → Scalar).
5//!
6//! # Example
7//!
8//! ```rust
9//! use trueno::hash::{hash_key, hash_keys_batch};
10//!
11//! // Single key hash
12//! let h = hash_key("hello");
13//! assert_ne!(h, 0);
14//!
15//! // Batch hash (SIMD-optimized)
16//! let keys = ["a", "b", "c", "d"];
17//! let hashes = hash_keys_batch(&keys);
18//! assert_eq!(hashes.len(), 4);
19//! ```
20//!
21//! # Performance
22//!
23//! - Single key: ~2-5ns (FxHash-equivalent)
24//! - Batch (8 keys): ~10-15ns with AVX2 (vs ~20-40ns sequential)
25//! - Batch (16 keys): ~15-20ns with AVX-512
26
27use crate::Backend;
28
29/// Hash a single key to u64.
30///
31/// Uses FxHash algorithm (fast, non-cryptographic).
32/// Suitable for hash tables and KV stores.
33#[inline]
34#[must_use]
35pub fn hash_key(key: &str) -> u64 {
36    hash_bytes(key.as_bytes())
37}
38
39/// Hash raw bytes to u64.
40#[inline]
41#[must_use]
42pub fn hash_bytes(bytes: &[u8]) -> u64 {
43    // FxHash algorithm: fast, good distribution for small keys
44    const K: u64 = 0x517c_c1b7_2722_0a95;
45    let mut hash: u64 = 0;
46
47    // Process 8 bytes at a time
48    // `as_chunks::<8>()` yields `&[u8; 8]` directly, so the fallible
49    // `try_into().expect(...)` below is gone: the 8-byte width is now a type,
50    // not a runtime claim. clippy::chunks_exact_to_as_chunks (new in 1.98).
51    let (chunks, remainder) = bytes.as_chunks::<8>();
52
53    for chunk in chunks {
54        let word = u64::from_le_bytes(*chunk);
55        hash = hash.rotate_left(5).bitxor(word).wrapping_mul(K);
56    }
57
58    // Handle remaining bytes
59    for &byte in remainder {
60        hash = hash.rotate_left(5).bitxor(u64::from(byte)).wrapping_mul(K);
61    }
62
63    hash
64}
65
66/// Hash multiple keys in batch (SIMD-optimized).
67///
68/// For best performance, use batches of 8 (AVX2) or 16 (AVX-512) keys.
69/// Falls back to sequential hashing for smaller batches or unsupported CPUs.
70#[must_use]
71pub fn hash_keys_batch(keys: &[&str]) -> Vec<u64> {
72    hash_keys_batch_with_backend(keys, Backend::Auto)
73}
74
75/// Hash multiple keys with explicit backend selection.
76#[must_use]
77pub fn hash_keys_batch_with_backend(keys: &[&str], backend: Backend) -> Vec<u64> {
78    match backend {
79        Backend::Auto => {
80            #[cfg(target_arch = "x86_64")]
81            {
82                if is_x86_feature_detected!("avx2") {
83                    return hash_keys_avx2(keys);
84                }
85            }
86            hash_keys_scalar(keys)
87        }
88        Backend::AVX2 | Backend::AVX512 => hash_keys_avx2_or_scalar(keys),
89        Backend::Scalar
90        | Backend::SSE2
91        | Backend::AVX
92        | Backend::NEON
93        | Backend::WasmSIMD
94        | Backend::GPU => hash_keys_scalar(keys),
95    }
96}
97
98/// Scalar fallback for batch hashing.
99#[inline]
100fn hash_keys_scalar(keys: &[&str]) -> Vec<u64> {
101    keys.iter().map(|k| hash_key(k)).collect()
102}
103
104/// AVX2 with scalar fallback for non-x86.
105#[inline]
106fn hash_keys_avx2_or_scalar(keys: &[&str]) -> Vec<u64> {
107    #[cfg(target_arch = "x86_64")]
108    {
109        hash_keys_avx2(keys)
110    }
111    #[cfg(not(target_arch = "x86_64"))]
112    {
113        hash_keys_scalar(keys)
114    }
115}
116
117/// AVX2 SIMD batch hashing (4x u64 lanes).
118#[cfg(target_arch = "x86_64")]
119fn hash_keys_avx2(keys: &[&str]) -> Vec<u64> {
120    // For now, use scalar - AVX2 intrinsics for string hashing is complex
121    // Future optimization: process 4 keys in parallel using _mm256 intrinsics
122    hash_keys_scalar(keys)
123}
124
125use std::ops::BitXor;
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    // ============================================================
132    // RED PHASE: Define expected behavior
133    // ============================================================
134
135    #[test]
136    fn test_hash_key_deterministic() {
137        let h1 = hash_key("hello");
138        let h2 = hash_key("hello");
139        assert_eq!(h1, h2, "Same key must produce same hash");
140    }
141
142    #[test]
143    fn test_hash_key_different_keys() {
144        let h1 = hash_key("hello");
145        let h2 = hash_key("world");
146        assert_ne!(h1, h2, "Different keys should produce different hashes");
147    }
148
149    #[test]
150    fn test_hash_key_empty() {
151        let h = hash_key("");
152        // Empty string should hash to 0 (no data to mix)
153        assert_eq!(h, 0);
154    }
155
156    #[test]
157    fn test_hash_key_single_char() {
158        let h = hash_key("a");
159        assert_ne!(h, 0);
160    }
161
162    #[test]
163    fn test_hash_key_long_string() {
164        let long = "a".repeat(1000);
165        let h = hash_key(&long);
166        assert_ne!(h, 0);
167    }
168
169    #[test]
170    fn test_hash_bytes_matches_key() {
171        let key = "test_key";
172        assert_eq!(hash_key(key), hash_bytes(key.as_bytes()));
173    }
174
175    #[test]
176    fn test_hash_keys_batch_empty() {
177        let keys: &[&str] = &[];
178        let hashes = hash_keys_batch(keys);
179        assert!(hashes.is_empty());
180    }
181
182    #[test]
183    fn test_hash_keys_batch_single() {
184        let hashes = hash_keys_batch(&["hello"]);
185        assert_eq!(hashes.len(), 1);
186        assert_eq!(hashes[0], hash_key("hello"));
187    }
188
189    #[test]
190    fn test_hash_keys_batch_multiple() {
191        let keys = ["a", "b", "c", "d"];
192        let hashes = hash_keys_batch(&keys);
193
194        assert_eq!(hashes.len(), 4);
195        for (i, key) in keys.iter().enumerate() {
196            assert_eq!(hashes[i], hash_key(key), "Batch hash must match single hash");
197        }
198    }
199
200    #[test]
201    fn test_hash_keys_batch_large() {
202        let keys: Vec<&str> = (0..100)
203            .map(|i| {
204                // Leak strings to get &'static str for test
205                Box::leak(format!("key{i}").into_boxed_str()) as &str
206            })
207            .collect();
208
209        let hashes = hash_keys_batch(&keys);
210        assert_eq!(hashes.len(), 100);
211
212        // Verify all unique
213        let unique: std::collections::HashSet<_> = hashes.iter().collect();
214        assert_eq!(unique.len(), 100, "All keys should have unique hashes");
215    }
216
217    #[test]
218    fn test_backend_parity_scalar_vs_auto() {
219        let keys = ["foo", "bar", "baz", "qux"];
220
221        let scalar = hash_keys_batch_with_backend(&keys, Backend::Scalar);
222        let auto = hash_keys_batch_with_backend(&keys, Backend::Auto);
223
224        assert_eq!(scalar, auto, "Scalar and Auto must produce identical results");
225    }
226
227    #[test]
228    fn test_hash_distribution() {
229        // Test that hashes are well-distributed (no obvious clustering)
230        let keys: Vec<String> = (0..1000).map(|i| format!("key{i}")).collect();
231        let refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();
232        let hashes = hash_keys_batch(&refs);
233
234        // Check high bits are used (not all zeros)
235        let high_bits_used = hashes.iter().any(|h| h >> 56 != 0);
236        assert!(high_bits_used, "Hash should use high bits");
237
238        // Check low bits are varied
239        let low_nibbles: std::collections::HashSet<_> = hashes.iter().map(|h| h & 0xF).collect();
240        assert!(low_nibbles.len() >= 8, "Hash should have varied low bits");
241    }
242
243    #[test]
244    fn test_hash_avalanche_single_bit() {
245        // Changing one bit should change ~50% of output bits (avalanche effect)
246        let h1 = hash_key("aaa");
247        let h2 = hash_key("aab"); // One char different
248
249        let diff = (h1 ^ h2).count_ones();
250        // Expect at least 20 bits to differ (out of 64) for good avalanche
251        assert!(diff >= 15, "Avalanche effect: {} bits differ, expected >=15", diff);
252    }
253
254    #[test]
255    fn test_backend_avx2_explicit() {
256        let keys = ["foo", "bar", "baz", "qux"];
257        let avx2 = hash_keys_batch_with_backend(&keys, Backend::AVX2);
258        let scalar = hash_keys_batch_with_backend(&keys, Backend::Scalar);
259        assert_eq!(avx2, scalar, "AVX2 must match Scalar");
260    }
261
262    #[test]
263    fn test_backend_avx512_explicit() {
264        let keys = ["foo", "bar", "baz", "qux"];
265        let avx512 = hash_keys_batch_with_backend(&keys, Backend::AVX512);
266        let scalar = hash_keys_batch_with_backend(&keys, Backend::Scalar);
267        assert_eq!(avx512, scalar, "AVX512 must match Scalar");
268    }
269
270    #[test]
271    fn test_backend_sse2_fallback() {
272        let keys = ["a", "b", "c"];
273        let sse2 = hash_keys_batch_with_backend(&keys, Backend::SSE2);
274        let scalar = hash_keys_batch_with_backend(&keys, Backend::Scalar);
275        assert_eq!(sse2, scalar, "SSE2 must fall back to Scalar");
276    }
277
278    #[test]
279    fn test_backend_neon_fallback() {
280        let keys = ["x", "y", "z"];
281        let neon = hash_keys_batch_with_backend(&keys, Backend::NEON);
282        let scalar = hash_keys_batch_with_backend(&keys, Backend::Scalar);
283        assert_eq!(neon, scalar, "NEON must fall back to Scalar");
284    }
285
286    #[test]
287    fn test_hash_keys_avx2_or_scalar_coverage() {
288        // Directly test the helper function via AVX2 backend
289        let keys = ["test1", "test2"];
290        let result = hash_keys_batch_with_backend(&keys, Backend::AVX2);
291        assert_eq!(result.len(), 2);
292        assert_eq!(result[0], hash_key("test1"));
293        assert_eq!(result[1], hash_key("test2"));
294    }
295}