Skip to content
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

Replace HashMap implementation with SwissTable #56241

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,269 changes: 341 additions & 928 deletions src/libstd/collections/hash/map.rs

Large diffs are not rendered by default.

10 changes: 1 addition & 9 deletions src/libstd/collections/hash/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@
//! Unordered containers, implemented as hash-tables

mod bench;
mod table;
mod raw;
pub mod map;
pub mod set;

trait Recover<Q: ?Sized> {
type Key;

fn get(&self, key: &Q) -> Option<&Self::Key>;
fn take(&mut self, key: &Q) -> Option<Self::Key>;
fn replace(&mut self, key: Self::Key) -> Option<Self::Key>;
}
100 changes: 100 additions & 0 deletions src/libstd/collections/hash/raw/bitmask.rs
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.
Amanieu marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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)
}
}
141 changes: 141 additions & 0 deletions src/libstd/collections/hash/raw/generic.rs
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))
}
}
Loading