Skip to main content

yo_common/
wyhash.rs

1//! wyhash, the shard's key hash.
2//!
3//! Written here rather than pulled in. aki measured wyhash at 1.95 ns against
4//! fnv1a's 5.39 ns on the same keys (L13), and the whole index probe budget is
5//! 4 ns, so the hash is not a place to accept a dependency's version churn.
6//!
7//! This is wyhash final version 3 with the default secret, which is the variant
8//! every other implementation calls `wyhash` today. The test vectors at the
9//! bottom are the reference vectors from the C implementation, so a change that
10//! alters a hash value fails the build rather than silently repartitioning
11//! every existing `.yo` file.
12
13/// The default secret from the reference implementation.
14const SECRET: [u64; 4] = [
15    0x2d35_8dcc_aa6c_78a5,
16    0x8bb8_4b93_962e_acc9,
17    0x4b33_a62e_d433_d4a3,
18    0x4d5a_2da5_1de1_aa47,
19];
20
21/// 64x64 to 128 multiply, returning the low and high halves.
22#[inline(always)]
23fn mum(a: u64, b: u64) -> (u64, u64) {
24    let r = (a as u128).wrapping_mul(b as u128);
25    (r as u64, (r >> 64) as u64)
26}
27
28#[inline(always)]
29fn mix(a: u64, b: u64) -> u64 {
30    let (lo, hi) = mum(a, b);
31    lo ^ hi
32}
33
34#[inline(always)]
35fn r8(p: &[u8]) -> u64 {
36    u64::from_le_bytes([p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]])
37}
38
39#[inline(always)]
40fn r4(p: &[u8]) -> u64 {
41    u32::from_le_bytes([p[0], p[1], p[2], p[3]]) as u64
42}
43
44#[inline(always)]
45fn r3(p: &[u8], k: usize) -> u64 {
46    ((p[0] as u64) << 16) | ((p[k >> 1] as u64) << 8) | (p[k - 1] as u64)
47}
48
49/// Hash `key` with `seed`.
50///
51/// The shard uses seed 0. A seed is exposed because the bloom and hyperloglog
52/// types need independent hash families over the same bytes.
53#[inline]
54pub fn wyhash(key: &[u8], seed: u64) -> u64 {
55    let len = key.len();
56    let mut seed = seed ^ mix(seed ^ SECRET[0], SECRET[1]);
57    let a: u64;
58    let b: u64;
59
60    if len <= 16 {
61        if len >= 4 {
62            // Two overlapping 4 byte reads from each end. The `(len >> 3) << 2`
63            // term is 0 for 4..=7 and 4 for 8..=15, which is what makes the
64            // pairs overlap rather than repeat for the short lengths.
65            let q = (len >> 3) << 2;
66            a = (r4(key) << 32) | r4(&key[q..]);
67            b = (r4(&key[len - 4..]) << 32) | r4(&key[len - 4 - q..]);
68        } else if len > 0 {
69            a = r3(key, len);
70            b = 0;
71        } else {
72            a = 0;
73            b = 0;
74        }
75    } else {
76        // Offsets into `key` rather than a re-sliced cursor. The tail read below
77        // deliberately reaches back over bytes the loop already consumed, which
78        // a shrinking slice would make unrepresentable.
79        let mut off = 0usize;
80        let mut i = len;
81
82        if i >= 48 {
83            let mut see1 = seed;
84            let mut see2 = seed;
85            while i >= 48 {
86                seed = mix(r8(&key[off..]) ^ SECRET[1], r8(&key[off + 8..]) ^ seed);
87                see1 = mix(
88                    r8(&key[off + 16..]) ^ SECRET[2],
89                    r8(&key[off + 24..]) ^ see1,
90                );
91                see2 = mix(
92                    r8(&key[off + 32..]) ^ SECRET[3],
93                    r8(&key[off + 40..]) ^ see2,
94                );
95                off += 48;
96                i -= 48;
97            }
98            seed ^= see1 ^ see2;
99        }
100
101        while i > 16 {
102            seed = mix(r8(&key[off..]) ^ SECRET[1], r8(&key[off + 8..]) ^ seed);
103            off += 16;
104            i -= 16;
105        }
106
107        // off + i == len, so this is the last 16 bytes of the key.
108        a = r8(&key[len - 16..]);
109        b = r8(&key[len - 8..]);
110    }
111
112    let (a, b) = mum(a ^ SECRET[1], b ^ seed);
113    mix(a ^ SECRET[0] ^ (len as u64), b ^ SECRET[1])
114}
115
116/// Hash with the shard's seed.
117///
118/// Every key placement decision in the engine goes through this, so that a
119/// change of seed is one edit rather than a search.
120#[inline(always)]
121pub fn hash_key(key: &[u8]) -> u64 {
122    wyhash(key, 0)
123}
124
125/// The 8 high bits of a hash, as the index bucket stores them.
126///
127/// Zero means empty in a bucket, so a hash whose top byte is zero is given tag
128/// 1 instead. The collision cost of folding one value into another is one extra
129/// key comparison on 1 in 256 probes, and the alternative is a separate
130/// occupancy word and a second load.
131#[inline(always)]
132pub const fn tag_of(hash: u64) -> u8 {
133    let t = (hash >> 56) as u8;
134    if t == 0 { 1 } else { t }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    /// Appends `n` in decimal.
142    fn push_num(out: &mut Vec<u8>, n: u32) {
143        if n >= 10 {
144            push_num(out, n / 10);
145        }
146        out.push(b'0' + (n % 10) as u8);
147    }
148
149    /// Vectors produced by the reference C implementation.
150    ///
151    /// Generated by compiling upstream `wyhash.h` at WYHASH_CONDOM 1 with the
152    /// default secret and hashing a fixed byte pattern, `buf[i] = (i*7+13) % 251`,
153    /// at every length where the algorithm changes shape: the 4, 16, 48 and 16
154    /// again boundaries, and either side of each. Two seeds per length, because
155    /// the seed feeds both the prologue and the tail mix and a bug can hide in
156    /// one and not the other.
157    ///
158    /// These are a conformance lock, not a quality claim. If one of them moves,
159    /// every existing `.yo` file has been repartitioned and the change is not
160    /// shippable.
161    #[test]
162    fn reference_vectors() {
163        const VECTORS: &[(usize, u64, u64)] = &[
164            (0, 0, 0x93228a4de0eec5a2),
165            (0, 0xdeadbeefcafe1234, 0x503eb6b76bdddf6a),
166            (1, 0, 0xb743f05e0334871c),
167            (1, 0xdeadbeefcafe1234, 0xa6d34f7586585149),
168            (2, 0, 0x28b8761367a2c6fe),
169            (2, 0xdeadbeefcafe1234, 0xfb04ad55c47b05d5),
170            (3, 0, 0x51f03dcee96db0fa),
171            (3, 0xdeadbeefcafe1234, 0xb3b1e9b933f562dc),
172            (4, 0, 0x63e00718a6d4f4b7),
173            (4, 0xdeadbeefcafe1234, 0x4855ac52ab265d8f),
174            (5, 0, 0x9ef5dce95adebad0),
175            (5, 0xdeadbeefcafe1234, 0x8e3109951c14d448),
176            (7, 0, 0x78f6520add84ac5e),
177            (7, 0xdeadbeefcafe1234, 0x1d55d13730d8415c),
178            (8, 0, 0x98bbb3a2b3bba145),
179            (8, 0xdeadbeefcafe1234, 0x1d7d14601c86064f),
180            (9, 0, 0x8a4336709aa03e64),
181            (9, 0xdeadbeefcafe1234, 0xa6114ea701c654b9),
182            (15, 0, 0x398099ca2f2dfcd5),
183            (15, 0xdeadbeefcafe1234, 0x07400d0acd97aa2f),
184            (16, 0, 0x31692917ca3ab597),
185            (16, 0xdeadbeefcafe1234, 0xb8ff17cdb0d4b1a8),
186            (17, 0, 0x1cde6dc1d5cb836a),
187            (17, 0xdeadbeefcafe1234, 0x3c83b9dfecbe18a4),
188            (18, 0, 0x7d6c4fc7691eec86),
189            (18, 0xdeadbeefcafe1234, 0x2e41d8a4f547009b),
190            (31, 0, 0x559ac6703ac3d00e),
191            (31, 0xdeadbeefcafe1234, 0x7490b7565d08d3aa),
192            (32, 0, 0x869edcb7f8c6e88d),
193            (32, 0xdeadbeefcafe1234, 0xfde153bdc921f628),
194            (33, 0, 0x25a357dbecbab818),
195            (33, 0xdeadbeefcafe1234, 0x75d4db744c1ad719),
196            (47, 0, 0x26a8fa17ae56094e),
197            (47, 0xdeadbeefcafe1234, 0xc7abd5000339a9db),
198            (48, 0, 0xc7f0608db098eb0d),
199            (48, 0xdeadbeefcafe1234, 0x8b90c58340ad764c),
200            (49, 0, 0xd3f0a40ba8f9b1a6),
201            (49, 0xdeadbeefcafe1234, 0x132f911560ef3fa5),
202            (63, 0, 0x325a689473b1c1a5),
203            (63, 0xdeadbeefcafe1234, 0x6b7f55c7b078e75c),
204            (64, 0, 0x3d6436f595468fea),
205            (64, 0xdeadbeefcafe1234, 0x6421f0f5d6084af7),
206            (65, 0, 0x37d6f92314dcc53b),
207            (65, 0xdeadbeefcafe1234, 0xfcf7ea1b14bfc627),
208            (95, 0, 0x648820b80a4835b4),
209            (95, 0xdeadbeefcafe1234, 0xc379879b80991aae),
210            (96, 0, 0x31105c4a5a7062f0),
211            (96, 0xdeadbeefcafe1234, 0xb55bf2517b5725fb),
212            (97, 0, 0x8d28a022c26475ab),
213            (97, 0xdeadbeefcafe1234, 0xbf1d8e4618c274db),
214            (127, 0, 0xf58addc6ccbb43d7),
215            (127, 0xdeadbeefcafe1234, 0xca48f21107ab1ce0),
216            (128, 0, 0x2d766ba73db255df),
217            (128, 0xdeadbeefcafe1234, 0x913fedabb47d5cad),
218            (200, 0, 0xe11fd2a4ab654ef4),
219            (200, 0xdeadbeefcafe1234, 0x592bbccac660734e),
220            (255, 0, 0x54ad42c80e00a501),
221            (255, 0xdeadbeefcafe1234, 0x67f85b63c0948ebf),
222            (299, 0, 0x5ffcff99c0b5fa98),
223            (299, 0xdeadbeefcafe1234, 0x7aa342b1566d19f8),
224        ];
225
226        let mut buf = [0u8; 300];
227        for (i, b) in buf.iter_mut().enumerate() {
228            *b = ((i * 7 + 13) % 251) as u8;
229        }
230        for &(len, seed, want) in VECTORS {
231            assert_eq!(
232                wyhash(&buf[..len], seed),
233                want,
234                "length {len} seed {seed:#x} does not match the reference"
235            );
236        }
237    }
238
239    /// Every length from 0 to 200 must at least be reachable without panicking.
240    /// The index arithmetic around 16, 17, 48 and 49 is where the bounds get
241    /// interesting, and a panic there would be an availability bug.
242    #[test]
243    fn all_lengths_up_to_200() {
244        let buf: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
245        for n in 0..=200 {
246            let _ = wyhash(&buf[..n], 0);
247        }
248    }
249
250    #[test]
251    fn tag_is_never_zero() {
252        // A hash whose top byte is zero must still produce a usable tag.
253        assert_eq!(tag_of(0x00ff_ffff_ffff_ffff), 1);
254        assert_eq!(tag_of(0xab00_0000_0000_0000), 0xab);
255    }
256
257    /// Not a quality claim, just a smoke check that the low bits used for
258    /// bucket selection are not obviously degenerate for sequential keys, which
259    /// is the shape a benchmark loop produces.
260    #[test]
261    fn sequential_keys_spread_over_buckets() {
262        // What both assertions actually depend on is the ratio of keys to
263        // buckets, not the size of either. A hundred keys per bucket is what
264        // makes an empty bucket damning and three times the mean a real
265        // outlier, so Miri keeps the ratio and shrinks the table. Sixty four
266        // buckets and sixty four hundred keys is the same claim about the same
267        // low bits at a fifteenth of the interpreted work.
268        let (buckets, keys): (usize, u32) = if cfg!(miri) {
269            (64, 6_400)
270        } else {
271            (1024, 100_000)
272        };
273        let mut counts = vec![0u32; buckets];
274        let mut k = Vec::with_capacity(16);
275        for i in 0..keys {
276            // The key is built by hand into a buffer that gets reused rather
277            // than through `format!`. Same bytes, and under Miri, which charges
278            // per operation rather than per instruction, the formatting
279            // machinery was most of what this test cost.
280            k.clear();
281            k.extend_from_slice(b"key:");
282            push_num(&mut k, i);
283            counts[(hash_key(&k) as usize) & (buckets - 1)] += 1;
284        }
285        let max = *counts.iter().max().unwrap();
286        let mean = keys / buckets as u32;
287        assert!(max < mean * 3, "worst bucket {max} against mean {mean}");
288        assert!(counts.iter().all(|&c| c > 0), "some bucket got nothing");
289    }
290}