#[inline(always)]
pub(crate) fn wyhash_pair(a: u8, b: u8) -> u64 {
let x = (u64::from(a) << 8) | u64::from(b);
let x = x.wrapping_mul(0x9E37_79B9_7F4A_7C15);
x ^ (x >> 32)
}
#[inline(always)]
pub(crate) fn derive_second_hash(h1: u64) -> u64 {
let h2 = h1 ^ (h1 >> 32);
h2.max(1)
}
#[inline(always)]
pub(crate) fn hash_pair(a: u8, b: u8) -> (u64, u64) {
let h1 = wyhash_pair(a, b);
let h2 = derive_second_hash(h1);
(h1, h2)
}
#[inline(always)]
#[allow(clippy::cast_possible_truncation)]
pub(crate) fn hash_to_index(hash: u64, num_bits: usize) -> usize {
debug_assert!(
(num_bits as u64).is_power_of_two(),
"hash_to_index: num_bits must be a power of two, got {num_bits}"
);
(hash & ((num_bits as u64).wrapping_sub(1))) as usize
}
#[cfg(test)]
#[allow(
clippy::cast_possible_truncation,
clippy::identity_op,
clippy::items_after_statements,
clippy::manual_range_contains,
clippy::uninlined_format_args,
clippy::unreadable_literal
)]
mod tests {
use super::*;
#[test]
fn hash_pair_consistent() {
let (h1_a, h2_a) = hash_pair(0x41, 0x42);
let (h1_b, h2_b) = hash_pair(0x41, 0x42);
assert_eq!(h1_a, h1_b);
assert_eq!(h2_a, h2_b);
}
#[test]
fn hash_pair_distinct_for_different_inputs() {
let (h1_ab, _) = hash_pair(0x41, 0x42);
let (h1_ba, _) = hash_pair(0x42, 0x41);
assert_ne!(h1_ab, h1_ba, "hash collision for reversed bytes");
}
#[test]
fn second_hash_never_zero() {
for a in 0_u8..=255 {
for b in 0_u8..=255 {
let (_, h2) = hash_pair(a, b);
assert_ne!(h2, 0, "h2 was zero for ({a}, {b})");
}
}
}
#[test]
fn second_hash_derived_from_first() {
for a in 0_u8..=255 {
for b in 0_u8..=255 {
let h1 = wyhash_pair(a, b);
let h2 = derive_second_hash(h1);
let expected = h1 ^ (h1 >> 32);
let expected = if expected == 0 { 1 } else { expected };
assert_eq!(h2, expected, "h2 derivation mismatch for ({a}, {b})");
}
}
}
#[test]
fn wyhash_fewer_collisions_than_fnv1a() {
use std::collections::HashSet;
let mut pairs = Vec::with_capacity(10_000);
let mut state: u64 = 0x1234_5678_9ABC_DEF0;
for _ in 0..10_000 {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let a = ((state >> 8) & 0xFF) as u8;
let b = ((state >> 24) & 0xFF) as u8;
pairs.push((a, b));
}
let mut wyhash_set = HashSet::new();
let mut wyhash_collisions = 0_usize;
for (a, b) in &pairs {
let h = wyhash_pair(*a, *b);
if !wyhash_set.insert(h) {
wyhash_collisions += 1;
}
}
fn fnv1a_pair_64(a: u8, b: u8) -> u64 {
const FNV_OFFSET_BASIS_64: u64 = 0xCBF2_9CE4_8422_2325;
const FNV_PRIME_64: u64 = 0x0000_0100_0000_01B3;
let mut hash = FNV_OFFSET_BASIS_64;
hash ^= u64::from(a);
hash = hash.wrapping_mul(FNV_PRIME_64);
hash ^= u64::from(b);
hash.wrapping_mul(FNV_PRIME_64)
}
let mut fnv_set = HashSet::new();
let mut fnv_collisions = 0_usize;
for (a, b) in &pairs {
let h = fnv1a_pair_64(*a, *b);
if !fnv_set.insert(h) {
fnv_collisions += 1;
}
}
assert!(
wyhash_collisions <= fnv_collisions,
"wyhash had more collisions ({}) than FNV-1a ({})",
wyhash_collisions,
fnv_collisions
);
}
#[test]
fn wyhash_avalanche() {
let base = wyhash_pair(0x00, 0x00);
let mut total_bit_diffs = 0_usize;
let mut count = 0_usize;
for i in 0..8 {
let flipped = wyhash_pair(0x00 | (1 << i), 0x00);
let diff = base ^ flipped;
total_bit_diffs += diff.count_ones() as usize;
count += 1;
let flipped = wyhash_pair(0x00, 0x00 | (1 << i));
let diff = base ^ flipped;
total_bit_diffs += diff.count_ones() as usize;
count += 1;
}
let avg_bits_changed = total_bit_diffs as f64 / count as f64;
assert!(
avg_bits_changed >= 16.0 && avg_bits_changed <= 48.0,
"avalanche test failed: average {} bits changed (expected ~32)",
avg_bits_changed
);
}
}