use alloc::vec;
use crate::encoding::util::hash_top_bits;
use crate::encoding::workspace::{Table, Workspace, region_bytes};
const ZSTD_HASHLOG_MAX: u32 = 30;
fn validate_params(hash_log: u32, mls: u32) {
assert!(
(1..=ZSTD_HASHLOG_MAX).contains(&hash_log),
"hash_log must be in 1..={ZSTD_HASHLOG_MAX} for upstream zstd-compatible Fast hashing (got {hash_log}); \
the lower bound prevents a full-word-width shift in hash_ptr, the upper bound is upstream zstd's ZSTD_HASHLOG_MAX",
);
assert!(
(4..=8).contains(&mls),
"ZSTD Fast strategy only supports mls 4..=8 (got {mls})",
);
}
pub(crate) const TAG_BITS: u32 = 8;
pub(crate) const TAG_MASK: u32 = (1 << TAG_BITS) - 1;
pub(crate) const TAGGED_POSITION_LIMIT: usize = 1 << (32 - TAG_BITS);
const PRIME_4_BYTES: u32 = 0x9E3779B1;
const PRIME_5_BYTES: u64 = 889_523_592_379;
const PRIME_6_BYTES: u64 = 227_718_039_650_203;
const PRIME_7_BYTES: u64 = 58_295_818_150_454_627;
const PRIME_8_BYTES: u64 = 0xCF1BBCDCB7A56463;
pub(crate) struct FastHashTable {
table: Table<u32>,
hash_log: u32,
mls: u32,
bias: u32,
tag_bits: u32,
}
impl Clone for FastHashTable {
fn clone(&self) -> Self {
Self {
table: self.table.clone(),
hash_log: self.hash_log,
mls: self.mls,
bias: self.bias,
tag_bits: self.tag_bits,
}
}
fn clone_from(&mut self, source: &Self) {
self.table.clone_from(&source.table);
self.hash_log = source.hash_log;
self.mls = source.mls;
self.bias = source.bias;
self.tag_bits = source.tag_bits;
}
}
impl FastHashTable {
pub(crate) fn new(hash_log: u32, mls: u32) -> Self {
validate_params(hash_log, mls);
Self {
table: Table::owned(vec![0u32; entry_count(hash_log)]),
hash_log,
mls,
bias: 0,
tag_bits: 0,
}
}
pub(crate) fn workspace_bytes(hash_log: u32) -> usize {
region_bytes::<u32>(entry_count(hash_log))
}
pub(crate) fn bind(
&mut self,
workspace: &mut Workspace,
hash_log: u32,
mls: u32,
tagged: bool,
) -> bool {
validate_params(hash_log, mls);
let tag_bits = if tagged { TAG_BITS } else { 0 };
assert!(
hash_log + tag_bits <= 32,
"a tagged table needs hash_log <= {} (got {hash_log})",
32 - TAG_BITS,
);
let same_shape = self.hash_log == hash_log && self.mls == mls && self.tag_bits == tag_bits;
let kept = self.table.bind(workspace, entry_count(hash_log), 0);
self.hash_log = hash_log;
self.mls = mls;
self.tag_bits = tag_bits;
if kept && same_shape {
return true;
}
if kept {
self.table.fill(0);
}
self.bias = 0;
false
}
#[inline(always)]
pub(crate) fn is_tagged(&self) -> bool {
self.tag_bits != 0
}
pub(crate) fn untag_empty(&mut self) {
debug_assert!(
self.table.iter().all(|&slot| slot == 0),
"untag_empty on a table that holds entries"
);
self.tag_bits = 0;
}
pub(crate) fn leave_workspace(&mut self) {
self.table.leave_workspace();
}
pub(crate) fn new_deferred(hash_log: u32, mls: u32) -> Self {
validate_params(hash_log, mls);
Self {
table: Table::empty(),
hash_log,
mls,
bias: 0,
tag_bits: 0,
}
}
#[inline(always)]
pub(crate) fn hash_log(&self) -> u32 {
self.hash_log
}
#[inline(always)]
pub(crate) fn mls(&self) -> u32 {
self.mls
}
pub(crate) fn heap_size(&self) -> usize {
self.table.owned_bytes()
}
pub(crate) fn clear(&mut self) {
self.table.fill(0);
self.bias = 0;
}
pub(crate) fn reduce_indices(&mut self, drop_n: u32) {
debug_assert!(
self.bias.checked_add(drop_n).is_some(),
"epoch bias {} plus a {drop_n}-byte slide overflows a stored position",
self.bias,
);
debug_assert!(self.tag_bits == 0 || self.bias == 0);
let correction = (self.bias + drop_n) << self.tag_bits;
for slot in self.table.iter_mut() {
*slot = slot.saturating_sub(correction);
}
self.bias = 0;
}
pub(crate) fn advance_epoch(&mut self, span: u32) {
const POSITION_CEILING: u32 = 1 << 31;
debug_assert!(!self.is_tagged(), "a tagged table is never biased");
match self.bias.checked_add(span) {
Some(new_bias) if new_bias <= u32::MAX - POSITION_CEILING => self.bias = new_bias,
_ => self.clear(),
}
}
#[inline(always)]
pub(crate) unsafe fn hash_ptr<const MLS: u32>(&self, ptr: *const u8) -> u32 {
debug_assert_eq!(MLS, self.mls, "monomorphised MLS must match table mls");
unsafe { hash_ptr_raw::<MLS>(ptr, self.hash_log + self.tag_bits) }
}
#[inline(always)]
pub(crate) fn hot_state(&mut self) -> (&mut [u32], u32) {
debug_assert_eq!(self.bias, 0, "hot_state requires an unbiased table");
(self.table.as_mut_slice(), self.hash_log)
}
#[inline(always)]
pub(crate) fn hot_state_biased(&mut self) -> (&mut [u32], u32, u32) {
debug_assert!(!self.is_tagged(), "the biased kernels read bare positions");
(self.table.as_mut_slice(), self.hash_log, self.bias)
}
#[inline(always)]
pub(crate) fn dict_slots(&self) -> &[u32] {
debug_assert!(!self.is_tagged(), "dictionary slots carry their own tag");
debug_assert_eq!(self.bias, 0, "a dictionary table is never epoch-advanced");
self.table.as_slice()
}
#[inline(always)]
pub(crate) unsafe fn get(&self, hash: u32) -> u32 {
let raw = unsafe { self.slot(hash) };
if self.tag_bits != 0 {
let tag_mask = (1 << self.tag_bits) - 1;
return if (raw ^ hash) & tag_mask == 0 {
raw >> self.tag_bits
} else {
0
};
}
raw.saturating_sub(self.bias)
}
#[inline(always)]
pub(crate) unsafe fn slot_is_empty(&self, hash: u32) -> bool {
unsafe { self.slot(hash) }.saturating_sub(self.bias) == 0
}
#[inline(always)]
pub(crate) unsafe fn put(&mut self, hash: u32, pos: u32) {
let index = (hash >> self.tag_bits) as usize;
debug_assert!(index < self.table.len());
debug_assert!(self.tag_bits == 0 || (pos as usize) < TAGGED_POSITION_LIMIT);
let tag_mask = (1u32 << self.tag_bits) - 1;
let stored = ((pos + self.bias) << self.tag_bits) | (hash & tag_mask);
unsafe {
*self.table.get_unchecked_mut(index) = stored;
}
}
#[inline(always)]
unsafe fn slot(&self, hash: u32) -> u32 {
let index = (hash >> self.tag_bits) as usize;
debug_assert!(index < self.table.len());
unsafe { *self.table.get_unchecked(index) }
}
}
fn entry_count(hash_log: u32) -> usize {
let entries = 1usize.checked_shl(hash_log).unwrap_or_else(|| {
panic!(
"FastHashTable cannot allocate 2^{hash_log} u32 entries on this target: \
`1usize << {hash_log}` overflows {0}-bit usize",
usize::BITS,
)
});
if entries.checked_mul(core::mem::size_of::<u32>()).is_none() {
panic!(
"FastHashTable cannot allocate {entries} u32 entries on this target: \
byte size overflows {0}-bit usize",
usize::BITS,
);
}
entries
}
#[inline(always)]
pub(crate) unsafe fn hash_ptr_raw<const MLS: u32>(ptr: *const u8, hash_log: u32) -> u32 {
debug_assert!(
(1..=32).contains(&hash_log),
"hash_log {hash_log} out of 1..=32"
);
match MLS {
4 => {
let u = unsafe { core::ptr::read_unaligned(ptr.cast::<u32>()) }.to_le();
u.wrapping_mul(PRIME_4_BYTES) >> (32 - hash_log)
}
5 => {
let u = unsafe { core::ptr::read_unaligned(ptr.cast::<u64>()) }.to_le();
hash_top_bits((u << (64 - 40)).wrapping_mul(PRIME_5_BYTES), hash_log)
}
6 => {
let u = unsafe { core::ptr::read_unaligned(ptr.cast::<u64>()) }.to_le();
hash_top_bits((u << (64 - 48)).wrapping_mul(PRIME_6_BYTES), hash_log)
}
7 => {
let u = unsafe { core::ptr::read_unaligned(ptr.cast::<u64>()) }.to_le();
hash_top_bits((u << (64 - 56)).wrapping_mul(PRIME_7_BYTES), hash_log)
}
8 => {
let u = unsafe { core::ptr::read_unaligned(ptr.cast::<u64>()) }.to_le();
hash_top_bits(u.wrapping_mul(PRIME_8_BYTES), hash_log)
}
_ => {
debug_assert!(false, "unsupported MLS {MLS}");
0
}
}
}
#[cfg(test)]
mod tests;