use std::sync::RwLock;
const SHARDS: usize = 64;
const MAX_KEY: usize = 16;
const MAX_IDS: usize = MAX_KEY;
#[derive(Clone, Copy)]
struct Slot {
key: [u8; MAX_KEY],
ids: [u32; MAX_IDS],
klen: u8,
nids: u8,
}
const EMPTY: u8 = 0;
const VACANT: Slot = Slot {
key: [0; MAX_KEY],
ids: [0; MAX_IDS],
klen: EMPTY,
nids: 0,
};
#[derive(Clone, Default)]
struct LongSlot {
key: Vec<u8>,
ids: Vec<u32>,
}
const WAYS: usize = 16;
#[inline]
fn tag_of(hash: u64) -> u8 {
((hash >> 24) as u8) | 1
}
const VACANT_TAG: u8 = 0;
#[inline]
fn matching_lanes(word: u64, tag: u8) -> u64 {
const LOW: u64 = 0x0101_0101_0101_0101;
const HIGH: u64 = 0x8080_8080_8080_8080;
let x = word ^ LOW.wrapping_mul(tag as u64);
x.wrapping_sub(LOW) & !x & HIGH
}
#[inline]
fn probe(tags: &[u8], home: usize, tag: u8, mut visit: impl FnMut(usize) -> bool) -> bool {
let mut base = home;
while base < home + WAYS {
let Some(lanes) = tags.get(base..base + 8).and_then(|s| s.try_into().ok()) else {
return false;
};
let mut hits = matching_lanes(u64::from_le_bytes(lanes), tag);
while hits != 0 {
let lane = hits.trailing_zeros() as usize / 8;
if visit(base + lane) {
return true;
}
hits &= hits - 1;
}
base += 8;
}
false
}
struct Tiers {
inline_tags: Box<[u8]>,
inline: Box<[Slot]>,
long_tags: Box<[u8]>,
long: Box<[LongSlot]>,
}
type Shard = RwLock<Tiers>;
pub(crate) struct ChunkCache {
shards: Box<[Shard]>,
mask: usize,
}
impl ChunkCache {
pub(crate) fn new(capacity: usize) -> Self {
let per_shard = (capacity / SHARDS).next_power_of_two().max(1);
let slots = per_shard + WAYS;
Self {
shards: (0..SHARDS)
.map(|_| {
RwLock::new(Tiers {
inline_tags: vec![VACANT_TAG; slots].into_boxed_slice(),
inline: vec![VACANT; slots].into_boxed_slice(),
long_tags: vec![VACANT_TAG; slots].into_boxed_slice(),
long: vec![LongSlot::default(); slots].into_boxed_slice(),
})
})
.collect(),
mask: per_shard - 1,
}
}
pub(crate) fn shard_hash(key: &[u8]) -> u64 {
crate::core::encoder::Encoder::hash_of(key)
}
#[inline]
fn locate(&self, hash: u64) -> (&Shard, usize) {
(
&self.shards[(hash >> 32) as usize % SHARDS],
hash as usize & self.mask,
)
}
pub(crate) fn extend_into(&self, hash: u64, key: &[u8], out: &mut Vec<u32>) -> bool {
let (shard, home) = self.locate(hash);
let Ok(tiers) = shard.read() else {
return false;
};
let tag = tag_of(hash);
if key.len() > MAX_KEY {
return probe(&tiers.long_tags, home, tag, |i| {
let slot = &tiers.long[i];
if slot.key != key {
return false;
}
out.extend_from_slice(&slot.ids);
true
});
}
probe(&tiers.inline_tags, home, tag, |i| {
let slot = &tiers.inline[i];
if slot.klen as usize != key.len() || &slot.key[..key.len()] != key {
return false;
}
match slot.nids {
1 => out.push(slot.ids[0]),
n => out.extend_from_slice(&slot.ids[..n as usize]),
}
true
})
}
fn insert_slot(
tags: &[u8],
home: usize,
hash: u64,
tag: u8,
matches: impl Fn(usize) -> bool,
) -> usize {
let mut found = None;
probe(tags, home, tag, |i| {
if matches(i) {
found = Some(i);
return true;
}
false
});
if let Some(i) = found {
return i;
}
if let Some(i) = (home..home + WAYS).find(|&i| tags.get(i) == Some(&VACANT_TAG)) {
return i;
}
home + (hash >> 56) as usize % WAYS
}
pub(crate) fn put(&self, hash: u64, key: &[u8], ids: &[u32]) {
if key.is_empty() {
return;
}
let (shard, home) = self.locate(hash);
let Ok(mut tiers) = shard.write() else {
return;
};
let tag = tag_of(hash);
if key.len() > MAX_KEY {
let index = Self::insert_slot(&tiers.long_tags, home, hash, tag, |i| {
tiers.long[i].key == key
});
let slot = &mut tiers.long[index];
slot.key.clear();
slot.key.extend_from_slice(key);
slot.ids.clear();
slot.ids.extend_from_slice(ids);
tiers.long_tags[index] = tag;
return;
}
if ids.len() > MAX_IDS {
return;
}
let index = Self::insert_slot(&tiers.inline_tags, home, hash, tag, |i| {
let slot = &tiers.inline[i];
slot.klen as usize == key.len() && slot.key[..key.len()] == *key
});
let slot = &mut tiers.inline[index];
slot.key[..key.len()].copy_from_slice(key);
slot.ids[..ids.len()].copy_from_slice(ids);
slot.klen = key.len() as u8;
slot.nids = ids.len() as u8;
tiers.inline_tags[index] = tag;
}
pub(crate) fn clear(&self) {
for shard in &self.shards {
if let Ok(mut tiers) = shard.write() {
tiers.inline_tags.fill(VACANT_TAG);
tiers.inline.fill(VACANT);
tiers.long_tags.fill(VACANT_TAG);
tiers.long.fill_with(LongSlot::default);
}
}
}
pub(crate) fn len(&self) -> usize {
self.shards
.iter()
.map(|shard| {
shard
.read()
.map(|tiers| {
tiers
.inline_tags
.iter()
.filter(|&&t| t != VACANT_TAG)
.count()
+ tiers.long_tags.iter().filter(|&&t| t != VACANT_TAG).count()
})
.unwrap_or(0)
})
.sum()
}
}
impl std::fmt::Debug for ChunkCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ChunkCache")
.field("shards", &SHARDS)
.field("len", &self.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(cache: &ChunkCache, key: &[u8], ids: &[u32]) -> Option<Vec<u32>> {
let hash = ChunkCache::shard_hash(key);
cache.put(hash, key, ids);
let mut out = Vec::new();
cache.extend_into(hash, key, &mut out).then_some(out)
}
#[test]
fn a_chunk_too_long_to_inline_is_still_cached() {
let cache = ChunkCache::new(1024);
let key = "那么,线性代数又是如何来解决这些问题的呢".as_bytes();
assert!(key.len() > MAX_KEY, "test key must exercise the boxed tier");
assert_eq!(
roundtrip(&cache, key, &[1, 2, 3, 4, 5]).as_deref(),
Some(&[1, 2, 3, 4, 5][..])
);
}
#[test]
fn overwriting_a_long_slot_replaces_its_ids() {
let cache = ChunkCache::new(1024);
let key = "那么,线性代数又是如何来解决这些问题的呢".as_bytes();
assert_eq!(
roundtrip(&cache, key, &[1, 2, 3, 4, 5]).as_deref(),
Some(&[1, 2, 3, 4, 5][..])
);
assert_eq!(roundtrip(&cache, key, &[9]).as_deref(), Some(&[9][..]));
}
#[test]
fn keys_sharing_a_home_slot_do_not_evict_each_other() {
let cache = ChunkCache::new(1024);
let hash = 0x1234_5678_9abc_def0;
cache.put(hash, b"alpha", &[1]);
cache.put(hash, b"bravo", &[2]);
let mut out = Vec::new();
assert!(cache.extend_into(hash, b"alpha", &mut out));
assert_eq!(out, [1], "the first key was evicted by the second");
out.clear();
assert!(cache.extend_into(hash, b"bravo", &mut out));
assert_eq!(out, [2]);
}
#[test]
fn a_window_holds_a_full_set_of_colliding_keys() {
let cache = ChunkCache::new(1024);
let hash = 0xdead_beef_0000_0000;
let keys: Vec<Vec<u8>> = (0..WAYS)
.map(|i| format!("key{i:02}").into_bytes())
.collect();
for (i, key) in keys.iter().enumerate() {
cache.put(hash, key, &[i as u32]);
}
cache.put(hash, &keys[0], &[99]);
let mut out = Vec::new();
for (i, key) in keys.iter().enumerate().skip(1) {
out.clear();
assert!(cache.extend_into(hash, key, &mut out), "lost key {i}");
assert_eq!(out, [i as u32], "key {i} came back with the wrong ids");
}
out.clear();
assert!(cache.extend_into(hash, &keys[0], &mut out));
assert_eq!(out, [99], "re-inserting a present key did not update it");
}
#[test]
fn a_short_chunk_round_trips_through_the_inline_tier() {
let cache = ChunkCache::new(1024);
assert_eq!(
roundtrip(&cache, b"hello", &[7, 8]).as_deref(),
Some(&[7, 8][..])
);
}
#[test]
fn clear_empties_both_tiers() {
let cache = ChunkCache::new(1024);
roundtrip(&cache, b"short", &[1]);
roundtrip(&cache, "那么,线性代数又是如何来解决".as_bytes(), &[2]);
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
}
#[test]
fn a_different_key_is_a_miss() {
let cache = ChunkCache::new(1024);
roundtrip(&cache, b"alpha", &[1]);
let mut out = Vec::new();
assert!(!cache.extend_into(ChunkCache::shard_hash(b"beta"), b"beta", &mut out));
let long = "这是一个很长的中文词组用来测试".as_bytes();
roundtrip(&cache, long, &[9]);
let other = "另一个完全不同的中文词组测试内容".as_bytes();
assert!(!cache.extend_into(ChunkCache::shard_hash(other), other, &mut out));
assert!(out.is_empty());
}
}