Skip to main content

triblespace_core/patch/
bytetable.rs

1//!
2//! The number of buckets is doubled with each table growth, which is not only
3//! commonly used middle ground for growing data-structures between expensive
4//! allocation/reallocation and unused memory, but also limits the work required
5//! for rehashing as we will see shortly.
6//!
7//! The hash functions used are parameterised over the current size of the table
8//! and are what we call "compressed permutations", where the whole function is
9//! composed of two separate parametric operations
10//!
11//! hash(size) = compression(size) • permutation
12//!
13//!  * permutation: domain(hash) → [0 .. |domain|] ⊆ Nat;
14//!    reifies the randomness of the hash as a (read lossless) bijection from the
15//!    hash domain to the natural numbers
16//!  * compression: range(permutation) → range(hash);
17//!    which reduces (read lossy) the range of the permutation so that multiple
18//!    values of the hashes range are pigeonholed to the same element of its domain
19//!
20//! The compression operation we use truncates the upper (most significant) bits
21//! of the input so that it's range is equal to
22//! [0 .. |buckets|].
23//!
24//! compression(size, x) = ~(~0 << log2(size)) & x
25//!
26//! The limitation to sizes of a power of two aligns with the doubling of the
27//! hash table at each growth. In fact using the number of doublings as the parameter makes the log2 call superfluous.
28//!
29//! This compression function has an important property, as a new
30//! most significant bit is taken into consideration with each growth,
31//! each item either keeps its position or is moved to its position * 2.
32//! The only maintenance operation required to keep the hash consistent
33//! for each growth and parameter change is therefore to traverse the lower half
34//! of buckets and copy elements where neither updated hash points to their
35//! current bucket, to the corresponding bucket in the upper half.
36//! Incidentally this might flip the hash function used for this entry.
37
38use rand::seq::SliceRandom;
39use rand::thread_rng;
40use std::fmt::Debug;
41use std::sync::Once;
42
43/// The number of slots per bucket.
44const BUCKET_ENTRY_COUNT: usize = 2;
45
46/// The maximum number of slots per table.
47const MAX_SLOT_COUNT: usize = 256;
48
49/// The maximum number of cuckoo displacements attempted during
50/// insert before the size of the table is increased.
51const MAX_RETRIES: usize = 2;
52
53/// Global randomness used for bucket selection.
54static mut RANDOM_PERMUTATION_RAND: [u8; 256] = [0; 256];
55static mut RANDOM_PERMUTATION_HASH: [u8; 256] = [0; 256];
56static INIT: Once = Once::new();
57
58/// Initialise the randomness source and hash function
59/// used by all tables.
60pub fn init() {
61    INIT.call_once(|| {
62        let mut rng = thread_rng();
63        let mut bytes: [u8; 256] = [0; 256];
64
65        for (i, b) in bytes.iter_mut().enumerate() {
66            *b = i as u8;
67        }
68
69        bytes.shuffle(&mut rng);
70        unsafe {
71            RANDOM_PERMUTATION_HASH = bytes;
72        }
73
74        bytes.shuffle(&mut rng);
75        unsafe {
76            RANDOM_PERMUTATION_RAND = bytes;
77        }
78    });
79}
80
81/// Types must implement this trait in order to be storable in the byte table.
82///
83/// # Safety
84///
85/// Implementors must ensure that `key()` returns `None` iff the memory of the
86/// type is `mem::zeroed()`. Failure to uphold this contract may lead to
87/// incorrect behavior when entries are inserted into the table.
88pub unsafe trait ByteEntry {
89    /// Returns the byte key that identifies this entry's bucket.
90    fn key(&self) -> u8;
91}
92
93/// Represents the hashtable's internal buckets, which allow for up to
94/// `BUCKET_ENTRY_COUNT` elements to share the same colliding hash values.
95/// Buckets are laid out implicitly in a flat slice so bucket operations simply
96/// compute offsets into the table rather than delegating to a trait.
97///
98/// A cheap hash *cough* identity *cough* function that maps every entry to an
99/// almost linear ordering (modulo `BUCKET_ENTRY_COUNT`) when maximally grown.
100#[inline]
101fn cheap_hash(byte_key: u8) -> u8 {
102    byte_key
103}
104
105/// A hash function that uses a lookup table to provide a random bijective
106/// byte -> byte mapping.
107#[inline]
108fn rand_hash(byte_key: u8) -> u8 {
109    unsafe { RANDOM_PERMUTATION_HASH[byte_key as usize] }
110}
111
112/// Cut off the upper bits so that it fits in the bucket count.
113#[inline]
114fn compress_hash(slot_count: usize, hash: u8) -> u8 {
115    let bucket_count = (slot_count / BUCKET_ENTRY_COUNT) as u8;
116    let mask = bucket_count - 1;
117    hash & mask
118}
119
120/// A 256-bit set indexed by byte. Two `u128` words give one bit per
121/// possible byte value, so `insert`/`remove`/`contains` are O(1) bit
122/// ops and `drain_next_ascending` walks set bits via `trailing_zeros`
123/// (cost proportional to popcount, not the 256-bit width).
124#[derive(Clone, Copy, Default, PartialEq, Eq)]
125pub(crate) struct ByteSet([u128; 2]);
126
127impl ByteSet {
128    pub(crate) fn new_empty() -> Self {
129        ByteSet([0, 0])
130    }
131
132    pub(crate) fn insert(&mut self, idx: u8) {
133        let bit = (idx & 0b0111_1111) as u32;
134        self.0[(idx >> 7) as usize] |= 1u128 << bit;
135    }
136
137    pub(crate) fn remove(&mut self, idx: u8) {
138        let bit = (idx & 0b0111_1111) as u32;
139        self.0[(idx >> 7) as usize] &= !(1u128 << bit);
140    }
141
142    pub(crate) fn contains(&self, idx: u8) -> bool {
143        let bit = (idx & 0b0111_1111) as u32;
144        (self.0[(idx >> 7) as usize] & (1u128 << bit)) != 0
145    }
146
147    /// Element-wise intersection — keys present in both sets.
148    #[cfg_attr(not(feature = "parallel"), allow(dead_code))]
149    pub(crate) fn intersect(&self, other: &ByteSet) -> ByteSet {
150        ByteSet([self.0[0] & other.0[0], self.0[1] & other.0[1]])
151    }
152
153    /// Element-wise symmetric difference (XOR) — keys in exactly one set.
154    #[cfg_attr(not(feature = "parallel"), allow(dead_code))]
155    pub(crate) fn symmetric_difference(&self, other: &ByteSet) -> ByteSet {
156        ByteSet([self.0[0] ^ other.0[0], self.0[1] ^ other.0[1]])
157    }
158
159    /// Number of set bits.
160    #[allow(dead_code)]
161    pub(crate) fn popcount(&self) -> u32 {
162        self.0[0].count_ones() + self.0[1].count_ones()
163    }
164
165    /// Returns the lowest set byte (ascending order) and clears it;
166    /// `None` when empty. Walks set bits via `trailing_zeros` so the
167    /// cost is proportional to popcount, not 256.
168    #[cfg_attr(not(feature = "parallel"), allow(dead_code))]
169    pub(crate) fn drain_next_ascending(&mut self) -> Option<u8> {
170        if self.0[0] != 0 {
171            let bit = self.0[0].trailing_zeros();
172            self.0[0] &= !(1u128 << bit);
173            Some(bit as u8)
174        } else if self.0[1] != 0 {
175            let bit = self.0[1].trailing_zeros();
176            self.0[1] &= !(1u128 << bit);
177            Some(128 + bit as u8)
178        } else {
179            None
180        }
181    }
182}
183
184fn plan_insert<T: ByteEntry + Debug>(
185    table: &mut [Option<T>],
186    bucket_idx: usize,
187    depth: usize,
188    visited: &mut ByteSet,
189) -> Option<usize> {
190    let bucket_start = bucket_idx * BUCKET_ENTRY_COUNT;
191
192    for slot_idx in 0..BUCKET_ENTRY_COUNT {
193        if table[bucket_start + slot_idx].is_none() {
194            return Some(bucket_start + slot_idx);
195        }
196    }
197
198    if depth == 0 {
199        return None;
200    }
201
202    for slot_idx in 0..BUCKET_ENTRY_COUNT {
203        let key = table[bucket_start + slot_idx]
204            .as_ref()
205            .expect("slot must be occupied")
206            .key();
207        if visited.contains(key) {
208            continue;
209        }
210        visited.insert(key);
211
212        let cheap = compress_hash(table.len(), cheap_hash(key)) as usize;
213        let rand = compress_hash(table.len(), rand_hash(key)) as usize;
214        // Try the other bucket that the key could occupy.
215        let alt_idx = if bucket_idx == cheap { rand } else { cheap };
216        if alt_idx != bucket_idx {
217            if let Some(hole_idx) = plan_insert(table, alt_idx, depth - 1, visited) {
218                table[hole_idx] = table[bucket_start + slot_idx].take();
219                visited.remove(key);
220                return Some(bucket_start + slot_idx);
221            }
222        }
223
224        visited.remove(key);
225    }
226
227    None
228}
229
230/// Operations on a cuckoo hash table indexed by single-byte keys.
231pub trait ByteTable<T: ByteEntry + Debug> {
232    /// Looks up an entry by its byte key, returning a reference if found.
233    fn table_get(&self, byte_key: u8) -> Option<&T>;
234    /// Returns a mutable reference to the slot holding `byte_key`, if present.
235    fn table_get_slot(&mut self, byte_key: u8) -> Option<&mut Option<T>>;
236    /// Inserts `entry` into the table, returning it back if the table is full.
237    fn table_insert(&mut self, entry: T) -> Option<T>;
238    /// Moves entries from `self` into `grown`, which must be twice the size.
239    fn table_grow(&mut self, grown: &mut Self);
240}
241
242impl<T: ByteEntry + Debug> ByteTable<T> for [Option<T>] {
243    fn table_get(&self, byte_key: u8) -> Option<&T> {
244        let cheap_start =
245            compress_hash(self.len(), cheap_hash(byte_key)) as usize * BUCKET_ENTRY_COUNT;
246        for slot in 0..BUCKET_ENTRY_COUNT {
247            if let Some(entry) = self[cheap_start + slot].as_ref() {
248                if entry.key() == byte_key {
249                    return Some(entry);
250                }
251            }
252        }
253
254        let rand_start =
255            compress_hash(self.len(), rand_hash(byte_key)) as usize * BUCKET_ENTRY_COUNT;
256        for slot in 0..BUCKET_ENTRY_COUNT {
257            if let Some(entry) = self[rand_start + slot].as_ref() {
258                if entry.key() == byte_key {
259                    return Some(entry);
260                }
261            }
262        }
263        None
264    }
265
266    fn table_get_slot(&mut self, byte_key: u8) -> Option<&mut Option<T>> {
267        let cheap_start =
268            compress_hash(self.len(), cheap_hash(byte_key)) as usize * BUCKET_ENTRY_COUNT;
269        for slot in 0..BUCKET_ENTRY_COUNT {
270            let idx = cheap_start + slot;
271            if let Some(entry) = self[idx].as_ref() {
272                if entry.key() == byte_key {
273                    return Some(&mut self[idx]);
274                }
275            }
276        }
277
278        let rand_start =
279            compress_hash(self.len(), rand_hash(byte_key)) as usize * BUCKET_ENTRY_COUNT;
280        for slot in 0..BUCKET_ENTRY_COUNT {
281            let idx = rand_start + slot;
282            if let Some(entry) = self[idx].as_ref() {
283                if entry.key() == byte_key {
284                    return Some(&mut self[idx]);
285                }
286            }
287        }
288        None
289    }
290
291    /// An entry with the same key must not exist in the table yet.
292    fn table_insert(&mut self, inserted: T) -> Option<T> {
293        debug_assert!(self.table_get(inserted.key()).is_none());
294
295        let mut visited = ByteSet::new_empty();
296        let key = inserted.key();
297        visited.insert(key);
298        let limit = if self.len() == MAX_SLOT_COUNT {
299            MAX_SLOT_COUNT
300        } else {
301            MAX_RETRIES
302        };
303
304        let cheap_bucket = compress_hash(self.len(), cheap_hash(key)) as usize;
305        if let Some(slot) = plan_insert(self, cheap_bucket, limit, &mut visited) {
306            self[slot] = Some(inserted);
307            return None;
308        }
309
310        let rand_bucket = compress_hash(self.len(), rand_hash(key)) as usize;
311        if let Some(slot) = plan_insert(self, rand_bucket, limit, &mut visited) {
312            self[slot] = Some(inserted);
313            return None;
314        }
315
316        Some(inserted)
317    }
318
319    fn table_grow(&mut self, grown: &mut Self) {
320        debug_assert!(self.len() * 2 == grown.len());
321        let buckets_len = self.len() / BUCKET_ENTRY_COUNT;
322        let grown_len = grown.len();
323        let (lower_portion, upper_portion) = grown.split_at_mut(self.len());
324        for bucket_index in 0..buckets_len {
325            let start = bucket_index * BUCKET_ENTRY_COUNT;
326            for slot in 0..BUCKET_ENTRY_COUNT {
327                if let Some(entry) = self[start + slot].take() {
328                    let byte_key = entry.key();
329                    let cheap_index = compress_hash(grown_len, cheap_hash(byte_key));
330                    let rand_index = compress_hash(grown_len, rand_hash(byte_key));
331
332                    let dest_bucket =
333                        if bucket_index as u8 == cheap_index || bucket_index as u8 == rand_index {
334                            &mut lower_portion[start..start + BUCKET_ENTRY_COUNT]
335                        } else {
336                            &mut upper_portion[start..start + BUCKET_ENTRY_COUNT]
337                        };
338
339                    for dest_slot in dest_bucket.iter_mut() {
340                        if dest_slot.is_none() {
341                            *dest_slot = Some(entry);
342                            break;
343                        }
344                    }
345                }
346            }
347        }
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use proptest::prelude::*;
355
356    #[derive(Copy, Clone, Debug)]
357    #[repr(C)]
358    struct DummyEntry {
359        value: u8,
360    }
361
362    impl DummyEntry {
363        fn new(byte_key: u8) -> Self {
364            DummyEntry { value: byte_key }
365        }
366    }
367
368    unsafe impl ByteEntry for DummyEntry {
369        fn key(&self) -> u8 {
370            self.value
371        }
372    }
373
374    proptest! {
375        #[test]
376        fn empty_table_then_empty_get(n in 0u8..255) {
377            init();
378            let table: [Option<DummyEntry>; 4] = [None; 4];
379            prop_assert!(table.table_get(n).is_none());
380        }
381
382        #[test]
383        fn single_insert_success(n in 0u8..255) {
384            init();
385            let mut table: [Option<DummyEntry>; 4] = [None; 4];
386            let entry = DummyEntry::new(n);
387            let displaced = table.table_insert(entry);
388            prop_assert!(displaced.is_none());
389            prop_assert!(table.table_get(n).is_some());
390        }
391
392        #[test]
393        fn insert_success(entry_set in prop::collection::hash_set(0u8..255, 1..32)) {
394            init();
395
396            let entries: Vec<_> = entry_set.iter().copied().collect();
397            let mut displaced: Option<DummyEntry> = None;
398            let mut i = 0;
399
400            macro_rules! insert_step {
401                ($table:ident, $grown_table:ident, $grown_size:expr) => {
402                    while displaced.is_none() && i < entries.len() {
403                        displaced = $table.table_insert(DummyEntry::new(entries[i]));
404                        if(displaced.is_none()) {
405                            for j in 0..=i {
406                                prop_assert!($table.table_get(entries[j]).is_some(),
407                                "Missing value {} after insert", entries[j]);
408                            }
409                        }
410                        i += 1;
411                    }
412
413                    if displaced.is_none() {return Ok(())};
414
415                    let mut $grown_table: [Option<DummyEntry>; $grown_size] = [None; $grown_size];
416                    $table.table_grow(&mut $grown_table);
417                    displaced = $grown_table.table_insert(displaced.unwrap());
418
419                    if displaced.is_none() {
420                        for j in 0..i {
421                            prop_assert!(
422                                $grown_table.table_get(entries[j]).is_some(),
423                                "Missing value {} after growth with hash {:?}",
424                                entries[j],
425                                unsafe { RANDOM_PERMUTATION_HASH }
426                            );
427                        }
428                    }
429                };
430            }
431
432            let mut table2: [Option<DummyEntry>; 2] = [None, None];
433            insert_step!(table2, table4, 4);
434            insert_step!(table4, table8, 8);
435            insert_step!(table8, table16, 16);
436            insert_step!(table16, table32, 32);
437            insert_step!(table32, table64, 64);
438            insert_step!(table64, table128, 128);
439            insert_step!(table128, table256, 256);
440
441            prop_assert!(displaced.is_none());
442        }
443    }
444
445    #[test]
446    fn sequential_insert_all_keys() {
447        init();
448        let mut table: [Option<DummyEntry>; 256] = [None; 256];
449        for n in 0u8..=255 {
450            assert!(table.table_insert(DummyEntry::new(n)).is_none());
451        }
452    }
453}