Skip to content
Merged
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
36 changes: 28 additions & 8 deletions crates/iddqd/src/bi_hash_map/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,8 +758,14 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> BiHashMap<T, S, A> {
/// ```
pub fn reserve(&mut self, additional: usize) {
self.items.reserve(additional);
self.tables.k1_to_item.reserve(additional);
self.tables.k2_to_item.reserve(additional);
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.reserve(additional, |ix| state.hash_one(items[*ix].key1()));
self.tables
.k2_to_item
.reserve(additional, |ix| state.hash_one(items[*ix].key2()));
}

/// Tries to reserve capacity for at least `additional` more elements to be
Expand Down Expand Up @@ -815,13 +821,15 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> BiHashMap<T, S, A> {
self.items
.try_reserve(additional)
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.try_reserve(additional)
.try_reserve(additional, |ix| state.hash_one(items[*ix].key1()))
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
self.tables
.k2_to_item
.try_reserve(additional)
.try_reserve(additional, |ix| state.hash_one(items[*ix].key2()))
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
Ok(())
}
Expand Down Expand Up @@ -864,8 +872,14 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> BiHashMap<T, S, A> {
/// ```
pub fn shrink_to_fit(&mut self) {
self.items.shrink_to_fit();
self.tables.k1_to_item.shrink_to_fit();
self.tables.k2_to_item.shrink_to_fit();
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.shrink_to_fit(|ix| state.hash_one(items[*ix].key1()));
self.tables
.k2_to_item
.shrink_to_fit(|ix| state.hash_one(items[*ix].key2()));
}

/// Shrinks the capacity of the map with a lower limit. It will drop
Expand Down Expand Up @@ -911,8 +925,14 @@ impl<T: BiHashItem, S: Clone + BuildHasher, A: Allocator> BiHashMap<T, S, A> {
/// ```
pub fn shrink_to(&mut self, min_capacity: usize) {
self.items.shrink_to(min_capacity);
self.tables.k1_to_item.shrink_to(min_capacity);
self.tables.k2_to_item.shrink_to(min_capacity);
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key1()));
self.tables
.k2_to_item
.shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key2()));
}

/// Returns an iterator over all items in the map.
Expand Down
22 changes: 18 additions & 4 deletions crates/iddqd/src/id_hash_map/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,11 @@ impl<T: IdHashItem, S: Clone + BuildHasher, A: Allocator> IdHashMap<T, S, A> {
/// ```
pub fn reserve(&mut self, additional: usize) {
self.items.reserve(additional);
self.tables.key_to_item.reserve(additional);
let items = &self.items;
let state = &self.tables.state;
self.tables
.key_to_item
.reserve(additional, |ix| state.hash_one(items[*ix].key()));
}

/// Tries to reserve capacity for at least `additional` more elements to be
Expand Down Expand Up @@ -693,9 +697,11 @@ impl<T: IdHashItem, S: Clone + BuildHasher, A: Allocator> IdHashMap<T, S, A> {
self.items
.try_reserve(additional)
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
let items = &self.items;
let state = &self.tables.state;
self.tables
.key_to_item
.try_reserve(additional)
.try_reserve(additional, |ix| state.hash_one(items[*ix].key()))
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
Ok(())
}
Expand Down Expand Up @@ -734,7 +740,11 @@ impl<T: IdHashItem, S: Clone + BuildHasher, A: Allocator> IdHashMap<T, S, A> {
/// ```
pub fn shrink_to_fit(&mut self) {
self.items.shrink_to_fit();
self.tables.key_to_item.shrink_to_fit();
let items = &self.items;
let state = &self.tables.state;
self.tables
.key_to_item
.shrink_to_fit(|ix| state.hash_one(items[*ix].key()));
}

/// Shrinks the capacity of the map with a lower limit. It will drop
Expand Down Expand Up @@ -776,7 +786,11 @@ impl<T: IdHashItem, S: Clone + BuildHasher, A: Allocator> IdHashMap<T, S, A> {
/// ```
pub fn shrink_to(&mut self, min_capacity: usize) {
self.items.shrink_to(min_capacity);
self.tables.key_to_item.shrink_to(min_capacity);
let items = &self.items;
let state = &self.tables.state;
self.tables
.key_to_item
.shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key()));
}

/// Iterates over the items in the map.
Expand Down
35 changes: 28 additions & 7 deletions crates/iddqd/src/support/hash_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,29 +173,50 @@ impl<A: Allocator> MapHashTable<A> {
}

/// Reserves capacity for at least `additional` more items.
///
/// `hasher` is invoked for every entry that hashbrown has to re-slot during
/// a growth rehash, and must return the same hash that was used to insert
/// that entry originally. Anything else silently corrupts the table: all
/// surviving entries land in the same bucket, and subsequent lookups —
/// which probe using the real key hash — miss.
#[inline]
pub(crate) fn reserve(&mut self, additional: usize) {
self.items.reserve(additional, |_| 0);
pub(crate) fn reserve(
&mut self,
additional: usize,
hasher: impl Fn(&usize) -> u64,
) {
self.items.reserve(additional, hasher);
}

/// Shrinks the capacity of the hash table as much as possible.
///
/// See [`Self::reserve`] for the contract `hasher` must satisfy.
#[inline]
pub(crate) fn shrink_to_fit(&mut self) {
self.items.shrink_to_fit(|_| 0);
pub(crate) fn shrink_to_fit(&mut self, hasher: impl Fn(&usize) -> u64) {
self.items.shrink_to_fit(hasher);
}

/// Shrinks the capacity of the hash table with a lower limit.
///
/// See [`Self::reserve`] for the contract `hasher` must satisfy.
#[inline]
pub(crate) fn shrink_to(&mut self, min_capacity: usize) {
self.items.shrink_to(min_capacity, |_| 0);
pub(crate) fn shrink_to(
&mut self,
min_capacity: usize,
hasher: impl Fn(&usize) -> u64,
) {
self.items.shrink_to(min_capacity, hasher);
}

/// Tries to reserve capacity for at least `additional` more items.
///
/// See [`Self::reserve`] for the contract `hasher` must satisfy.
#[inline]
pub(crate) fn try_reserve(
&mut self,
additional: usize,
hasher: impl Fn(&usize) -> u64,
) -> Result<(), hashbrown::TryReserveError> {
self.items.try_reserve(additional, |_| 0)
self.items.try_reserve(additional, hasher)
}
}
50 changes: 38 additions & 12 deletions crates/iddqd/src/tri_hash_map/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,9 +854,17 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
/// ```
pub fn reserve(&mut self, additional: usize) {
self.items.reserve(additional);
self.tables.k1_to_item.reserve(additional);
self.tables.k2_to_item.reserve(additional);
self.tables.k3_to_item.reserve(additional);
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.reserve(additional, |ix| state.hash_one(items[*ix].key1()));
self.tables
.k2_to_item
.reserve(additional, |ix| state.hash_one(items[*ix].key2()));
self.tables
.k3_to_item
.reserve(additional, |ix| state.hash_one(items[*ix].key3()));
}

/// Tries to reserve capacity for at least `additional` more elements to be
Expand Down Expand Up @@ -917,17 +925,19 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
self.items
.try_reserve(additional)
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.try_reserve(additional)
.try_reserve(additional, |ix| state.hash_one(items[*ix].key1()))
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
self.tables
.k2_to_item
.try_reserve(additional)
.try_reserve(additional, |ix| state.hash_one(items[*ix].key2()))
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
self.tables
.k3_to_item
.try_reserve(additional)
.try_reserve(additional, |ix| state.hash_one(items[*ix].key3()))
.map_err(crate::errors::TryReserveError::from_hashbrown)?;
Ok(())
}
Expand Down Expand Up @@ -985,9 +995,17 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
/// ```
pub fn shrink_to_fit(&mut self) {
self.items.shrink_to_fit();
self.tables.k1_to_item.shrink_to_fit();
self.tables.k2_to_item.shrink_to_fit();
self.tables.k3_to_item.shrink_to_fit();
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.shrink_to_fit(|ix| state.hash_one(items[*ix].key1()));
self.tables
.k2_to_item
.shrink_to_fit(|ix| state.hash_one(items[*ix].key2()));
self.tables
.k3_to_item
.shrink_to_fit(|ix| state.hash_one(items[*ix].key3()));
}

/// Shrinks the capacity of the map with a lower limit. It will drop
Expand Down Expand Up @@ -1048,9 +1066,17 @@ impl<T: TriHashItem, S: Clone + BuildHasher, A: Allocator> TriHashMap<T, S, A> {
/// ```
pub fn shrink_to(&mut self, min_capacity: usize) {
self.items.shrink_to(min_capacity);
self.tables.k1_to_item.shrink_to(min_capacity);
self.tables.k2_to_item.shrink_to(min_capacity);
self.tables.k3_to_item.shrink_to(min_capacity);
let items = &self.items;
let state = &self.tables.state;
self.tables
.k1_to_item
.shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key1()));
self.tables
.k2_to_item
.shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key2()));
self.tables
.k3_to_item
.shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key3()));
}

/// Iterates over the items in the map.
Expand Down
42 changes: 41 additions & 1 deletion crates/iddqd/tests/integration/bi_hash_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,29 @@ enum Operation {
#[weight(2)]
RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool),
Clear,
// `additional` is kept modest so that reservations frequently
// exceed the current `growth_left` and so trigger hashbrown's
// rehash path.
Reserve(#[strategy(0..256_usize)] usize),
TryReserve(#[strategy(0..256_usize)] usize),
ShrinkToFit,
ShrinkTo(#[strategy(0..256_usize)] usize),
}

impl Operation {
fn compactness_change(&self) -> CompactnessChange {
match self {
// `shrink_to_fit` / `shrink_to` flow through hashbrown's
// rehash path the same way `reserve` does; like `reserve`
// they touch the allocation, not the item set's index
// space, so compactness is unchanged.
Operation::InsertUnique(_)
| Operation::Get1(_)
| Operation::Get2(_) => CompactnessChange::NoChange,
| Operation::Get2(_)
| Operation::Reserve(_)
| Operation::TryReserve(_)
| Operation::ShrinkToFit
| Operation::ShrinkTo(_) => CompactnessChange::NoChange,
// The act of removing items, including calls to insert_overwrite,
// can make the map non-compact.
Operation::InsertOverwrite(_)
Expand Down Expand Up @@ -366,6 +381,31 @@ fn proptest_ops(
});
map.validate(compactness).expect("map should be valid");
}
Operation::Reserve(additional) => {
map.reserve(additional);
// `reserve` has no observable effect beyond capacity; the
// naive map has no equivalent. `validate` is the real
// check — it iterates items and asks `find_index` for
// each, which catches a hash-table left mis-bucketed by
// a regrowth rehash.
map.validate(compactness).expect("map should be valid");
}
Operation::TryReserve(additional) => {
// Mirror `Reserve`; we don't assert `Ok` because the
// allocator could (legitimately) refuse a large request,
// and bailing on that would mask the actual regression
// we care about (silent hash-table corruption).
let _ = map.try_reserve(additional);
map.validate(compactness).expect("map should be valid");
}
Operation::ShrinkToFit => {
map.shrink_to_fit();
map.validate(compactness).expect("map should be valid");
}
Operation::ShrinkTo(min_capacity) => {
map.shrink_to(min_capacity);
map.validate(compactness).expect("map should be valid");
}
Operation::Clear => {
map.clear();
naive_map.clear();
Expand Down
45 changes: 42 additions & 3 deletions crates/iddqd/tests/integration/id_hash_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,28 @@ enum Operation {
#[weight(2)]
RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool),
Clear,
// `additional` is kept modest so that reservations frequently
// exceed the current `growth_left` and so trigger hashbrown's
// rehash path.
Reserve(#[strategy(0..256_usize)] usize),
TryReserve(#[strategy(0..256_usize)] usize),
ShrinkToFit,
ShrinkTo(#[strategy(0..256_usize)] usize),
}

impl Operation {
fn compactness_change(&self) -> CompactnessChange {
match self {
Operation::InsertUnique(_) | Operation::Get(_) => {
CompactnessChange::NoChange
}
// `shrink_to_fit` / `shrink_to` flow through hashbrown's
// rehash path the same way `reserve` does; like `reserve`
// they touch the allocation, not the item set's index
// space, so compactness is unchanged.
Operation::InsertUnique(_)
| Operation::Get(_)
| Operation::Reserve(_)
| Operation::TryReserve(_)
| Operation::ShrinkToFit
| Operation::ShrinkTo(_) => CompactnessChange::NoChange,
// The act of removing items, including calls to insert_overwrite,
// can make the map non-compact.
Operation::InsertOverwrite(_)
Expand Down Expand Up @@ -301,6 +315,31 @@ fn proptest_ops(
naive_map.clear();
map.validate(compactness).expect("map should be valid");
}
Operation::Reserve(additional) => {
map.reserve(additional);
// `reserve` has no observable effect beyond capacity; the
// naive map has no equivalent. `validate` is the real
// check — it iterates items and asks `find_index` for
// each, which catches a hash-table left mis-bucketed by
// a regrowth rehash.
map.validate(compactness).expect("map should be valid");
}
Operation::TryReserve(additional) => {
// Mirror `Reserve`; we don't assert `Ok` because the
// allocator could (legitimately) refuse a large request,
// and bailing on that would mask the actual regression
// we care about (silent hash-table corruption).
let _ = map.try_reserve(additional);
map.validate(compactness).expect("map should be valid");
}
Operation::ShrinkToFit => {
map.shrink_to_fit();
map.validate(compactness).expect("map should be valid");
}
Operation::ShrinkTo(min_capacity) => {
map.shrink_to(min_capacity);
map.validate(compactness).expect("map should be valid");
}
}

// Check that the iterators work correctly.
Expand Down
Loading
Loading