Skip to main content

polars_core/hashing/
vector_hasher.rs

1use std::hash::BuildHasher;
2
3use polars_arrow::bitmap::utils::get_bit_unchecked;
4use polars_utils::aliases::PlSeedableRandomStateQuality;
5use polars_utils::hashing::{_boost_hash_combine, folded_multiply};
6use polars_utils::total_ord::{ToTotalOrd, TotalHash};
7use rayon::prelude::*;
8use xxhash_rust::xxh3::xxh3_64_with_seed;
9
10use super::*;
11use crate::prelude::*;
12use crate::runtime::RAYON;
13use crate::series::implementations::null::NullChunked;
14
15// See: https://github.com/tkaitchuck/aHash/blob/f9acd508bd89e7c5b2877a9510098100f9018d64/src/operations.rs#L4
16const MULTIPLE: u64 = 6364136223846793005;
17
18// Read more:
19//  https://www.cockroachlabs.com/blog/vectorized-hash-joiner/
20//  http://myeyesareblind.com/2017/02/06/Combine-hash-values/
21
22pub trait VecHash {
23    /// Compute the hash for all values in the array.
24    fn vec_hash(
25        &self,
26        _random_state: PlSeedableRandomStateQuality,
27        _buf: &mut Vec<u64>,
28    ) -> PolarsResult<()>;
29
30    fn vec_hash_combine(
31        &self,
32        _random_state: PlSeedableRandomStateQuality,
33        _hashes: &mut [u64],
34    ) -> PolarsResult<()>;
35}
36
37pub(crate) fn get_null_hash_value(random_state: &PlSeedableRandomStateQuality) -> u64 {
38    // we just start with a large prime number and hash that twice
39    // to get a constant hash value for null/None
40    let first = random_state.hash_one(3188347919usize);
41    random_state.hash_one(first)
42}
43
44fn insert_null_hash(
45    chunks: &[ArrayRef],
46    random_state: PlSeedableRandomStateQuality,
47    buf: &mut Vec<u64>,
48) {
49    let null_h = get_null_hash_value(&random_state);
50    let hashes = buf.as_mut_slice();
51
52    let mut offset = 0;
53    chunks.iter().for_each(|arr| {
54        if arr.null_count() > 0 {
55            let validity = arr.validity().unwrap();
56            let (slice, byte_offset, _) = validity.as_slice();
57            (0..validity.len())
58                .map(|i| unsafe { get_bit_unchecked(slice, i + byte_offset) })
59                .zip(&mut hashes[offset..])
60                .for_each(|(valid, h)| {
61                    *h = [null_h, *h][valid as usize];
62                })
63        }
64        offset += arr.len();
65    });
66}
67
68fn numeric_vec_hash<T>(
69    ca: &ChunkedArray<T>,
70    random_state: PlSeedableRandomStateQuality,
71    buf: &mut Vec<u64>,
72) where
73    T: PolarsNumericType,
74    T::Native: TotalHash + ToTotalOrd,
75    <T::Native as ToTotalOrd>::TotalOrdItem: Hash,
76{
77    // Note that we don't use the no null branch! This can break in unexpected ways.
78    // for instance with threading we split an array in n_threads, this may lead to
79    // splits that have no nulls and splits that have nulls. Then one array is hashed with
80    // Option<T> and the other array with T.
81    // Meaning that they cannot be compared. By always hashing on Option<T> the random_state is
82    // the only deterministic seed.
83    buf.clear();
84    buf.reserve(ca.len());
85
86    #[allow(unused_unsafe)]
87    #[allow(clippy::useless_transmute)]
88    ca.downcast_iter().for_each(|arr| {
89        buf.extend(
90            arr.values()
91                .as_slice()
92                .iter()
93                .copied()
94                .map(|v| random_state.hash_one(v.to_total_ord())),
95        );
96    });
97    insert_null_hash(&ca.chunks, random_state, buf)
98}
99
100fn numeric_vec_hash_combine<T>(
101    ca: &ChunkedArray<T>,
102    random_state: PlSeedableRandomStateQuality,
103    hashes: &mut [u64],
104) where
105    T: PolarsNumericType,
106    T::Native: TotalHash + ToTotalOrd,
107    <T::Native as ToTotalOrd>::TotalOrdItem: Hash,
108{
109    let null_h = get_null_hash_value(&random_state);
110
111    let mut offset = 0;
112    ca.downcast_iter().for_each(|arr| {
113        match arr.null_count() {
114            0 => arr
115                .values()
116                .as_slice()
117                .iter()
118                .zip(&mut hashes[offset..])
119                .for_each(|(v, h)| {
120                    // Inlined from ahash. This ensures we combine with the previous state.
121                    *h = folded_multiply(
122                        // Be careful not to xor the hash directly with the existing hash,
123                        // it would lead to 0-hashes for 2 columns containing equal values.
124                        random_state.hash_one(v.to_total_ord()) ^ folded_multiply(*h, MULTIPLE),
125                        MULTIPLE,
126                    );
127                }),
128            _ => {
129                let validity = arr.validity().unwrap();
130                let (slice, byte_offset, _) = validity.as_slice();
131                (0..validity.len())
132                    .map(|i| unsafe { get_bit_unchecked(slice, i + byte_offset) })
133                    .zip(&mut hashes[offset..])
134                    .zip(arr.values().as_slice())
135                    .for_each(|((valid, h), l)| {
136                        let lh = random_state.hash_one(l.to_total_ord());
137                        let to_hash = [null_h, lh][valid as usize];
138                        *h = folded_multiply(to_hash ^ folded_multiply(*h, MULTIPLE), MULTIPLE);
139                    });
140            },
141        }
142        offset += arr.len();
143    });
144}
145
146macro_rules! vec_hash_numeric {
147    ($ca:ident) => {
148        impl VecHash for $ca {
149            fn vec_hash(
150                &self,
151                random_state: PlSeedableRandomStateQuality,
152                buf: &mut Vec<u64>,
153            ) -> PolarsResult<()> {
154                numeric_vec_hash(self, random_state, buf);
155                Ok(())
156            }
157
158            fn vec_hash_combine(
159                &self,
160                random_state: PlSeedableRandomStateQuality,
161                hashes: &mut [u64],
162            ) -> PolarsResult<()> {
163                numeric_vec_hash_combine(self, random_state, hashes);
164                Ok(())
165            }
166        }
167    };
168}
169
170vec_hash_numeric!(Int64Chunked);
171vec_hash_numeric!(Int32Chunked);
172vec_hash_numeric!(Int16Chunked);
173vec_hash_numeric!(Int8Chunked);
174vec_hash_numeric!(UInt64Chunked);
175vec_hash_numeric!(UInt32Chunked);
176vec_hash_numeric!(UInt16Chunked);
177vec_hash_numeric!(UInt8Chunked);
178vec_hash_numeric!(Float64Chunked);
179vec_hash_numeric!(Float32Chunked);
180#[cfg(feature = "dtype-f16")]
181vec_hash_numeric!(Float16Chunked);
182#[cfg(feature = "dtype-u128")]
183vec_hash_numeric!(UInt128Chunked);
184#[cfg(any(feature = "dtype-decimal", feature = "dtype-i128"))]
185vec_hash_numeric!(Int128Chunked);
186
187impl VecHash for StringChunked {
188    fn vec_hash(
189        &self,
190        random_state: PlSeedableRandomStateQuality,
191        buf: &mut Vec<u64>,
192    ) -> PolarsResult<()> {
193        self.as_binary().vec_hash(random_state, buf)?;
194        Ok(())
195    }
196
197    fn vec_hash_combine(
198        &self,
199        random_state: PlSeedableRandomStateQuality,
200        hashes: &mut [u64],
201    ) -> PolarsResult<()> {
202        self.as_binary().vec_hash_combine(random_state, hashes)?;
203        Ok(())
204    }
205}
206
207// used in polars-pipe
208pub fn _hash_binary_array(
209    arr: &BinaryArray<i64>,
210    random_state: PlSeedableRandomStateQuality,
211    buf: &mut Vec<u64>,
212) {
213    let null_h = get_null_hash_value(&random_state);
214    if arr.null_count() == 0 {
215        // use the null_hash as seed to get a hash determined by `random_state` that is passed
216        buf.extend(arr.values_iter().map(|v| xxh3_64_with_seed(v, null_h)))
217    } else {
218        buf.extend(arr.into_iter().map(|opt_v| match opt_v {
219            Some(v) => xxh3_64_with_seed(v, null_h),
220            None => null_h,
221        }))
222    }
223}
224
225fn hash_binview_array(
226    arr: &BinaryViewArray,
227    random_state: PlSeedableRandomStateQuality,
228    buf: &mut Vec<u64>,
229) {
230    let null_h = get_null_hash_value(&random_state);
231    let views = arr.views().iter();
232    let buffers = arr.data_buffers();
233    unsafe {
234        if arr.null_count() == 0 {
235            buf.extend(views.map(|v| v.hash_with_buffers_unchecked(buffers, &random_state)))
236        } else {
237            buf.extend(views.zip(arr.validity().unwrap()).map(|(v, is_valid)| {
238                if is_valid {
239                    v.hash_with_buffers_unchecked(buffers, &random_state)
240                } else {
241                    null_h
242                }
243            }))
244        }
245    }
246}
247
248impl VecHash for BinaryChunked {
249    fn vec_hash(
250        &self,
251        random_state: PlSeedableRandomStateQuality,
252        buf: &mut Vec<u64>,
253    ) -> PolarsResult<()> {
254        buf.clear();
255        buf.reserve(self.len());
256        self.downcast_iter()
257            .for_each(|arr| hash_binview_array(arr, random_state.clone(), buf));
258        Ok(())
259    }
260
261    fn vec_hash_combine(
262        &self,
263        random_state: PlSeedableRandomStateQuality,
264        hashes: &mut [u64],
265    ) -> PolarsResult<()> {
266        let null_h = get_null_hash_value(&random_state);
267
268        let mut offset = 0;
269        self.downcast_iter().for_each(|arr| {
270            let views = arr.views().iter();
271            let buffers = arr.data_buffers();
272            let hashes = &mut hashes[offset..];
273            unsafe {
274                if arr.null_count() == 0 {
275                    views.zip(hashes).for_each(|(v, h)| {
276                        let l = v.hash_with_buffers_unchecked(buffers, &random_state);
277                        *h = _boost_hash_combine(l, *h)
278                    })
279                } else {
280                    views
281                        .zip(arr.validity().unwrap())
282                        .zip(hashes)
283                        .for_each(|((v, is_valid), h)| {
284                            let l = if is_valid {
285                                v.hash_with_buffers_unchecked(buffers, &random_state)
286                            } else {
287                                null_h
288                            };
289                            *h = _boost_hash_combine(l, *h)
290                        })
291                }
292            }
293            offset += arr.len();
294        });
295        Ok(())
296    }
297}
298
299impl VecHash for BinaryOffsetChunked {
300    fn vec_hash(
301        &self,
302        random_state: PlSeedableRandomStateQuality,
303        buf: &mut Vec<u64>,
304    ) -> PolarsResult<()> {
305        buf.clear();
306        buf.reserve(self.len());
307        self.downcast_iter()
308            .for_each(|arr| _hash_binary_array(arr, random_state.clone(), buf));
309        Ok(())
310    }
311
312    fn vec_hash_combine(
313        &self,
314        random_state: PlSeedableRandomStateQuality,
315        hashes: &mut [u64],
316    ) -> PolarsResult<()> {
317        let null_h = get_null_hash_value(&random_state);
318
319        let mut offset = 0;
320        self.downcast_iter().for_each(|arr| {
321            match arr.null_count() {
322                0 => arr
323                    .values_iter()
324                    .zip(&mut hashes[offset..])
325                    .for_each(|(v, h)| {
326                        let l = xxh3_64_with_seed(v, null_h);
327                        *h = _boost_hash_combine(l, *h)
328                    }),
329                _ => {
330                    let validity = arr.validity().unwrap();
331                    let (slice, byte_offset, _) = validity.as_slice();
332                    (0..validity.len())
333                        .map(|i| unsafe { get_bit_unchecked(slice, i + byte_offset) })
334                        .zip(&mut hashes[offset..])
335                        .zip(arr.values_iter())
336                        .for_each(|((valid, h), l)| {
337                            let l = if valid {
338                                xxh3_64_with_seed(l, null_h)
339                            } else {
340                                null_h
341                            };
342                            *h = _boost_hash_combine(l, *h)
343                        });
344                },
345            }
346            offset += arr.len();
347        });
348        Ok(())
349    }
350}
351
352impl VecHash for NullChunked {
353    fn vec_hash(
354        &self,
355        random_state: PlSeedableRandomStateQuality,
356        buf: &mut Vec<u64>,
357    ) -> PolarsResult<()> {
358        let null_h = get_null_hash_value(&random_state);
359        buf.clear();
360        buf.resize(self.len(), null_h);
361        Ok(())
362    }
363
364    fn vec_hash_combine(
365        &self,
366        random_state: PlSeedableRandomStateQuality,
367        hashes: &mut [u64],
368    ) -> PolarsResult<()> {
369        let null_h = get_null_hash_value(&random_state);
370        hashes
371            .iter_mut()
372            .for_each(|h| *h = _boost_hash_combine(null_h, *h));
373        Ok(())
374    }
375}
376impl VecHash for BooleanChunked {
377    fn vec_hash(
378        &self,
379        random_state: PlSeedableRandomStateQuality,
380        buf: &mut Vec<u64>,
381    ) -> PolarsResult<()> {
382        buf.clear();
383        buf.reserve(self.len());
384        let true_h = random_state.hash_one(true);
385        let false_h = random_state.hash_one(false);
386        let null_h = get_null_hash_value(&random_state);
387        self.downcast_iter().for_each(|arr| {
388            if arr.null_count() == 0 {
389                buf.extend(arr.values_iter().map(|v| if v { true_h } else { false_h }))
390            } else {
391                buf.extend(arr.into_iter().map(|opt_v| match opt_v {
392                    Some(true) => true_h,
393                    Some(false) => false_h,
394                    None => null_h,
395                }))
396            }
397        });
398        Ok(())
399    }
400
401    fn vec_hash_combine(
402        &self,
403        random_state: PlSeedableRandomStateQuality,
404        hashes: &mut [u64],
405    ) -> PolarsResult<()> {
406        let true_h = random_state.hash_one(true);
407        let false_h = random_state.hash_one(false);
408        let null_h = get_null_hash_value(&random_state);
409
410        let mut offset = 0;
411        self.downcast_iter().for_each(|arr| {
412            match arr.null_count() {
413                0 => arr
414                    .values_iter()
415                    .zip(&mut hashes[offset..])
416                    .for_each(|(v, h)| {
417                        let l = if v { true_h } else { false_h };
418                        *h = _boost_hash_combine(l, *h)
419                    }),
420                _ => {
421                    let validity = arr.validity().unwrap();
422                    let (slice, byte_offset, _) = validity.as_slice();
423                    (0..validity.len())
424                        .map(|i| unsafe { get_bit_unchecked(slice, i + byte_offset) })
425                        .zip(&mut hashes[offset..])
426                        .zip(arr.values())
427                        .for_each(|((valid, h), l)| {
428                            let l = if valid {
429                                if l { true_h } else { false_h }
430                            } else {
431                                null_h
432                            };
433                            *h = _boost_hash_combine(l, *h)
434                        });
435                },
436            }
437            offset += arr.len();
438        });
439        Ok(())
440    }
441}
442
443#[cfg(feature = "object")]
444impl<T> VecHash for ObjectChunked<T>
445where
446    T: PolarsObject,
447{
448    fn vec_hash(
449        &self,
450        random_state: PlSeedableRandomStateQuality,
451        buf: &mut Vec<u64>,
452    ) -> PolarsResult<()> {
453        // Note that we don't use the no null branch! This can break in unexpected ways.
454        // for instance with threading we split an array in n_threads, this may lead to
455        // splits that have no nulls and splits that have nulls. Then one array is hashed with
456        // Option<T> and the other array with T.
457        // Meaning that they cannot be compared. By always hashing on Option<T> the random_state is
458        // the only deterministic seed.
459        buf.clear();
460        buf.reserve(self.len());
461
462        self.downcast_iter()
463            .for_each(|arr| buf.extend(arr.into_iter().map(|opt_v| random_state.hash_one(opt_v))));
464
465        Ok(())
466    }
467
468    fn vec_hash_combine(
469        &self,
470        random_state: PlSeedableRandomStateQuality,
471        hashes: &mut [u64],
472    ) -> PolarsResult<()> {
473        self.apply_to_slice(
474            |opt_v, h| {
475                let hashed = random_state.hash_one(opt_v);
476                _boost_hash_combine(hashed, *h)
477            },
478            hashes,
479        );
480        Ok(())
481    }
482}
483
484pub fn _df_rows_to_hashes_threaded_vertical(
485    keys: &[DataFrame],
486    build_hasher: Option<PlSeedableRandomStateQuality>,
487) -> PolarsResult<(Vec<UInt64Chunked>, PlSeedableRandomStateQuality)> {
488    let build_hasher = build_hasher.unwrap_or_default();
489
490    let hashes = RAYON.install(|| {
491        keys.into_par_iter()
492            .map(|df| {
493                let hb = build_hasher.clone();
494                let mut hashes = vec![];
495                columns_to_hashes(df, Some(hb), &mut hashes)?;
496                Ok(UInt64Chunked::from_vec(PlSmallStr::EMPTY, hashes))
497            })
498            .collect::<PolarsResult<Vec<_>>>()
499    })?;
500    Ok((hashes, build_hasher))
501}
502
503pub fn columns_to_hashes(
504    keys: &DataFrame,
505    build_hasher: Option<PlSeedableRandomStateQuality>,
506    hashes: &mut Vec<u64>,
507) -> PolarsResult<PlSeedableRandomStateQuality> {
508    let build_hasher = build_hasher.unwrap_or_default();
509
510    if keys.width() == 0 {
511        let null_h = get_null_hash_value(&build_hasher);
512
513        for _ in 0..keys.height() {
514            hashes.push(null_h);
515        }
516
517        return Ok(build_hasher);
518    }
519
520    let mut iter = keys.columns().iter();
521    let first = iter.next().expect("at least one key");
522    first.vec_hash(build_hasher.clone(), hashes)?;
523
524    for keys in iter {
525        keys.vec_hash_combine(build_hasher.clone(), hashes)?;
526    }
527
528    Ok(build_hasher)
529}