use crate::containers::FastVec;
use crate::error::Result;
use crate::hash_map::cache_locality::{
CacheMetrics,
};
use crate::hash_map::simd_string_ops::{SimdStringOps, get_global_simd_ops};
use crate::memory::cache_layout::{CacheLayoutConfig, CacheOptimizedAllocator};
use ahash::RandomState;
use std::borrow::Borrow;
use std::fmt;
use std::hash::{BuildHasher, Hash};
use std::mem::MaybeUninit;
use crate::hash_map::config::{HashStorageStrategy, OptimizationStrategy, ZiporaHashMapConfig};
use crate::hash_map::storage::*;
pub struct ZiporaHashMap<K, V, S = RandomState>
where
K: Hash + Eq + Clone,
V: Clone,
S: BuildHasher,
{
pub(super) config: ZiporaHashMapConfig,
pub(super) hash_builder: S,
pub(super) storage: HashMapStorage<K, V>,
pub(super) stats: HashMapStats,
pub(super) _simd_ops: &'static SimdStringOps,
pub(super) _cache_allocator: Option<CacheOptimizedAllocator>,
pub(super) cache_metrics: CacheMetrics,
}
#[derive(Debug, Default, Clone)]
pub struct HashMapStats {
pub insertions: u64,
pub lookups: u64,
pub collisions: u64,
pub probe_distance_sum: u64,
pub rehashes: u64,
pub cache_hits: u64,
pub cache_misses: u64,
}
impl<K, V, S> ZiporaHashMap<K, V, S>
where
K: Hash + Eq + Clone,
V: Clone,
S: BuildHasher,
{
pub fn new() -> Result<Self>
where
S: Default,
{
Self::with_config(ZiporaHashMapConfig::default())
}
pub fn with_capacity(capacity: usize) -> Result<Self>
where
S: Default,
{
let cap = capacity.max(16);
let config = ZiporaHashMapConfig {
initial_capacity: cap,
storage_strategy: HashStorageStrategy::Standard {
initial_capacity: cap,
growth_factor: 2.0,
},
..ZiporaHashMapConfig::default()
};
Self::with_config(config)
}
pub fn with_config(config: ZiporaHashMapConfig) -> Result<Self>
where
S: Default,
{
Self::with_config_and_hasher(config, S::default())
}
pub fn with_config_and_hasher(config: ZiporaHashMapConfig, hash_builder: S) -> Result<Self> {
let simd_ops = get_global_simd_ops();
let cache_allocator = match &config.optimization_strategy {
OptimizationStrategy::CacheAware { .. }
| OptimizationStrategy::HighPerformance {
cache_optimized: true,
..
} => Some(CacheOptimizedAllocator::new(CacheLayoutConfig::default())),
_ => None,
};
let storage = Self::create_storage(&config)?;
Ok(Self {
config,
hash_builder,
storage,
stats: HashMapStats::default(),
_simd_ops: simd_ops,
_cache_allocator: cache_allocator,
cache_metrics: CacheMetrics::new(),
})
}
pub(super) fn create_storage(config: &ZiporaHashMapConfig) -> Result<HashMapStorage<K, V>> {
match &config.storage_strategy {
HashStorageStrategy::Standard {
initial_capacity, ..
} => Ok(HashMapStorage::Standard {
buckets: FastVec::with_capacity(*initial_capacity)?,
entries: FastVec::with_capacity(*initial_capacity)?,
mask: initial_capacity.saturating_sub(1),
}),
HashStorageStrategy::SmallInline {
inline_capacity: _, ..
} => {
Ok(HashMapStorage::SmallInline {
inline_data: InlineStorage {
_data: [const { MaybeUninit::uninit() }; 16],
occupied: 0,
},
fallback: None,
len: 0,
})
}
HashStorageStrategy::CacheOptimized { .. } => {
Err(crate::error::ZiporaError::not_supported(
"CacheOptimized storage strategy is not yet implemented",
))
}
HashStorageStrategy::StringOptimized { .. } => {
Err(crate::error::ZiporaError::not_supported(
"StringOptimized storage strategy is not yet implemented",
))
}
HashStorageStrategy::PoolAllocated { .. } => {
Err(crate::error::ZiporaError::not_supported(
"PoolAllocated storage strategy is not yet implemented",
))
}
}
}
pub fn insert(&mut self, key: K, value: V) -> Result<Option<V>> {
self.stats.insertions += 1;
let hash = self.hash_key(&key);
match &mut self.storage {
HashMapStorage::Standard {
buckets,
entries,
mask,
} => {
match Self::insert_standard(
&self.hash_builder,
buckets,
entries,
mask,
key,
value,
hash,
) {
Ok(result) => Ok(result),
Err((key, value)) => {
self.resize_storage()?;
if let HashMapStorage::Standard {
buckets,
entries,
mask,
} = &mut self.storage
{
Self::insert_standard(
&self.hash_builder,
buckets,
entries,
mask,
key,
value,
hash,
)
.map_err(|_| {
crate::error::ZiporaError::invalid_state(
"Hash table full after resize",
)
})
} else {
Err(crate::error::ZiporaError::invalid_state(
"Storage type changed during resize",
))
}
}
}
}
HashMapStorage::SmallInline {
inline_data,
fallback,
len,
} => Self::insert_small_inline(
inline_data,
fallback,
len,
key,
value,
hash,
&self.hash_builder,
),
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
pub fn get<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let hash = self.hash_key_borrowed(key);
match &self.storage {
HashMapStorage::Standard {
buckets,
entries,
mask,
} => self.get_standard(buckets, entries, mask, key, hash),
HashMapStorage::SmallInline {
inline_data,
fallback,
len,
} => self.get_small_inline(inline_data, fallback, len, key),
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
#[inline]
pub fn len(&self) -> usize {
match &self.storage {
HashMapStorage::Standard { entries, .. } => {
entries
.iter()
.filter(|entry| entry.hash != 0 && entry.hash != u64::MAX)
.count()
}
HashMapStorage::SmallInline { len, .. } => *len,
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn stats(&self) -> &HashMapStats {
&self.stats
}
pub fn cache_metrics(&self) -> &CacheMetrics {
&self.cache_metrics
}
pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
match &mut self.storage {
HashMapStorage::Standard {
buckets,
entries,
mask,
} => Self::get_mut_standard(&self.hash_builder, buckets, entries, mask, key),
HashMapStorage::SmallInline {
inline_data,
fallback,
len,
} => Self::get_mut_small_inline(inline_data, fallback, len, key),
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
match &mut self.storage {
HashMapStorage::Standard {
buckets,
entries,
mask,
} => Self::remove_standard(&self.hash_builder, buckets, entries, mask, key),
HashMapStorage::SmallInline {
inline_data,
fallback,
len,
} => Self::remove_small_inline(inline_data, fallback, len, key),
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
pub fn clear(&mut self) {
match &mut self.storage {
HashMapStorage::Standard {
buckets,
entries,
mask,
} => Self::clear_standard(buckets, entries, mask),
HashMapStorage::SmallInline {
inline_data,
fallback,
len,
} => Self::clear_small_inline(inline_data, fallback, len),
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
pub fn contains_key<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.get(key).is_some()
}
pub fn capacity(&self) -> usize {
match &self.storage {
HashMapStorage::Standard { entries, .. } => entries.capacity(), HashMapStorage::SmallInline {
inline_data: _,
fallback,
..
} => {
16 + fallback.as_ref().map_or(0, |f| match f.as_ref() {
HashMapStorage::Standard { entries, .. } => entries.capacity(),
_ => 0,
})
}
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
pub fn iter(&self) -> ZiporaHashMapIterator<'_, K, V> {
ZiporaHashMapIterator {
storage: &self.storage,
index: 0,
}
}
}
impl<K, V, S> Default for ZiporaHashMap<K, V, S>
where
K: Hash + Eq + Clone,
V: Clone,
S: BuildHasher + Default,
{
fn default() -> Self {
Self::new().unwrap_or_else(|e| {
panic!(
"ZiporaHashMap creation failed in Default: {}. \
This indicates severe memory pressure.",
e
)
})
}
}
impl<K, V, S> fmt::Debug for ZiporaHashMap<K, V, S>
where
K: Hash + Eq + Clone + fmt::Debug,
V: Clone + fmt::Debug,
S: BuildHasher,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ZiporaHashMap")
.field("len", &self.len())
.field("config", &self.config)
.field("stats", &self.stats)
.finish()
}
}
impl<K, V, S> Clone for ZiporaHashMap<K, V, S>
where
K: Hash + Eq + Clone,
V: Clone,
S: BuildHasher + Clone,
{
fn clone(&self) -> Self {
let new_map = Self::with_config_and_hasher(self.config.clone(), self.hash_builder.clone())
.unwrap_or_else(|e| {
panic!(
"ZiporaHashMap clone failed: {}. \
This indicates severe memory pressure.",
e
)
});
new_map
}
}
pub struct ZiporaHashMapIterator<'a, K, V>
where
K: Clone,
V: Clone,
{
storage: &'a HashMapStorage<K, V>,
index: usize,
}
impl<'a, K, V> Iterator for ZiporaHashMapIterator<'a, K, V>
where
K: Clone,
V: Clone,
{
type Item = (&'a K, &'a V);
fn next(&mut self) -> Option<Self::Item> {
match self.storage {
HashMapStorage::Standard { entries, .. } => {
while self.index < entries.len() {
let entry = &entries[self.index];
self.index += 1;
if entry.hash != 0 && entry.hash != u64::MAX {
return Some((
entry.key.as_ref().expect("occupied entry must have key"),
entry
.value
.as_ref()
.expect("occupied entry must have value"),
));
}
}
None
}
HashMapStorage::SmallInline { len: _, .. } => {
None
}
HashMapStorage::CacheOptimized { .. } | HashMapStorage::StringOptimized { .. } => {
unreachable!("unimplemented strategies are rejected at construction")
}
}
}
}
#[cfg(test)]
#[path = "zipora_hash_map_tests.rs"]
mod tests;