Skip to content

Commit 89bdf54

Browse files
committed
Extract index from the high hash bits instead of the low bits.
Some Hashers, and FxHasher in particular, mix the low bits poorly; so only use the high bits for both hashes. Instead of introducing bucket_shift next to bucket_mask in the top-level struct, derive them both from capacity_log2. This avoids exacerbating struct size in rust-lang#69. This change is also a prerequisite for rayon-based parallel collect, which requires that the high bits of the index are always in the same place regardless of table size. name old ns/iter new ns/iter diff ns/iter diff % speedup find_existing 0 0 0 NaN% x NaN find_existing_high_bits 84,320 3,442 -80,878 -95.92% x 24.50 find_nonexisting 0 0 0 NaN% x NaN get_remove_insert 25 29 4 16.00% x 0.86 grow_by_insertion 205 209 4 1.95% x 0.98 grow_by_insertion_kb 290 180 -110 -37.93% x 1.61 hashmap_as_queue 25 26 1 4.00% x 0.96 insert_8_char_string 18,038 17,491 -547 -3.03% x 1.03 new_drop 0 0 0 NaN% x NaN new_insert_drop 45 50 5 11.11% x 0.90
1 parent 7e79b0c commit 89bdf54

File tree

1 file changed

+40
-31
lines changed

1 file changed

+40
-31
lines changed

src/raw/mod.rs

+40-31
Original file line numberDiff line numberDiff line change
@@ -114,22 +114,14 @@ fn special_is_empty(ctrl: u8) -> bool {
114114
ctrl & 0x01 != 0
115115
}
116116

117-
/// Primary hash function, used to select the initial bucket to probe from.
118-
#[inline]
119-
#[allow(clippy::cast_possible_truncation)]
120-
fn h1(hash: u64) -> usize {
121-
// On 32-bit platforms we simply ignore the higher hash bits.
122-
hash as usize
123-
}
124-
125117
/// Secondary hash function, saved in the low 7 bits of the control byte.
126118
#[inline]
127119
#[allow(clippy::cast_possible_truncation)]
128120
fn h2(hash: u64) -> u8 {
129121
// Grab the top 7 bits of the hash. While the hash is normally a full 64-bit
130122
// value, some hash functions (such as FxHash) produce a usize result
131123
// instead, which means that the top 32 bits are 0 on 32-bit platforms.
132-
let hash_len = usize::min(mem::size_of::<usize>(), mem::size_of::<u64>());
124+
let hash_len = mem::size_of::<usize>();
133125
let top7 = hash >> (hash_len * 8 - 7);
134126
(top7 & 0x7f) as u8 // truncation
135127
}
@@ -327,9 +319,8 @@ impl<T> Bucket<T> {
327319

328320
/// A raw hash table with an unsafe API.
329321
pub struct RawTable<T> {
330-
// Mask to get an index from a hash value. The value is one less than the
331-
// number of buckets in the table.
332-
bucket_mask: usize,
322+
// Log2 of the table capacity (which is always a power of 2)
323+
capacity_log2: u8,
333324

334325
// Pointer to the array of control bytes
335326
ctrl: NonNull<u8>,
@@ -358,7 +349,7 @@ impl<T> RawTable<T> {
358349
Self {
359350
data: NonNull::dangling(),
360351
ctrl: NonNull::from(&Group::static_empty()[0]),
361-
bucket_mask: 0,
352+
capacity_log2: 0,
362353
items: 0,
363354
growth_left: 0,
364355
marker: PhantomData,
@@ -380,13 +371,31 @@ impl<T> RawTable<T> {
380371
Ok(Self {
381372
data,
382373
ctrl,
383-
bucket_mask: buckets - 1,
374+
capacity_log2: if buckets == 0 { 0 } else { buckets.trailing_zeros() as u8},
384375
items: 0,
385376
growth_left: bucket_mask_to_capacity(buckets - 1),
386377
marker: PhantomData,
387378
})
388379
}
389380

381+
// Mask to get an index from a hash value. The value is one less than the
382+
// number of buckets in the table.
383+
#[inline]
384+
fn bucket_mask(&self) -> usize {
385+
(1 << self.capacity_log2 as usize) - 1
386+
}
387+
388+
// Number of bits to shift to extract the index from the hash.
389+
#[inline]
390+
fn bucket_shift(&self) -> u32 {
391+
// The secondary hash in h2 takes the top 7 bits, this extracts the
392+
// primary hash from the high bits just under those.
393+
// The default FxHasher does not mix in low bits as well, so we only
394+
// use high bits.
395+
let hash_len = mem::size_of::<usize>();
396+
8 * hash_len as u32 - 7 - self.capacity_log2 as u32
397+
}
398+
390399
/// Attempts to allocate a new hash table with at least enough capacity
391400
/// for inserting the given number of elements without reallocating.
392401
fn try_with_capacity(
@@ -442,7 +451,7 @@ impl<T> RawTable<T> {
442451
/// Returns a pointer to an element in the table.
443452
#[inline]
444453
pub unsafe fn bucket(&self, index: usize) -> Bucket<T> {
445-
debug_assert_ne!(self.bucket_mask, 0);
454+
debug_assert_ne!(self.bucket_mask(), 0);
446455
debug_assert!(index < self.buckets());
447456
Bucket::from_base_index(self.data.as_ptr(), index)
448457
}
@@ -451,7 +460,7 @@ impl<T> RawTable<T> {
451460
#[inline]
452461
pub unsafe fn erase_no_drop(&mut self, item: &Bucket<T>) {
453462
let index = self.bucket_index(item);
454-
let index_before = index.wrapping_sub(Group::WIDTH) & self.bucket_mask;
463+
let index_before = index.wrapping_sub(Group::WIDTH) & self.bucket_mask();
455464
let empty_before = Group::load(self.ctrl(index_before)).match_empty();
456465
let empty_after = Group::load(self.ctrl(index)).match_empty();
457466

@@ -481,8 +490,8 @@ impl<T> RawTable<T> {
481490
#[inline]
482491
fn probe_seq(&self, hash: u64) -> ProbeSeq {
483492
ProbeSeq {
484-
bucket_mask: self.bucket_mask,
485-
pos: h1(hash) & self.bucket_mask,
493+
bucket_mask: self.bucket_mask(),
494+
pos: (hash as usize).wrapping_shr(self.bucket_shift()) & self.bucket_mask(),
486495
stride: 0,
487496
}
488497
}
@@ -494,7 +503,7 @@ impl<T> RawTable<T> {
494503
// Replicate the first Group::WIDTH control bytes at the end of
495504
// the array without using a branch:
496505
// - If index >= Group::WIDTH then index == index2.
497-
// - Otherwise index2 == self.bucket_mask + 1 + index.
506+
// - Otherwise index2 == self.bucket_mask() + 1 + index.
498507
//
499508
// The very last replicated control byte is never actually read because
500509
// we mask the initial index for unaligned loads, but we write it
@@ -509,7 +518,7 @@ impl<T> RawTable<T> {
509518
// ---------------------------------------------
510519
// | [A] | [B] | [EMPTY] | [EMPTY] | [A] | [B] |
511520
// ---------------------------------------------
512-
let index2 = ((index.wrapping_sub(Group::WIDTH)) & self.bucket_mask) + Group::WIDTH;
521+
let index2 = ((index.wrapping_sub(Group::WIDTH)) & self.bucket_mask()) + Group::WIDTH;
513522

514523
*self.ctrl(index) = ctrl;
515524
*self.ctrl(index2) = ctrl;
@@ -525,7 +534,7 @@ impl<T> RawTable<T> {
525534
unsafe {
526535
let group = Group::load(self.ctrl(pos));
527536
if let Some(bit) = group.match_empty_or_deleted().lowest_set_bit() {
528-
let result = (pos + bit) & self.bucket_mask;
537+
let result = (pos + bit) & self.bucket_mask();
529538

530539
// In tables smaller than the group width, trailing control
531540
// bytes outside the range of the table are filled with
@@ -537,7 +546,7 @@ impl<T> RawTable<T> {
537546
// slot (due to the load factor) before hitting the trailing
538547
// control bytes (containing EMPTY).
539548
if unlikely(is_full(*self.ctrl(result))) {
540-
debug_assert!(self.bucket_mask < Group::WIDTH);
549+
debug_assert!(self.bucket_mask() < Group::WIDTH);
541550
debug_assert_ne!(pos, 0);
542551
return Group::load_aligned(self.ctrl(0))
543552
.match_empty_or_deleted()
@@ -562,7 +571,7 @@ impl<T> RawTable<T> {
562571
}
563572
}
564573
self.items = 0;
565-
self.growth_left = bucket_mask_to_capacity(self.bucket_mask);
574+
self.growth_left = bucket_mask_to_capacity(self.bucket_mask());
566575
}
567576

568577
/// Removes all elements from the table without freeing the backing memory.
@@ -653,7 +662,7 @@ impl<T> RawTable<T> {
653662

654663
// Rehash in-place without re-allocating if we have plenty of spare
655664
// capacity that is locked up due to DELETED entries.
656-
if new_items < bucket_mask_to_capacity(self.bucket_mask) / 2 {
665+
if new_items < bucket_mask_to_capacity(self.bucket_mask()) / 2 {
657666
self.rehash_in_place(hasher);
658667
Ok(())
659668
} else {
@@ -700,7 +709,7 @@ impl<T> RawTable<T> {
700709
}
701710
}
702711
}
703-
self_.growth_left = bucket_mask_to_capacity(self_.bucket_mask) - self_.items;
712+
self_.growth_left = bucket_mask_to_capacity(self_.bucket_mask()) - self_.items;
704713
});
705714

706715
// At this point, DELETED elements are elements that we haven't
@@ -724,7 +733,7 @@ impl<T> RawTable<T> {
724733
// same unaligned group, then there is no benefit in moving
725734
// it and we can just continue to the next item.
726735
let probe_index = |pos: usize| {
727-
(pos.wrapping_sub(guard.probe_seq(hash).pos) & guard.bucket_mask)
736+
(pos.wrapping_sub(guard.probe_seq(hash).pos) & guard.bucket_mask())
728737
/ Group::WIDTH
729738
};
730739
if likely(probe_index(i) == probe_index(new_i)) {
@@ -755,7 +764,7 @@ impl<T> RawTable<T> {
755764
}
756765
}
757766

758-
guard.growth_left = bucket_mask_to_capacity(guard.bucket_mask) - guard.items;
767+
guard.growth_left = bucket_mask_to_capacity(guard.bucket_mask()) - guard.items;
759768
mem::forget(guard);
760769
}
761770
}
@@ -851,7 +860,7 @@ impl<T> RawTable<T> {
851860
for pos in self.probe_seq(hash) {
852861
let group = Group::load(self.ctrl(pos));
853862
for bit in group.match_byte(h2(hash)) {
854-
let index = (pos + bit) & self.bucket_mask;
863+
let index = (pos + bit) & self.bucket_mask();
855864
let bucket = self.bucket(index);
856865
if likely(eq(bucket.as_ref())) {
857866
return Some(bucket);
@@ -885,20 +894,20 @@ impl<T> RawTable<T> {
885894
/// Returns the number of buckets in the table.
886895
#[inline]
887896
pub fn buckets(&self) -> usize {
888-
self.bucket_mask + 1
897+
self.bucket_mask() + 1
889898
}
890899

891900
/// Returns the number of control bytes in the table.
892901
#[inline]
893902
fn num_ctrl_bytes(&self) -> usize {
894-
self.bucket_mask + 1 + Group::WIDTH
903+
self.bucket_mask() + 1 + Group::WIDTH
895904
}
896905

897906
/// Returns whether this table points to the empty singleton with a capacity
898907
/// of 0.
899908
#[inline]
900909
fn is_empty_singleton(&self) -> bool {
901-
self.bucket_mask == 0
910+
self.capacity_log2 == 0
902911
}
903912

904913
/// Returns an iterator over every element in the table. It is up to

0 commit comments

Comments
 (0)