use std::hash::{BuildHasher, Hash};
use std::mem::{self, MaybeUninit};
use allocator_api2::alloc::{Allocator, Global, Layout};
use equivalent::Equivalent;
use crate::common::DefaultHashBuilder;
use crate::common::arena::{self, Arena, ArenaSlots, SlotEntry};
use crate::common::config::{GROUP_SIZE, GROUP_SIZE_U32, INITIAL_CAPACITY};
use crate::common::control::{self, CTRL_EMPTY, CTRL_TOMBSTONE};
use crate::common::error::TryReserveError;
use crate::common::iter::RegionCursor;
use crate::common::math::{self, align, capacity, probe};
use crate::map::{self, RawTable};
use crate::set;
type ElasticScanItem<K, V> = (*mut SlotEntry<K, V>, (usize, usize));
const DEFRAG_OPS_DIVISOR: usize = 4;
const DEFRAG_DRIFT_MULT: f64 = 3.0;
struct Level<T> {
ctrl_ptr: *mut u8,
data_ptr: *mut MaybeUninit<T>,
capacity: u32,
group_count: u32,
group_count_mask: u32,
max_probe_groups: u32,
len: u32,
tombstones: u32,
half_reserve_slot_threshold: u32,
probe_drift_threshold: u32,
salt: u64,
budget_cap: f64,
}
unsafe impl<T: Send> Send for Level<T> {}
unsafe impl<T: Sync> Sync for Level<T> {}
const _: () = assert!(mem::size_of::<Level<SlotEntry<u64, u64>>>() <= 64);
impl<T> ArenaSlots<T> for Level<T> {
#[inline]
fn ctrl_ptr(&self) -> *mut u8 {
self.ctrl_ptr
}
#[inline]
fn data_ptr(&self) -> *mut MaybeUninit<T> {
self.data_ptr
}
#[inline]
fn capacity(&self) -> usize {
self.capacity as usize
}
}
impl<T> Level<T> {
fn new_at(
level_idx: usize,
cap_u32: u32,
reserve_fraction: f64,
ctrl_ptr: *mut u8,
data_ptr: *mut MaybeUninit<T>,
) -> Self {
let cap = cap_u32 as usize;
let gc = cap_u32 / GROUP_SIZE_U32;
let budget_cap = compute_budget_cap(reserve_fraction, gc as usize);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let probe_drift_threshold = (DEFRAG_DRIFT_MULT * budget_cap) as u32;
Self {
ctrl_ptr,
data_ptr,
capacity: cap_u32,
group_count: gc,
group_count_mask: gc.wrapping_sub(1),
max_probe_groups: 0,
salt: math::level_salt_wide(level_idx),
len: 0,
tombstones: 0,
half_reserve_slot_threshold: u32::try_from(capacity::floor_half_reserve_slots(
reserve_fraction,
cap,
))
.unwrap_or(u32::MAX),
probe_drift_threshold,
budget_cap,
}
}
#[inline]
fn group_count(&self) -> usize {
self.group_count as usize
}
#[inline]
fn free_slots(&self) -> usize {
self.capacity.saturating_sub(self.len) as usize
}
#[inline]
#[allow(
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_truncation
)]
fn limited_group_budget(&self) -> usize {
let cap = self.capacity as usize;
let free = self.free_slots();
if cap == 0 || free == 0 {
return 1;
}
let log_inv_eps = (cap as f64 / free as f64).log2();
let raw = 1.0 + log_inv_eps * log_inv_eps;
raw.min(self.budget_cap) as usize
}
#[inline]
fn needs_cleanup(&self) -> bool {
self.tombstones as usize > capacity::tombstone_cleanup_threshold(self.capacity as usize)
}
#[inline]
fn probe_drifted(&self) -> bool {
self.max_probe_groups > self.probe_drift_threshold
}
#[inline]
fn triangular_group_start(&self, key_hash: u64) -> usize {
let mixed = key_hash ^ self.salt;
probe::hash_to_usize(mixed) & self.group_count_mask as usize
}
#[inline]
fn note_placement(&mut self, key_hash: u64, slot_idx: usize) {
let target_group = slot_idx / GROUP_SIZE;
let mask = self.group_count_mask as usize;
let mut probe = probe::TriangularProbe::new(self.triangular_group_start(key_hash));
let mut steps = 0u32;
while probe.pos != target_group {
probe.advance(mask);
steps += 1;
}
if steps + 1 > self.max_probe_groups {
self.max_probe_groups = steps + 1;
}
}
}
impl<K, V> Level<SlotEntry<K, V>> {
#[inline]
fn find_by_probe<Q>(&self, key_hash: u64, key_fingerprint: u8, key: &Q) -> Option<usize>
where
Q: Equivalent<K> + ?Sized,
{
if self.len == 0 {
return None;
}
let mask = self.group_count_mask as usize;
let mut probe = probe::TriangularProbe::new(self.triangular_group_start(key_hash));
for _ in 0..self.max_probe_groups as usize {
let match_mask = self.group_match_mask(probe.pos, key_fingerprint);
for relative_idx in match_mask {
let slot_idx = probe.pos * GROUP_SIZE + relative_idx;
let entry = unsafe { self.get_ref(slot_idx) };
if key.equivalent(&entry.key) {
return Some(slot_idx);
}
}
if self.group_match_mask(probe.pos, CTRL_EMPTY).any() {
return None;
}
probe.advance(mask);
}
None
}
}
pub struct ElasticTable<K, V, S = DefaultHashBuilder, A: Allocator + Clone = Global> {
levels: LevelSlice<K, V>,
len: usize,
total_slots: usize,
max_insertions: usize,
reserve_fraction: f64,
batch_plan: Box<[usize]>,
current_batch_index: usize,
batch_remaining: usize,
max_populated_level: usize,
defrag_pending: bool,
inserts_since_repack: usize,
hash_builder: S,
alloc: A,
arena: Arena,
}
unsafe impl<K: Send, V: Send, S: Send, A: Allocator + Clone + Send> Send
for ElasticTable<K, V, S, A>
{
}
unsafe impl<K: Sync, V: Sync, S: Sync, A: Allocator + Clone + Sync> Sync
for ElasticTable<K, V, S, A>
{
}
impl<K, V, S, A: Allocator + Clone> Drop for ElasticTable<K, V, S, A> {
fn drop(&mut self) {
let arena = mem::replace(&mut self.arena, Arena::empty());
let guard = arena::DeallocGuard::new(arena, &self.alloc);
for level in &mut self.levels {
level.drop_values();
}
drop(guard);
}
}
pub type ElasticHashMap<K, V, S = DefaultHashBuilder, A = Global> =
map::HashMap<K, V, ElasticTable<K, V, S, A>>;
pub type ElasticEntry<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::Entry<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticOccupiedEntry<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::OccupiedEntry<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticVacantEntry<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::VacantEntry<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticOccupiedError<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::OccupiedError<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticIter<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::Iter<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticIterMut<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::IterMut<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticIntoIter<K, V, S = DefaultHashBuilder, A = Global> =
map::IntoIter<K, V, ElasticTable<K, V, S, A>>;
pub type ElasticKeys<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::Keys<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticValues<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::Values<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticValuesMut<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::ValuesMut<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticIntoKeys<K, V, S = DefaultHashBuilder, A = Global> =
map::IntoKeys<K, V, ElasticTable<K, V, S, A>>;
pub type ElasticIntoValues<K, V, S = DefaultHashBuilder, A = Global> =
map::IntoValues<K, V, ElasticTable<K, V, S, A>>;
pub type ElasticDrain<'a, K, V, S = DefaultHashBuilder, A = Global> =
map::Drain<'a, K, V, ElasticTable<K, V, S, A>>;
pub type ElasticExtractIf<'a, K, V, F, S = DefaultHashBuilder, A = Global> =
map::ExtractIf<'a, K, V, ElasticTable<K, V, S, A>, F>;
pub type ElasticHashSet<T, S = DefaultHashBuilder, A = Global> =
set::HashSet<T, ElasticTable<T, (), S, A>>;
pub type ElasticSetIter<'a, T, S = DefaultHashBuilder, A = Global> =
set::Iter<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticSetIntoIter<T, S = DefaultHashBuilder, A = Global> =
set::IntoIter<T, ElasticTable<T, (), S, A>>;
pub type ElasticSetDrain<'a, T, S = DefaultHashBuilder, A = Global> =
set::Drain<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticSetExtractIf<'a, T, S = DefaultHashBuilder, A = Global> =
set::ExtractIf<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticDifference<'a, T, S = DefaultHashBuilder, A = Global> =
set::Difference<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticIntersection<'a, T, S = DefaultHashBuilder, A = Global> =
set::Intersection<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticSymmetricDifference<'a, T, S = DefaultHashBuilder, A = Global> =
set::SymmetricDifference<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticUnion<'a, T, S = DefaultHashBuilder, A = Global> =
set::Union<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticSetEntry<'a, T, S = DefaultHashBuilder, A = Global> =
set::Entry<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticSetOccupiedEntry<'a, T, S = DefaultHashBuilder, A = Global> =
set::OccupiedEntry<'a, T, ElasticTable<T, (), S, A>>;
pub type ElasticSetVacantEntry<'a, T, S = DefaultHashBuilder, A = Global> =
set::VacantEntry<'a, T, ElasticTable<T, (), S, A>>;
type LevelSlice<K, V> = Box<[Level<SlotEntry<K, V>>]>;
type ElasticArenaBuild<K, V> = (Arena, LevelSlice<K, V>);
struct ElasticGeometry {
total_slots: usize,
max_insertions: usize,
level_capacities: Vec<usize>,
batch_plan: Box<[usize]>,
initial_batch_remaining: usize,
}
impl ElasticGeometry {
fn for_insert_budget(requested_insertions: usize, reserve_fraction: f64) -> Option<Self> {
let total_slots = if requested_insertions == 0 {
0
} else {
capacity::capacity_for(INITIAL_CAPACITY, requested_insertions, reserve_fraction)?
};
Some(Self::for_slots(total_slots, reserve_fraction))
}
fn for_slots(total_slots: usize, reserve_fraction: f64) -> Self {
let max_insertions = capacity::max_insertions(total_slots, reserve_fraction);
let level_capacities = partition_levels(total_slots);
let batch_plan = build_batch_plan(&level_capacities, reserve_fraction, max_insertions);
let initial_batch_remaining = batch_plan.first().copied().unwrap_or(0);
Self {
total_slots,
max_insertions,
level_capacities,
batch_plan,
initial_batch_remaining,
}
}
}
fn build_elastic_levels<K, V>(
arena_base: *mut u8,
data_base_off: usize,
level_capacities: &[usize],
reserve_fraction: f64,
) -> Result<LevelSlice<K, V>, TryReserveError> {
let mut cursor = arena::LayoutCursor::<SlotEntry<K, V>>::new(arena_base, data_base_off)?;
let mut levels: Vec<Level<SlotEntry<K, V>>> = Vec::new();
levels
.try_reserve_exact(level_capacities.len())
.map_err(|_| TryReserveError::AllocError)?;
for (level_idx, &cap) in level_capacities.iter().enumerate() {
let cap_u32 = u32::try_from(cap).map_err(|_| TryReserveError::CapacityOverflow)?;
let (ctrl_ptr, data_ptr) = unsafe { cursor.reserve(cap_u32)? };
levels.push(Level::new_at(
level_idx,
cap_u32,
reserve_fraction,
ctrl_ptr,
data_ptr,
));
}
Ok(levels.into_boxed_slice())
}
fn try_alloc_elastic_arena<K, V, A: Allocator + Clone>(
level_capacities: &[usize],
reserve_fraction: f64,
alloc: &A,
) -> Result<ElasticArenaBuild<K, V>, TryReserveError> {
let total_ctrl: usize = level_capacities.iter().sum();
let (arena_layout, data_base_off) = arena::layout_for::<K, V>(total_ctrl)?;
let arena = Arena::try_allocate_with_ctrl_zeroed(arena_layout, total_ctrl, alloc)?;
match build_elastic_levels::<K, V>(
arena.as_ptr(),
data_base_off,
level_capacities,
reserve_fraction,
) {
Ok(levels) => Ok((arena, levels)),
Err(e) => {
arena.deallocate(alloc);
Err(e)
}
}
}
fn alloc_elastic_arena<K, V, A: Allocator + Clone>(
level_capacities: &[usize],
reserve_fraction: f64,
alloc: &A,
) -> ElasticArenaBuild<K, V> {
try_alloc_elastic_arena(level_capacities, reserve_fraction, alloc).unwrap_or_else(|_| {
let total_ctrl: usize = level_capacities.iter().sum();
let layout = match arena::layout_for::<K, V>(total_ctrl) {
Ok((l, _)) => l,
Err(_) => Layout::from_size_align(1, 1).unwrap(),
};
allocator_api2::alloc::handle_alloc_error(layout)
})
}
impl<K, V> arena::RegionSet for LevelSlice<K, V> {
fn drop_all_values(&mut self) {
for level in self.iter_mut() {
level.drop_values();
}
}
}
impl<K, V, S, A> ElasticTable<K, V, S, A>
where
K: Eq + Hash,
S: BuildHasher,
A: Allocator + Clone,
{
#[must_use]
pub fn with_capacity_and_reserve_fraction_and_hasher_in(
capacity: usize,
reserve_fraction: f64,
hash_builder: S,
alloc: A,
) -> Self {
let reserve_fraction = capacity::sanitize_reserve_fraction(reserve_fraction);
let geometry = ElasticGeometry::for_insert_budget(capacity, reserve_fraction)
.expect("capacity overflow");
let (arena, levels) =
alloc_elastic_arena(&geometry.level_capacities, reserve_fraction, &alloc);
Self {
levels,
len: 0,
total_slots: geometry.total_slots,
max_insertions: geometry.max_insertions,
reserve_fraction,
batch_plan: geometry.batch_plan,
current_batch_index: 0,
batch_remaining: geometry.initial_batch_remaining,
max_populated_level: 0,
defrag_pending: false,
inserts_since_repack: 0,
hash_builder,
alloc,
arena,
}
}
fn grow_capacity_for(&self, needed: usize) -> Option<usize> {
capacity::capacity_for(
self.total_slots.max(INITIAL_CAPACITY),
needed,
self.reserve_fraction,
)
}
fn clear(&mut self) {
for level in &mut self.levels {
level.drop_values();
level.clear_all_controls();
level.len = 0;
level.tombstones = 0;
level.max_probe_groups = 0;
}
self.len = 0;
self.current_batch_index = 0;
self.batch_remaining = self.batch_plan.first().copied().unwrap_or(0);
self.max_populated_level = 0;
self.defrag_pending = false;
self.inserts_since_repack = 0;
}
fn insert_for_vacant_entry(&mut self, key: K, value: V, key_hash: u64) -> (usize, usize) {
let key_fingerprint = control::control_fingerprint(key_hash);
self.inserts_since_repack += 1;
if self.len >= self.max_insertions {
let new_capacity = if self.total_slots == 0 {
INITIAL_CAPACITY
} else {
self.total_slots.saturating_mul(2)
};
self.resize(new_capacity);
} else if self.defrag_pending
&& self.inserts_since_repack > (self.total_slots / DEFRAG_OPS_DIVISOR).max(1)
{
self.resize(self.total_slots);
}
self.advance_batch_window();
let (level_idx, slot_idx) = self
.choose_slot_for_new_key(key_hash)
.expect("no free slot found after resize");
let level = &mut self.levels[level_idx];
let prev_ctrl = level.control_at(slot_idx);
level.write_with_control(slot_idx, SlotEntry { key, value }, key_fingerprint);
level.note_placement(key_hash, slot_idx);
level.len += 1;
if prev_ctrl == CTRL_TOMBSTONE {
level.tombstones -= 1;
}
let drifted = level.probe_drifted();
if drifted {
self.defrag_pending = true;
}
if level_idx > self.max_populated_level {
self.max_populated_level = level_idx;
}
self.len += 1;
if self.batch_remaining > 0 {
self.batch_remaining -= 1;
}
(level_idx, slot_idx)
}
#[inline]
unsafe fn slot_ptr_at(&self, level_idx: usize, slot_idx: usize) -> *mut SlotEntry<K, V> {
let levels_ptr: *const Level<SlotEntry<K, V>> = self.levels.as_ptr();
let level = unsafe { &*levels_ptr.add(level_idx) };
level.slot_ptr(slot_idx)
}
fn take_and_tombstone(&mut self, level_idx: usize, slot_idx: usize) -> (K, V) {
let removed = {
let level = &mut self.levels[level_idx];
let removed = unsafe { level.take(slot_idx) };
level.mark_tombstone(slot_idx);
level.len -= 1;
level.tombstones += 1;
removed
};
self.len -= 1;
(removed.key, removed.value)
}
#[cold]
fn scan_advance(&self, scan: &mut ElasticScan) -> Option<ElasticScanItem<K, V>> {
if !scan.region.started() {
if self.levels.is_empty() {
return None;
}
scan.region.enter(&self.levels[0]);
}
loop {
if let Some((ptr, slot_idx)) = scan.region.step::<SlotEntry<K, V>>() {
return Some((ptr, (scan.level_idx, slot_idx)));
}
scan.level_idx += 1;
if scan.level_idx >= self.levels.len() {
return None;
}
scan.region.enter(&self.levels[scan.level_idx]);
}
}
}
#[allow(private_interfaces)]
impl<K, V, S, A> RawTable<K, V> for ElasticTable<K, V, S, A>
where
K: Eq + Hash,
S: BuildHasher,
A: Allocator + Clone,
{
type Location = (usize, usize);
type Hasher = S;
type Alloc = A;
type Scan = ElasticScan;
#[inline]
fn with_capacity_and_reserve_fraction_and_hasher_in(
capacity: usize,
reserve_fraction: f64,
hash_builder: S,
alloc: A,
) -> Self {
Self::with_capacity_and_reserve_fraction_and_hasher_in(
capacity,
reserve_fraction,
hash_builder,
alloc,
)
}
#[inline]
fn hasher(&self) -> &S {
&self.hash_builder
}
#[inline]
fn allocator(&self) -> &A {
&self.alloc
}
#[inline]
fn len(&self) -> usize {
self.len
}
#[inline]
fn capacity(&self) -> usize {
self.max_insertions
}
#[inline]
fn total_slots(&self) -> usize {
self.total_slots
}
#[inline]
fn reserve_fraction(&self) -> f64 {
self.reserve_fraction
}
#[inline]
fn grow_capacity_for(&self, needed: usize) -> Option<usize> {
self.grow_capacity_for(needed)
}
#[inline]
fn resize(&mut self, new_capacity: usize) {
self.resize(new_capacity);
}
#[inline]
fn try_resize(&mut self, new_capacity: usize) -> Result<(), TryReserveError>
where
S: Clone,
{
self.try_resize(new_capacity)
}
#[inline]
fn clear(&mut self) {
self.clear();
}
#[inline]
fn find<Q>(&self, key: &Q, hash: u64, fingerprint: u8) -> Option<(usize, usize)>
where
Q: Hash + Equivalent<K> + ?Sized,
{
self.find_slot_indices_with_hash(key, hash, fingerprint)
}
#[inline]
unsafe fn slot_ref(&self, (level_idx, slot_idx): (usize, usize)) -> &SlotEntry<K, V> {
unsafe { self.slot_ref(level_idx, slot_idx) }
}
#[inline]
unsafe fn slot_ptr(&self, (level_idx, slot_idx): (usize, usize)) -> *mut SlotEntry<K, V> {
unsafe { self.slot_ptr_at(level_idx, slot_idx) }
}
#[inline]
fn replace_value(&mut self, (level_idx, slot_idx): (usize, usize), value: V) -> V {
let slot = unsafe { self.slot_mut(level_idx, slot_idx) };
mem::replace(&mut slot.value, value)
}
#[inline]
fn insert_for_vacant(&mut self, key: K, value: V, hash: u64) -> (usize, usize) {
self.insert_for_vacant_entry(key, value, hash)
}
fn remove(&mut self, (level_idx, slot_idx): (usize, usize)) -> (K, V) {
let kv = self.take_and_tombstone(level_idx, slot_idx);
let needs_resize = self.levels[level_idx].needs_cleanup();
self.shrink_max_populated_level();
if needs_resize {
self.resize(self.total_slots);
}
kv
}
#[inline]
fn tombstone_slot(&mut self, (level_idx, slot_idx): (usize, usize)) {
self.levels[level_idx].mark_tombstone(slot_idx);
}
#[inline]
fn extract_finish(&mut self, (level_idx, slot_idx): (usize, usize)) {
let level = &mut self.levels[level_idx];
level.mark_tombstone(slot_idx);
level.len -= 1;
level.tombstones += 1;
self.len -= 1;
}
#[inline]
fn scan(&self) -> ElasticScan {
ElasticScan {
level_idx: 0,
region: RegionCursor::new(),
}
}
#[inline]
fn scan_next(&self, scan: &mut ElasticScan) -> Option<ElasticScanItem<K, V>> {
if scan.region.started()
&& let Some((ptr, slot_idx)) = scan.region.step::<SlotEntry<K, V>>()
{
return Some((ptr, (scan.level_idx, slot_idx)));
}
self.scan_advance(scan)
}
fn wipe_all(&mut self) {
for level in &mut self.levels {
level.clear_all_controls();
level.len = 0;
level.tombstones = 0;
level.max_probe_groups = 0;
}
self.len = 0;
self.max_populated_level = 0;
self.defrag_pending = false;
self.inserts_since_repack = 0;
self.current_batch_index = 0;
self.batch_remaining = self.batch_plan.first().copied().unwrap_or(0);
}
fn clone_table(&self) -> Self
where
K: Clone,
V: Clone,
S: Clone,
{
let level_capacities: Vec<usize> =
self.levels.iter().map(|l| l.capacity as usize).collect();
let (arena, levels) =
alloc_elastic_arena(&level_capacities, self.reserve_fraction, &self.alloc);
let mut guard = arena::ArenaDropGuard::new(arena, levels, self.alloc.clone());
for (dst, src_lvl) in guard.regions_mut().iter_mut().zip(self.levels.iter()) {
arena::clone_region_panic_safe::<K, V>(
src_lvl.ctrl_ptr,
dst.ctrl_ptr,
src_lvl.data_ptr,
dst.data_ptr,
src_lvl.capacity as usize,
);
dst.len = src_lvl.len;
dst.tombstones = src_lvl.tombstones;
dst.max_probe_groups = src_lvl.max_probe_groups;
}
let (arena, levels) = guard.disarm();
Self {
levels,
len: self.len,
total_slots: self.total_slots,
max_insertions: self.max_insertions,
reserve_fraction: self.reserve_fraction,
batch_plan: self.batch_plan.clone(),
current_batch_index: self.current_batch_index,
batch_remaining: self.batch_remaining,
max_populated_level: self.max_populated_level,
defrag_pending: self.defrag_pending,
inserts_since_repack: self.inserts_since_repack,
hash_builder: self.hash_builder.clone(),
alloc: self.alloc.clone(),
arena,
}
}
}
#[derive(Clone)]
pub struct ElasticScan {
level_idx: usize,
region: RegionCursor,
}
impl<K, V, S, A> ElasticTable<K, V, S, A>
where
K: Eq + Hash,
S: BuildHasher,
A: Allocator + Clone,
{
#[inline]
fn insert_unique(&mut self, key: K, value: V) {
let key_hash = self.hash_key(&key);
let key_fingerprint = control::control_fingerprint(key_hash);
self.advance_batch_window();
let (level_idx, slot_idx) = self
.choose_slot_for_new_key(key_hash)
.expect("no free slot found in freshly-allocated map");
let level = &mut self.levels[level_idx];
level.write_with_control(slot_idx, SlotEntry { key, value }, key_fingerprint);
level.note_placement(key_hash, slot_idx);
level.len += 1;
if level_idx > self.max_populated_level {
self.max_populated_level = level_idx;
}
self.len += 1;
if self.batch_remaining > 0 {
self.batch_remaining -= 1;
}
}
fn resize(&mut self, new_capacity: usize) {
self.defrag_pending = false;
self.inserts_since_repack = 0;
let geometry = ElasticGeometry::for_slots(new_capacity, self.reserve_fraction);
let (new_arena, new_levels) = alloc_elastic_arena(
&geometry.level_capacities,
self.reserve_fraction,
&self.alloc,
);
let old_arena = mem::replace(&mut self.arena, new_arena);
let old_levels = mem::replace(&mut self.levels, new_levels);
self.total_slots = geometry.total_slots;
self.max_insertions = geometry.max_insertions;
self.batch_plan = geometry.batch_plan;
self.current_batch_index = 0;
self.batch_remaining = geometry.initial_batch_remaining;
self.max_populated_level = 0;
self.len = 0;
let mut guard = arena::ArenaDropGuard::new(old_arena, old_levels, self.alloc.clone());
for level in guard.regions_mut().iter_mut() {
level.drain_values_and_clear(|entry| {
self.insert_unique(entry.key, entry.value);
});
}
drop(guard);
}
fn try_resize(&mut self, new_capacity: usize) -> Result<(), TryReserveError>
where
S: Clone,
{
let hash_builder = self.hash_builder.clone();
let mut new_map = Self::try_with_slots_and_reserve_fraction_and_hasher_in(
new_capacity,
self.reserve_fraction,
hash_builder,
self.alloc.clone(),
)?;
for level in &mut self.levels {
level.drain_values_and_clear(|entry| {
new_map.insert_unique(entry.key, entry.value);
});
}
self.len = 0;
self.max_populated_level = 0;
*self = new_map;
Ok(())
}
fn try_with_slots_and_reserve_fraction_and_hasher_in(
slots: usize,
reserve_fraction: f64,
hash_builder: S,
alloc: A,
) -> Result<Self, TryReserveError> {
let reserve_fraction = capacity::sanitize_reserve_fraction(reserve_fraction);
let geometry = ElasticGeometry::for_slots(slots, reserve_fraction);
let (arena, levels) =
try_alloc_elastic_arena(&geometry.level_capacities, reserve_fraction, &alloc)?;
Ok(Self {
levels,
len: 0,
total_slots: geometry.total_slots,
max_insertions: geometry.max_insertions,
reserve_fraction,
batch_plan: geometry.batch_plan,
current_batch_index: 0,
batch_remaining: geometry.initial_batch_remaining,
max_populated_level: 0,
defrag_pending: false,
inserts_since_repack: 0,
hash_builder,
alloc,
arena,
})
}
#[inline]
fn hash_key<Q>(&self, key: &Q) -> u64
where
Q: Hash + ?Sized,
{
self.hash_builder.hash_one(key)
}
#[inline]
fn advance_batch_window(&mut self) {
while self.batch_remaining == 0 && self.current_batch_index + 1 < self.batch_plan.len() {
self.current_batch_index += 1;
self.batch_remaining = self.batch_plan[self.current_batch_index];
}
}
#[inline]
fn choose_slot_for_new_key(&mut self, key_hash: u64) -> Option<(usize, usize)> {
if self.levels.is_empty() {
return None;
}
if let Some(pair) = self.choose_slot_targeted(key_hash) {
return Some(pair);
}
for li in 0..self.levels.len() {
if let Some(slot_idx) = self.first_free_uniform(key_hash, li) {
return Some((li, slot_idx));
}
}
None
}
#[inline]
fn choose_slot_targeted(&self, key_hash: u64) -> Option<(usize, usize)> {
if self.current_batch_index == 0 {
return self
.first_free_uniform(key_hash, 0)
.map(|slot_idx| (0, slot_idx));
}
let level_idx = self.current_batch_index.saturating_sub(1);
if level_idx + 1 >= self.levels.len() {
let last = self.levels.len() - 1;
return self
.first_free_uniform(key_hash, last)
.map(|slot_idx| (last, slot_idx));
}
let current_level = &self.levels[level_idx];
let next_level = &self.levels[level_idx + 1];
let current_free_slots = current_level.free_slots();
let next_free_slots = next_level.free_slots();
if current_free_slots > current_level.half_reserve_slot_threshold as usize
&& next_free_slots.saturating_mul(4) > next_level.capacity()
{
let limited_budget = current_level.limited_group_budget();
if let Some(slot_idx) = self.first_free_limited(key_hash, level_idx, limited_budget) {
return Some((level_idx, slot_idx));
}
if let Some(slot_idx) = self.first_free_uniform(key_hash, level_idx + 1) {
return Some((level_idx + 1, slot_idx));
}
return self
.first_free_uniform(key_hash, level_idx)
.map(|slot_idx| (level_idx, slot_idx));
}
if current_free_slots <= current_level.half_reserve_slot_threshold as usize {
if let Some(slot_idx) = self.first_free_uniform(key_hash, level_idx + 1) {
return Some((level_idx + 1, slot_idx));
}
return self
.first_free_uniform(key_hash, level_idx)
.map(|slot_idx| (level_idx, slot_idx));
}
if let Some(slot_idx) = self.first_free_uniform(key_hash, level_idx) {
return Some((level_idx, slot_idx));
}
self.first_free_uniform(key_hash, level_idx + 1)
.map(|slot_idx| (level_idx + 1, slot_idx))
}
#[inline]
unsafe fn slot_ref(&self, level_idx: usize, slot_idx: usize) -> &SlotEntry<K, V> {
unsafe { self.levels[level_idx].get_ref(slot_idx) }
}
#[inline]
unsafe fn slot_mut(&mut self, level_idx: usize, slot_idx: usize) -> &mut SlotEntry<K, V> {
unsafe { self.levels[level_idx].get_mut(slot_idx) }
}
#[inline]
fn find_slot_indices_with_hash<Q>(
&self,
key: &Q,
key_hash: u64,
key_fingerprint: u8,
) -> Option<(usize, usize)>
where
Q: Equivalent<K> + ?Sized,
{
let search_limit = (self.max_populated_level + 1).min(self.levels.len());
for (level_idx, level) in self.levels[..search_limit].iter().enumerate() {
if let Some(slot_idx) = level.find_by_probe(key_hash, key_fingerprint, key) {
return Some((level_idx, slot_idx));
}
}
None
}
#[inline]
fn first_free_limited(
&self,
key_hash: u64,
level_idx: usize,
max_groups: usize,
) -> Option<usize> {
let level = &self.levels[level_idx];
if level.len as usize >= level.capacity() {
return None;
}
let group_count = level.group_count();
let max_groups = max_groups.min(group_count.max(1));
let mask = level.group_count_mask as usize;
let mut probe = probe::TriangularProbe::new(level.triangular_group_start(key_hash));
for _ in 0..max_groups {
if let Some(slot_idx) = level.first_free_in_group(probe.pos) {
return Some(slot_idx);
}
probe.advance(mask);
}
None
}
#[inline]
fn first_free_uniform(&self, key_hash: u64, level_idx: usize) -> Option<usize> {
let level = &self.levels[level_idx];
if level.len as usize >= level.capacity() {
return None;
}
let group_count = level.group_count();
let mask = level.group_count_mask as usize;
let mut probe = probe::TriangularProbe::new(level.triangular_group_start(key_hash));
for _ in 0..group_count {
if let Some(slot_idx) = level.first_free_in_group(probe.pos) {
return Some(slot_idx);
}
probe.advance(mask);
}
None
}
fn shrink_max_populated_level(&mut self) {
while self.max_populated_level > 0
&& self.levels[self.max_populated_level].len == 0
&& self.levels[self.max_populated_level].tombstones == 0
{
self.max_populated_level -= 1;
}
if self.levels.is_empty() || (self.levels[0].len == 0 && self.levels[0].tombstones == 0) {
self.max_populated_level = 0;
}
}
}
fn compute_budget_cap(reserve_fraction: f64, group_count: usize) -> f64 {
let log_cap = 1.0 + (1.0 / reserve_fraction).log2();
let max_budget = math::cast::usize_to_f64(group_count.max(1));
log_cap.min(max_budget).max(1.0)
}
fn partition_levels(total_capacity: usize) -> Vec<usize> {
if total_capacity == 0 {
return Vec::new();
}
let mut sizes = Vec::new();
let mut remaining = total_capacity;
let mut next_size = total_capacity.div_ceil(2);
while remaining > 0 {
let size = next_size.min(remaining).max(1);
sizes.push(size);
remaining -= size;
if remaining == 0 {
break;
}
next_size = (size / 2).max(1);
}
sizes
.into_iter()
.map(align::round_up_to_pow2_groups)
.collect()
}
fn build_batch_plan(
level_capacities: &[usize],
reserve_fraction: f64,
max_insertions: usize,
) -> Box<[usize]> {
if level_capacities.is_empty() || max_insertions == 0 {
return Box::new([]);
}
let mut plan = Vec::with_capacity(level_capacities.len() + 1);
plan.push(capacity::ceil_three_quarters(level_capacities[0]));
for level_index in 1..level_capacities.len() {
let current_level_capacity = level_capacities[level_index - 1];
let next_level_capacity = level_capacities[level_index];
let target_current_level_occupancy = current_level_capacity.saturating_sub(
capacity::floor_half_reserve_slots(reserve_fraction, current_level_capacity),
);
let initial_current_level_occupancy = capacity::ceil_three_quarters(current_level_capacity);
let initial_next_level_occupancy = capacity::ceil_three_quarters(next_level_capacity);
let batch_size = target_current_level_occupancy
.saturating_sub(initial_current_level_occupancy)
.saturating_add(initial_next_level_occupancy);
plan.push(batch_size);
}
let mut inserted = 0;
for size in &mut plan {
if inserted >= max_insertions {
*size = 0;
continue;
}
let room = max_insertions - inserted;
if *size > room {
*size = room;
}
inserted += *size;
}
if inserted < max_insertions {
plan.push(max_insertions - inserted);
}
plan.into_boxed_slice()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn level_partition_inflates_to_pow2_groups_and_preserves_halving() {
for &cap in &[127usize, 1_000, 10_000, 100_000] {
let sizes = partition_levels(cap);
assert!(!sizes.is_empty());
for &s in &sizes {
let g = s / GROUP_SIZE;
assert!(
g.is_power_of_two(),
"cap={cap} level slots={s} groups={g} not pow2"
);
}
let total: usize = sizes.iter().sum();
assert!(total >= cap, "cap={cap} total={total} below request");
#[cfg(not(opthash_wide_group))]
assert!(total <= cap * 2, "cap={cap} total={total} exceeds 2x");
for w in sizes.windows(2) {
assert!(w[1] <= w[0], "non-monotonic: {} → {}", w[0], w[1]);
assert!(w[1] * 2 >= w[0], "shrinks too fast: {} → {}", w[0], w[1]);
}
}
}
#[test]
fn elastic_geometry_carries_capacity_and_batch_state() {
for &requested in &[0usize, 1, 127, 1_000, 10_000] {
let reserve_fraction = crate::common::config::DEFAULT_RESERVE_FRACTION;
let geometry = ElasticGeometry::for_insert_budget(requested, reserve_fraction).unwrap();
assert!(
geometry.max_insertions >= requested,
"requested={requested} max_insertions={}",
geometry.max_insertions
);
assert!(
geometry.level_capacities.iter().sum::<usize>() >= geometry.total_slots,
"rounded level capacities must cover total slots"
);
assert_eq!(
geometry.initial_batch_remaining,
geometry.batch_plan.first().copied().unwrap_or(0)
);
assert!(
geometry.batch_plan.iter().sum::<usize>() >= geometry.max_insertions,
"batch plan must cover the insertion budget"
);
}
}
#[test]
fn clone_and_clear_preserve_elastic_lookups() {
let mut map: ElasticHashMap<u64, u64> = ElasticHashMap::with_capacity(512);
for i in 0..384 {
map.insert(i, i ^ 0xa5a5);
}
let cloned = map.clone();
for i in 0..384 {
assert_eq!(cloned.get(&i), Some(&(i ^ 0xa5a5)));
}
for i in 10_000..10_128 {
assert_eq!(cloned.get(&i), None);
}
map.clear();
for i in 512..896 {
map.insert(i, i ^ 0x5a5a);
}
for i in 512..896 {
assert_eq!(map.get(&i), Some(&(i ^ 0x5a5a)));
}
for i in 0..384 {
assert_eq!(map.get(&i), None);
}
}
#[test]
fn retain_does_not_trigger_mid_iter_resize_with_clustered_tombstones() {
let mut map: ElasticHashMap<i32, i32> = ElasticHashMap::with_capacity(256);
let cap = i32::try_from(map.capacity()).expect("test capacity fits i32");
let n = cap * 2 / 3;
for i in 0..n {
map.insert(i, i);
}
let initial_capacity = map.capacity();
map.retain(|k, _| k % 2 == 0);
let expected_count = (0..n).filter(|i| i % 2 == 0).count();
assert_eq!(map.len(), expected_count);
for i in 0..n {
if i % 2 == 0 {
assert_eq!(map.get(&i), Some(&i), "kept key {i} missing");
} else {
assert!(map.get(&i).is_none(), "dropped key {i} survived");
}
}
assert_eq!(
map.capacity(),
initial_capacity,
"retain must not change the slot count, only rehash in place"
);
}
#[test]
fn inserts_spill_to_deeper_levels_at_high_load() {
let mut map: ElasticHashMap<i32, i32> = ElasticHashMap::with_capacity(512);
assert!(
map.table().levels.len() > 1,
"test requires multi-level layout"
);
let max = i32::try_from(map.capacity()).expect("test capacity fits i32");
for i in 0..max {
map.insert(i, i);
}
assert!(
map.table().max_populated_level > 0,
"expected spill into deeper level; max_populated_level = {}",
map.table().max_populated_level
);
for i in 0..max {
assert_eq!(map.get(&i), Some(&i));
}
}
#[test]
fn max_populated_level_shrinks_when_deepest_levels_emptied() {
let mut map: ElasticHashMap<i32, i32> = ElasticHashMap::with_capacity(512);
let max = i32::try_from(map.capacity()).expect("test capacity fits i32");
for i in 0..max {
map.insert(i, i);
}
let high_water = map.table().max_populated_level;
assert!(high_water > 0, "need a multi-level state to test shrinkage");
for i in 0..max {
map.remove(&i);
}
assert_eq!(map.len(), 0);
assert_eq!(
map.table().max_populated_level,
0,
"max_populated_level should walk back to 0 once every level empties"
);
}
}