Skip to main content

polars_utils/
hashing.rs

1use std::hash::{Hash, Hasher};
2
3use crate::nulls::IsNull;
4
5// Hash combine from c++' boost lib.
6#[inline(always)]
7pub fn _boost_hash_combine(l: u64, r: u64) -> u64 {
8    l ^ r.wrapping_add(0x9e3779b9u64.wrapping_add(l << 6).wrapping_add(r >> 2))
9}
10
11#[inline(always)]
12pub const fn folded_multiply(a: u64, b: u64) -> u64 {
13    let full = (a as u128).wrapping_mul(b as u128);
14    (full as u64) ^ ((full >> 64) as u64)
15}
16
17/// Contains a byte slice and a precomputed hash for that string.
18/// During rehashes, we will rehash the hash instead of the string, that makes
19/// rehashing cheap and allows cache coherent small hash tables.
20#[derive(Eq, Copy, Clone, Debug)]
21pub struct BytesHash<'a> {
22    payload: Option<&'a [u8]>,
23    pub(super) hash: u64,
24}
25
26impl<'a> BytesHash<'a> {
27    #[inline]
28    pub fn new(s: Option<&'a [u8]>, hash: u64) -> Self {
29        Self { payload: s, hash }
30    }
31}
32
33impl<'a> IsNull for BytesHash<'a> {
34    const HAS_NULLS: bool = true;
35    type Inner = BytesHash<'a>;
36
37    #[inline(always)]
38    fn is_null(&self) -> bool {
39        self.payload.is_none()
40    }
41
42    fn unwrap_inner(self) -> Self::Inner {
43        assert!(self.payload.is_some());
44        self
45    }
46}
47
48impl Hash for BytesHash<'_> {
49    #[inline]
50    fn hash<H: Hasher>(&self, state: &mut H) {
51        state.write_u64(self.hash)
52    }
53}
54
55impl PartialEq for BytesHash<'_> {
56    #[inline]
57    fn eq(&self, other: &Self) -> bool {
58        (self.hash == other.hash) && (self.payload == other.payload)
59    }
60}
61
62#[inline(always)]
63pub fn hash_to_partition(h: u64, n_partitions: usize) -> usize {
64    // Assuming h is a 64-bit random number, we note that
65    // h / 2^64 is almost a uniform random number in [0, 1), and thus
66    // floor(h * n_partitions / 2^64) is almost a uniform random integer in
67    // [0, n_partitions). Despite being written with u128 multiplication this
68    // compiles to a single mul / mulhi instruction on x86-x64/aarch64.
69    ((h as u128 * n_partitions as u128) >> 64) as usize
70}
71
72#[derive(Clone)]
73pub struct HashPartitioner {
74    num_partitions: usize,
75    seed: u64,
76}
77
78impl HashPartitioner {
79    /// Creates a new hash partitioner with the given number of partitions and
80    /// seed.
81    #[inline]
82    pub fn new(num_partitions: usize, mut seed: u64) -> Self {
83        assert!(num_partitions > 0);
84        // Make sure seeds bits are properly randomized, and is odd.
85        const ARBITRARY1: u64 = 0x85921e81c41226a0;
86        const ARBITRARY2: u64 = 0x3bc1d0faba166294;
87        const ARBITRARY3: u64 = 0xfbde893e21a73756;
88        seed = folded_multiply(seed ^ ARBITRARY1, ARBITRARY2);
89        seed = folded_multiply(seed, ARBITRARY3);
90        seed |= 1;
91        Self {
92            seed,
93            num_partitions,
94        }
95    }
96
97    /// Converts a hash to a partition. It is guaranteed that the output is
98    /// in the range [0, n_partitions), and that independent HashPartitioners
99    /// that we initialized with the same num_partitions and seed return the same
100    /// partition.
101    #[inline(always)]
102    pub fn hash_to_partition(&self, hash: u64) -> usize {
103        // Assuming r is a 64-bit random number, we note that
104        // r / 2^64 is almost a uniform random number in [0, 1), and thus
105        // floor(r * n_partitions / 2^64) is almost a uniform random integer in
106        // [0, n_partitions). Despite being written with u128 multiplication this
107        // compiles to a single mul / mulhi instruction on x86-x64/aarch64.
108        let shuffled = hash.wrapping_mul(self.seed);
109        ((shuffled as u128 * self.num_partitions as u128) >> 64) as usize
110    }
111
112    /// The partition nulls are put into.
113    #[inline(always)]
114    pub fn null_partition(&self) -> usize {
115        0
116    }
117
118    #[inline(always)]
119    pub fn num_partitions(&self) -> usize {
120        self.num_partitions
121    }
122}
123
124// TODO: use Hasher interface and support a random state.
125pub trait DirtyHash {
126    // A quick and dirty hash. Only the top bits of the hash are decent, such as
127    // used in hash_to_partition.
128    fn dirty_hash(&self) -> u64;
129}
130
131// Multiplication by a 'random' odd number gives a universal hash function in
132// the top bits.
133const RANDOM_ODD: u64 = 0x55fbfd6bfc5458e9;
134
135macro_rules! impl_hash_partition_as_u64 {
136    ($T: ty) => {
137        impl DirtyHash for $T {
138            #[inline(always)]
139            fn dirty_hash(&self) -> u64 {
140                (*self as u64).wrapping_mul(RANDOM_ODD)
141            }
142        }
143    };
144}
145
146impl_hash_partition_as_u64!(u8);
147impl_hash_partition_as_u64!(u16);
148impl_hash_partition_as_u64!(u32);
149impl_hash_partition_as_u64!(u64);
150impl_hash_partition_as_u64!(i8);
151impl_hash_partition_as_u64!(i16);
152impl_hash_partition_as_u64!(i32);
153impl_hash_partition_as_u64!(i64);
154
155impl DirtyHash for u128 {
156    #[inline(always)]
157    fn dirty_hash(&self) -> u64 {
158        (*self as u64)
159            .wrapping_mul(RANDOM_ODD)
160            .wrapping_add((*self >> 64) as u64)
161    }
162}
163
164impl DirtyHash for i128 {
165    #[inline(always)]
166    fn dirty_hash(&self) -> u64 {
167        (*self as u64)
168            .wrapping_mul(RANDOM_ODD)
169            .wrapping_add((*self >> 64) as u64)
170    }
171}
172
173impl DirtyHash for BytesHash<'_> {
174    #[inline(always)]
175    fn dirty_hash(&self) -> u64 {
176        self.hash
177    }
178}
179
180impl<T: DirtyHash + ?Sized> DirtyHash for &T {
181    #[inline(always)]
182    fn dirty_hash(&self) -> u64 {
183        (*self).dirty_hash()
184    }
185}
186
187// TODO: we should probably encourage explicit null handling, but for now we'll
188// allow directly getting a partition from a nullable value.
189impl<T: DirtyHash> DirtyHash for Option<T> {
190    #[inline(always)]
191    fn dirty_hash(&self) -> u64 {
192        self.as_ref().map(|s| s.dirty_hash()).unwrap_or(0)
193    }
194}