-
Notifications
You must be signed in to change notification settings - Fork 13.4k
Replace HashMap implementation with SwissTable #56241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
25f6e67
WIP: Replace HashMap implementation with SwissTable
Amanieu bd6164a
Change Entry<'a, K, V, S> back to Entry<'a, K, V>
Amanieu a818cf0
Add missing #[may_dangle]
Amanieu 2f89153
Fix warning
Amanieu 7fb0e90
Fix bug in convert_special_to_empty_and_full_to_deleted
Amanieu 7f493fa
Fix convert_special_to_empty_and_full_to_deleted again
Amanieu 8d1b330
Fix an overflow in subtract
Amanieu 2bec4c8
Fix a test
Amanieu e2e743a
Inline ALL THE THINGS!
Amanieu b1b2032
Fix tests
Amanieu b87e919
Fix test_behavior_resize_policy
Amanieu 9eb3eb8
Apply feedback
Amanieu 5f44f16
Implement try_reserve
Amanieu 1ed8a6a
Remove test for adaptive early resize
Amanieu a7b34ab
Fix test_try_reserve
Amanieu 44a9ce8
Remove search_bucket from raw_entry
Amanieu d687300
Fix variance for IterMut
Amanieu ed31ad7
Apply feedback for Group/BitMask
Amanieu 3bee3c0
Sync iterator changes from hashbrown
Amanieu ac86ef6
Minor changes
Amanieu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
use super::imp::{BitMaskWord, BITMASK_MASK, BITMASK_STRIDE}; | ||
use core::intrinsics; | ||
|
||
/// A bit mask which contains the result of a `Match` operation on a `Group` and | ||
/// allows iterating through them. | ||
/// | ||
/// The bit mask is arranged so that low-order bits represent lower memory | ||
/// addresses for group match results. | ||
/// | ||
/// For implementation reasons, the bits in the set may be sparsely packed, so | ||
/// that there is only one bit-per-byte used (the high bit, 7). If this is the | ||
/// case, `BITMASK_STRIDE` will be 8 to indicate a divide-by-8 should be | ||
/// performed on counts/indices to normalize this difference. `BITMASK_MASK` is | ||
/// similarly a mask of all the actually-used bits. | ||
#[derive(Copy, Clone)] | ||
pub struct BitMask(pub BitMaskWord); | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
impl BitMask { | ||
/// Returns a new `BitMask` with all bits inverted. | ||
#[inline] | ||
#[must_use] | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub fn invert(self) -> BitMask { | ||
BitMask(self.0 ^ BITMASK_MASK) | ||
} | ||
|
||
/// Returns a new `BitMask` with the lowest bit removed. | ||
#[inline] | ||
#[must_use] | ||
pub fn remove_lowest_bit(self) -> BitMask { | ||
BitMask(self.0 & (self.0 - 1)) | ||
} | ||
/// Returns whether the `BitMask` has at least one set bit. | ||
#[inline] | ||
pub fn any_bit_set(self) -> bool { | ||
self.0 != 0 | ||
} | ||
|
||
/// Returns the first set bit in the `BitMask`, if there is one. | ||
#[inline] | ||
pub fn lowest_set_bit(self) -> Option<usize> { | ||
if self.0 == 0 { | ||
None | ||
} else { | ||
Some(unsafe { self.lowest_set_bit_nonzero() }) | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/// Returns the first set bit in the `BitMask`, if there is one. The | ||
/// bitmask must not be empty. | ||
#[inline] | ||
pub unsafe fn lowest_set_bit_nonzero(self) -> usize { | ||
intrinsics::cttz_nonzero(self.0) as usize / BITMASK_STRIDE | ||
} | ||
|
||
/// Returns the number of trailing zeroes in the `BitMask`. | ||
#[inline] | ||
pub fn trailing_zeros(self) -> usize { | ||
// ARM doesn't have a trailing_zeroes instruction, and instead uses | ||
// reverse_bits (RBIT) + leading_zeroes (CLZ). However older ARM | ||
// versions (pre-ARMv7) don't have RBIT and need to emulate it | ||
// instead. Since we only have 1 bit set in each byte on ARM, we can | ||
// use swap_bytes (REV) + leading_zeroes instead. | ||
if cfg!(target_arch = "arm") && BITMASK_STRIDE % 8 == 0 { | ||
self.0.swap_bytes().leading_zeros() as usize / BITMASK_STRIDE | ||
} else { | ||
self.0.trailing_zeros() as usize / BITMASK_STRIDE | ||
} | ||
} | ||
|
||
/// Returns the number of leading zeroes in the `BitMask`. | ||
#[inline] | ||
pub fn leading_zeros(self) -> usize { | ||
self.0.leading_zeros() as usize / BITMASK_STRIDE | ||
} | ||
} | ||
|
||
impl IntoIterator for BitMask { | ||
type Item = usize; | ||
type IntoIter = BitMaskIter; | ||
|
||
#[inline] | ||
fn into_iter(self) -> BitMaskIter { | ||
BitMaskIter(self) | ||
} | ||
} | ||
|
||
/// Iterator over the contents of a `BitMask`, returning the indicies of set | ||
/// bits. | ||
pub struct BitMaskIter(BitMask); | ||
|
||
impl Iterator for BitMaskIter { | ||
type Item = usize; | ||
|
||
#[inline] | ||
fn next(&mut self) -> Option<usize> { | ||
let bit = self.0.lowest_set_bit()?; | ||
self.0 = self.0.remove_lowest_bit(); | ||
Some(bit) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
use super::bitmask::BitMask; | ||
use super::EMPTY; | ||
use core::{mem, ptr}; | ||
|
||
// Use the native word size as the group size. Using a 64-bit group size on | ||
// a 32-bit architecture will just end up being more expensive because | ||
// shifts and multiplies will need to be emulated. | ||
#[cfg(any( | ||
target_pointer_width = "64", | ||
target_arch = "aarch64", | ||
target_arch = "x86_64", | ||
))] | ||
type GroupWord = u64; | ||
#[cfg(all( | ||
target_pointer_width = "32", | ||
not(target_arch = "aarch64"), | ||
not(target_arch = "x86_64"), | ||
))] | ||
type GroupWord = u32; | ||
|
||
pub type BitMaskWord = GroupWord; | ||
pub const BITMASK_STRIDE: usize = 8; | ||
// We only care about the highest bit of each byte for the mask. | ||
pub const BITMASK_MASK: BitMaskWord = 0x8080_8080_8080_8080u64 as GroupWord; | ||
|
||
/// Helper function to replicate a byte across a `GroupWord`. | ||
#[inline] | ||
fn repeat(byte: u8) -> GroupWord { | ||
let repeat = byte as GroupWord; | ||
let repeat = repeat | repeat.wrapping_shl(8); | ||
let repeat = repeat | repeat.wrapping_shl(16); | ||
// This last line is a no-op with a 32-bit GroupWord | ||
repeat | repeat.wrapping_shl(32) | ||
} | ||
|
||
/// Abstraction over a group of control bytes which can be scanned in | ||
/// parallel. | ||
/// | ||
/// This implementation uses a word-sized integer. | ||
#[derive(Copy, Clone)] | ||
pub struct Group(GroupWord); | ||
|
||
// We perform all operations in the native endianess, and convert to | ||
// little-endian just before creating a BitMask. The can potentially | ||
// enable the compiler to eliminate unnecessary byte swaps if we are | ||
// only checking whether a BitMask is empty. | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
impl Group { | ||
/// Number of bytes in the group. | ||
pub const WIDTH: usize = mem::size_of::<Self>(); | ||
|
||
/// Returns a full group of empty bytes, suitable for use as the initial | ||
/// value for an empty hash table. | ||
/// | ||
/// This is guaranteed to be aligned to the group size. | ||
#[inline] | ||
pub fn static_empty() -> &'static [u8] { | ||
union AlignedBytes { | ||
_align: Group, | ||
bytes: [u8; Group::WIDTH], | ||
}; | ||
const ALIGNED_BYTES: AlignedBytes = AlignedBytes { | ||
bytes: [EMPTY; Group::WIDTH], | ||
}; | ||
unsafe { &ALIGNED_BYTES.bytes } | ||
} | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// Loads a group of bytes starting at the given address. | ||
#[inline] | ||
pub unsafe fn load(ptr: *const u8) -> Group { | ||
Group(ptr::read_unaligned(ptr as *const _)) | ||
} | ||
|
||
/// Loads a group of bytes starting at the given address, which must be | ||
/// aligned to `mem::align_of::<Group>()`. | ||
#[inline] | ||
pub unsafe fn load_aligned(ptr: *const u8) -> Group { | ||
debug_assert_eq!(ptr as usize & (mem::align_of::<Group>() - 1), 0); | ||
Group(ptr::read(ptr as *const _)) | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Stores the group of bytes to the given address, which must be | ||
/// aligned to `mem::align_of::<Group>()`. | ||
#[inline] | ||
pub unsafe fn store_aligned(&self, ptr: *mut u8) { | ||
debug_assert_eq!(ptr as usize & (mem::align_of::<Group>() - 1), 0); | ||
ptr::write(ptr as *mut _, self.0); | ||
} | ||
|
||
/// Returns a `BitMask` indicating all bytes in the group which *may* | ||
/// have the given value. | ||
/// | ||
/// This function may return a false positive in certain cases where | ||
/// the byte in the group differs from the searched value only in its | ||
/// lowest bit. This is fine because: | ||
/// - This never happens for `EMPTY` and `DELETED`, only full entries. | ||
/// - The check for key equality will catch these. | ||
/// - This only happens if there is at least 1 true match. | ||
/// - The chance of this happening is very low (< 1% chance per byte). | ||
#[inline] | ||
pub fn match_byte(&self, byte: u8) -> BitMask { | ||
// This algorithm is derived from | ||
// http://graphics.stanford.edu/~seander/bithacks.html##ValueInWord | ||
let cmp = self.0 ^ repeat(byte); | ||
BitMask((cmp.wrapping_sub(repeat(0x01)) & !cmp & repeat(0x80)).to_le()) | ||
} | ||
|
||
/// Returns a `BitMask` indicating all bytes in the group which are | ||
/// `EMPTY`. | ||
#[inline] | ||
pub fn match_empty(&self) -> BitMask { | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// If the high bit is set, then the byte must be either: | ||
// 1111_1111 (EMPTY) or 1000_0000 (DELETED). | ||
// So we can just check if the top two bits are 1 by ANDing them. | ||
BitMask((self.0 & (self.0 << 1) & repeat(0x80)).to_le()) | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Returns a `BitMask` indicating all bytes in the group which are | ||
/// `EMPTY` or `DELETED`. | ||
#[inline] | ||
pub fn match_empty_or_deleted(&self) -> BitMask { | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// A byte is EMPTY or DELETED iff the high bit is set | ||
BitMask((self.0 & repeat(0x80)).to_le()) | ||
} | ||
|
||
/// Performs the following transformation on all bytes in the group: | ||
/// - `EMPTY => EMPTY` | ||
/// - `DELETED => EMPTY` | ||
/// - `FULL => DELETED` | ||
#[inline] | ||
pub fn convert_special_to_empty_and_full_to_deleted(&self) -> Group { | ||
Amanieu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Map high_bit = 1 (EMPTY or DELETED) to 1111_1111 | ||
// and high_bit = 0 (FULL) to 1000_0000 | ||
// | ||
// Here's this logic expanded to concrete values: | ||
// let full = 1000_0000 (true) or 0000_0000 (false) | ||
// !1000_0000 + 1 = 0111_1111 + 1 = 1000_0000 (no carry) | ||
// !0000_0000 + 0 = 1111_1111 + 0 = 1111_1111 (no carry) | ||
let full = !self.0 & repeat(0x80); | ||
Group(!full + (full >> 7)) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.