Skip to content
Open
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
12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@ readme = "README.md"
repository = "https://github.com/ihciah/small-map"

[dependencies]
hashbrown = { version = "0.14", features = [
hashbrown = { version = "0.15.4", features = [
"inline-more",
"allocator-api2",
], default-features = false }

serde = { version = "1", default-features = false, optional = true }
ahash = { version = "0.8", default-features = false, optional = true }
rustc-hash = { version = "1", default-features = false, optional = true }
rustc-hash = { version = "2.1.1", default-features = false, optional = true }

[dev-dependencies]
rand = { version = "0.8" }
criterion = { version = "0.5", features = ["html_reports"] }
pprof = { version = "0.13", features = ["flamegraph"] }
rand = { version = "0.9.1" }
criterion = { version = "0.6.0", features = ["html_reports"] }

[target.'cfg(unix)'.dev-dependencies]
pprof = { version = "0.15.0", features = ["flamegraph"] }

[features]
default = ["hashes"]
Expand Down
6 changes: 5 additions & 1 deletion benches/simple.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};

fn smallmap<const N: usize>(n: u8) {
let mut map = small_map::SmallMap::<N, _, _>::default();
Expand Down Expand Up @@ -55,6 +57,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("stdhashmap-simple-16", |b| b.iter(|| std_hashmap(16)));
}

#[cfg(unix)]
mod profile {
use std::{fs::File, path::Path};

Expand Down Expand Up @@ -100,6 +103,7 @@ mod profile {

criterion_main!(benches);
criterion_group!(benches, criterion_benchmark);
//#[cfg(unix)]
// criterion_group! {
// name = benches;
// // This can be any expression that returns a `Criterion` object.
Expand Down
60 changes: 35 additions & 25 deletions src/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub(crate) struct AlignedGroups<const N: usize> {
impl<const N: usize> AlignedGroups<N> {
#[inline]
unsafe fn ctrl(&self, index: usize) -> *mut u8 {
self.groups.as_ptr().add(index).cast_mut()
unsafe { self.groups.as_ptr().add(index) }.cast_mut()
}

#[inline]
Expand Down Expand Up @@ -218,17 +218,19 @@ impl<const N: usize, T> RawInline<N, T> {
/// raw bucket.
#[inline]
unsafe fn insert_in_slot(&mut self, hash: u64, slot: InsertSlot, value: T) -> Bucket<T> {
self.record_item_insert_at(slot.index, hash);
let bucket = self.bucket(slot.index);
bucket.write(value);
bucket
unsafe {
self.record_item_insert_at(slot.index, hash);
let bucket = self.bucket(slot.index);
bucket.write(value);
bucket
}
}

/// Inserts a new element into the table in the given slot, and returns its
/// raw bucket.
#[inline]
unsafe fn record_item_insert_at(&mut self, index: usize, hash: u64) {
self.set_ctrl_h2(index, hash);
unsafe { self.set_ctrl_h2(index, hash); }
self.len += 1;
}

Expand All @@ -237,7 +239,7 @@ impl<const N: usize, T> RawInline<N, T> {
#[inline]
unsafe fn set_ctrl_h2(&mut self, index: usize, hash: u64) {
// SAFETY: The caller must uphold the safety rules for the [`RawTableInner::set_ctrl_h2`]
*self.aligned_groups.ctrl(index) = h2(hash);
unsafe { *self.aligned_groups.ctrl(index) = h2(hash); }
}

/// Finds and removes an element from the table, returning it.
Expand All @@ -254,50 +256,58 @@ impl<const N: usize, T> RawInline<N, T> {
#[inline]
#[allow(clippy::needless_pass_by_value)]
unsafe fn remove(&mut self, item: Bucket<T>) -> (T, InsertSlot) {
self.erase_no_drop(&item);
(
item.read(),
InsertSlot {
index: self.bucket_index(&item),
},
)
unsafe {
self.erase_no_drop(&item);
(
item.read(),
InsertSlot {
index: self.bucket_index(&item),
},
)
}
}

/// Erases an element from the table without dropping it.
#[inline]
unsafe fn erase_no_drop(&mut self, item: &Bucket<T>) {
let index = self.bucket_index(item);
self.erase(index);
unsafe {
let index = self.bucket_index(item);
self.erase(index);
}
}

/// Returns the index of a bucket from a `Bucket`.
#[inline]
unsafe fn bucket_index(&self, bucket: &Bucket<T>) -> usize {
bucket.to_base_index(NonNull::new_unchecked(self.data.as_ptr() as _))
unsafe { bucket.to_base_index(NonNull::new_unchecked(self.data.as_ptr() as _)) }
}

/// Erases the [`Bucket`]'s control byte at the given index so that it does not
/// triggered as full, decreases the `items` of the table and, if it can be done,
/// increases `self.growth_left`.
#[inline]
unsafe fn erase(&mut self, index: usize) {
*self.aligned_groups.ctrl(index) = DELETED;
unsafe { *self.aligned_groups.ctrl(index) = DELETED; }
self.len -= 1;
}

/// Returns a pointer to an element in the table.
#[inline]
unsafe fn bucket(&self, index: usize) -> Bucket<T> {
Bucket::from_base_index(
NonNull::new_unchecked(transmute(self.data.as_ptr().cast_mut())),
index,
)
unsafe {
Bucket::from_base_index(
NonNull::new_unchecked(transmute(self.data.as_ptr().cast_mut())),
index,
)
}
}

#[inline]
unsafe fn raw_iter_inner(&self) -> RawIterInner<T> {
let init_group = Group::load_aligned(self.aligned_groups.ctrl(0)).match_full();
RawIterInner::new(init_group, self.len)
unsafe {
let init_group = Group::load_aligned(self.aligned_groups.ctrl(0)).match_full();
RawIterInner::new(init_group, self.len)
}
}

#[inline]
Expand Down Expand Up @@ -434,7 +444,7 @@ impl<const N: usize, K, V, S> Inline<N, K, V, S> {
// Hasher must exist.
#[inline]
pub(crate) unsafe fn take_hasher(&mut self) -> S {
self.hash_builder.take().unwrap_unchecked()
unsafe { self.hash_builder.take().unwrap_unchecked() }
}

#[inline]
Expand Down
72 changes: 64 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ where
SmallMap::Inline(_) => unsafe { unreachable_unchecked() },
};
for (k, v) in inline.into_iter() {
heap.insert_unique_unchecked(k, v);
// Safety: Heap is new, so each K is unique (as long as all keys in Inline are unique)
unsafe { heap.insert_unique_unchecked(k, v); }
}
heap
}
Expand Down Expand Up @@ -254,6 +255,42 @@ where
}
}

impl<const N: usize, K, V, S> SmallMap<N, K, V, S>
where
S: Clone,
{
/// Clears the map.
///
/// This method clears the map as resets it to the Inline state.
///
/// # Examples
///
/// ```
/// use small_map::SmallMap;
///
/// let mut map: SmallMap<8, i32, ()> = SmallMap::new();
/// for i in 0..16 {
/// map.insert(i, ());
/// }
/// assert!(!map.is_inline());
/// assert_eq!(map.len(), 16);
///
/// map.clear();
/// assert!(map.is_inline());
/// assert_eq!(map.len(), 0);
/// ```
#[inline]
pub fn clear(&mut self) {
let hash_builder = match self {
// Too bad there's no HashMap::take_hasher()
SmallMap::Heap(inner) => inner.hasher().clone(),
// Safety: We're about to destroy this inner, so it doesn't need its hasher
SmallMap::Inline(inner) => unsafe { inner.take_hasher() },
};
*self = Self::Inline(Inline::new(hash_builder))
}
}

pub enum Iter<'a, const N: usize, K, V> {
Heap(hashbrown::hash_map::Iter<'a, K, V>),
Inline(inline::Iter<'a, N, K, V>),
Expand Down Expand Up @@ -408,6 +445,14 @@ mod tests {
assert_eq!(map.remove("hello2").unwrap(), "world2".to_string());
assert_eq!(map.len(), 0);
assert!(map.get("hello").is_none());

map.insert("hello3".to_string(), "world3".to_string());
map.insert("hello4".to_string(), "world4".to_string());
assert_eq!(map.len(), 2);
map.clear();
assert_eq!(map.len(), 0);
assert!(map.get("hello3").is_none());
assert!(map.get("hello4").is_none());
}

#[test]
Expand All @@ -430,10 +475,21 @@ mod tests {
}
}

#[test]
fn clear_to_inline() {
let mut map = SmallMap::<16, i32, i32>::default();
for i in 0..32 {
map.insert(i, i * 2);
}
assert!(!map.is_inline());
map.clear();
assert!(map.is_inline());
}

#[test]
fn fuzzing() {
let mut smallmap = SmallMap::<16, i32, i32>::default();
let mut hashmap = HashMap::<i32, i32, RandomState>::default();
let mut hashmap = HashMap::<i32, i32, RandomState>::with_hasher(RandomState::new());
for _ in 0..1000000 {
let op = Operation::random();
op.exec(&mut smallmap, &mut hashmap);
Expand All @@ -447,14 +503,14 @@ mod tests {
}
impl Operation {
fn random() -> Self {
let mut rng = rand::thread_rng();
let mut rng = rand::rng();

let choice: u8 = rng.gen();
let choice: u8 = rng.random();
match choice % 4 {
0 => Operation::Insert(rng.gen_range(0..32), rng.gen()),
1 => Operation::Remove(rng.gen_range(0..32)),
2 => Operation::Get(rng.gen_range(0..32)),
3 => Operation::ModifyIfExist(rng.gen_range(0..32), rng.gen()),
0 => Operation::Insert(rng.random_range(0..32), rng.random()),
1 => Operation::Remove(rng.random_range(0..32)),
2 => Operation::Get(rng.random_range(0..32)),
3 => Operation::ModifyIfExist(rng.random_range(0..32), rng.random()),
_ => unreachable!(),
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/raw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ impl<T> RawIterInner<T> {
unsafe fn next_impl(&mut self, group_base: NonNull<u8>, base: Bucket<T>) -> Bucket<T> {
loop {
if let Some(index) = self.current_group.next() {
return base.next_n(index + self.group_offset);
return unsafe { base.next_n(index + self.group_offset) };
}

self.group_offset += Group::WIDTH;
self.current_group = Group::load_aligned(group_base.as_ptr().add(self.group_offset))
self.current_group = unsafe { Group::load_aligned(group_base.as_ptr().add(self.group_offset)) }
.match_full()
.into_iter();
}
Expand Down Expand Up @@ -126,7 +126,7 @@ impl<T> RawIterInner<T> {
pub(crate) unsafe fn drop_elements(&mut self, group_base: NonNull<u8>, base: Bucket<T>) {
if T::NEEDS_DROP && self.len != 0 {
while let Some(item) = self.next(group_base, base.clone()) {
item.drop();
unsafe { item.drop(); }
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/raw/sse2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Group {
#[inline]
#[allow(clippy::cast_ptr_alignment)] // unaligned load
pub(crate) unsafe fn load(ptr: *const u8) -> Self {
Group(x86::_mm_loadu_si128(ptr.cast()))
Group(unsafe { x86::_mm_loadu_si128(ptr.cast()) })
}

/// Loads a group of bytes starting at the given address, which must be
Expand All @@ -58,7 +58,7 @@ impl Group {
pub(crate) unsafe fn load_aligned(ptr: *const u8) -> Self {
// FIXME: use align_offset once it stabilizes
debug_assert_eq!(ptr as usize & (mem::align_of::<Self>() - 1), 0);
Group(x86::_mm_load_si128(ptr.cast()))
Group(unsafe { x86::_mm_load_si128(ptr.cast()) })
}

/// Returns a `BitMask` indicating all bytes in the group which have
Expand Down
Loading