use std::{
collections::HashSet,
hash::{BuildHasher, Hash, Hasher, RandomState},
sync::{
Mutex,
atomic::{AtomicUsize, Ordering},
},
};
const SHARDS: usize = 256;
#[derive(Debug, Default, Clone, Copy)]
pub struct IdHasherBuilder;
impl BuildHasher for IdHasherBuilder {
type Hasher = IdHasher;
fn build_hasher(&self) -> IdHasher {
IdHasher(0)
}
}
#[derive(Debug, Default)]
pub struct IdHasher(u64);
impl Hasher for IdHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
if bytes.len() >= 9 {
let mut eight = [0u8; 8];
eight.copy_from_slice(&bytes[1..9]);
self.0 = self.0.rotate_left(29) ^ u64::from_le_bytes(eight);
} else {
for b in bytes {
self.0 = self.0.rotate_left(8) ^ u64::from(*b);
}
}
}
fn write_usize(&mut self, _: usize) {}
}
#[derive(Debug)]
pub struct ShardedSet<K, S = RandomState> {
hasher: S,
shards: Vec<Mutex<HashSet<K, S>>>,
rotor: AtomicUsize,
}
pub type ChunkIdSet = ShardedSet<crate::format::ChunkId, IdHasherBuilder>;
impl<K, S> Default for ShardedSet<K, S>
where
K: Hash + Eq,
S: BuildHasher + Default,
{
fn default() -> Self {
Self::new()
}
}
impl<K, S> ShardedSet<K, S>
where
K: Hash + Eq,
S: BuildHasher + Default,
{
pub fn new() -> Self {
Self {
hasher: S::default(),
shards: (0..SHARDS)
.map(|_| Mutex::new(HashSet::with_hasher(S::default())))
.collect(),
rotor: AtomicUsize::new(0),
}
}
fn shard_of(&self, key: &K) -> usize {
((self.hasher.hash_one(key) >> 32) & 0xFF) as usize
}
fn lock(&self, shard: usize) -> std::sync::MutexGuard<'_, HashSet<K, S>> {
self.shards[shard].lock().unwrap_or_else(|p| p.into_inner())
}
pub fn insert(&self, key: K) -> bool {
let shard = self.shard_of(&key);
self.lock(shard).insert(key)
}
pub fn contains(&self, key: &K) -> bool {
self.lock(self.shard_of(key)).contains(key)
}
pub fn len(&self) -> usize {
(0..SHARDS).map(|s| self.lock(s).len()).sum()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn extend_weighted(&self, items: impl IntoIterator<Item = (K, u64)>) -> u64 {
self.try_extend_weighted::<std::convert::Infallible>(items.into_iter().map(Ok))
.unwrap_or_else(|never| match never {})
}
pub fn try_extend_weighted<E>(
&self,
items: impl IntoIterator<Item = Result<(K, u64), E>>,
) -> Result<u64, E> {
let mut buckets: Vec<Vec<(K, u64)>> = (0..SHARDS).map(|_| Vec::new()).collect();
for item in items {
let (key, weight) = item?;
buckets[self.shard_of(&key)].push((key, weight));
}
let start = (self.rotor.fetch_add(1, Ordering::Relaxed) * 8) % SHARDS;
let mut new_weight = 0u64;
for offset in 0..SHARDS {
let shard = (start + offset) % SHARDS;
let bucket = std::mem::take(&mut buckets[shard]);
if bucket.is_empty() {
continue;
}
let mut set = self.lock(shard);
for (key, weight) in bucket {
if set.insert(key) {
new_weight = new_weight.saturating_add(weight);
}
}
}
Ok(new_weight)
}
pub fn extend(&self, keys: impl IntoIterator<Item = K>) {
self.extend_weighted(keys.into_iter().map(|k| (k, 0)));
}
pub fn shard_lens(&self) -> Vec<usize> {
(0..SHARDS).map(|s| self.lock(s).len()).collect()
}
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use proptest::prelude::*;
use super::*;
use crate::format::ChunkId;
#[derive(Debug, Clone)]
enum Op<K> {
Insert(K),
Contains(K),
Extend(Vec<(K, u64)>),
TryExtend(Vec<(K, u64)>, usize),
}
fn ops<K: Debug + Clone + 'static>(
key: BoxedStrategy<K>,
) -> impl Strategy<Value = Vec<Op<K>>> {
let batch = prop::collection::vec((key.clone(), 0..1_000u64), 0..32);
let op = prop_oneof![
key.clone().prop_map(Op::Insert),
key.prop_map(Op::Contains),
batch.clone().prop_map(Op::Extend),
(batch, 0..=32usize).prop_map(|(items, pos)| {
let pos = pos.min(items.len());
Op::TryExtend(items, pos)
}),
];
prop::collection::vec(op, 0..64)
}
fn behaves_like_hash_set<K, S>(ops: Vec<Op<K>>) -> Result<(), TestCaseError>
where
K: Hash + Eq + Clone + Debug,
S: BuildHasher + Default,
{
let set: ShardedSet<K, S> = ShardedSet::new();
let mut model: HashSet<K> = HashSet::new();
for op in ops {
match op {
Op::Insert(k) => {
prop_assert_eq!(set.insert(k.clone()), model.insert(k));
}
Op::Contains(k) => {
prop_assert_eq!(set.contains(&k), model.contains(&k));
}
Op::Extend(items) => {
let expected: u64 = items
.iter()
.filter(|(k, _)| model.insert(k.clone()))
.map(|(_, w)| w)
.sum();
prop_assert_eq!(set.extend_weighted(items), expected);
}
Op::TryExtend(items, pos) => {
let len_before = set.len();
let (head, tail) = items.split_at(pos);
let iter = head
.iter()
.cloned()
.map(Ok)
.chain(std::iter::once(Err(())))
.chain(tail.iter().cloned().map(Ok));
prop_assert_eq!(set.try_extend_weighted(iter), Err(()));
prop_assert_eq!(set.len(), len_before);
for (k, _) in &items {
prop_assert_eq!(set.contains(k), model.contains(k));
}
}
}
prop_assert_eq!(set.len(), model.len());
prop_assert_eq!(set.is_empty(), model.is_empty());
prop_assert_eq!(set.shard_lens().iter().sum::<usize>(), model.len());
}
for k in &model {
prop_assert!(set.contains(k));
}
Ok(())
}
fn chunk_id() -> BoxedStrategy<ChunkId> {
prop_oneof![
any::<[u8; 12]>(),
(any::<u8>(), any::<u8>()).prop_map(|(head, tail)| {
let mut bytes = [7u8; 12];
bytes[0] = head;
bytes[11] = tail;
bytes
}),
(0..4u8).prop_map(|b| [b; 12]),
]
.prop_map(ChunkId::new)
.boxed()
}
proptest! {
#[test]
fn chunk_id_set_behaves_like_hash_set(ops in ops(chunk_id())) {
behaves_like_hash_set::<ChunkId, IdHasherBuilder>(ops)?;
}
#[test]
fn generic_set_behaves_like_hash_set(ops in ops(any::<u16>().boxed())) {
behaves_like_hash_set::<u16, RandomState>(ops)?;
}
}
#[test]
fn insert_contains_len() {
let set = ChunkIdSet::new();
let a = ChunkId::random();
let b = ChunkId::random();
assert!(set.insert(a.clone()));
assert!(!set.insert(a.clone()));
assert!(set.contains(&a));
assert!(!set.contains(&b));
assert_eq!(set.len(), 1);
}
#[test]
fn extend_weighted_counts_only_new_keys() {
let set = ChunkIdSet::new();
let ids: Vec<ChunkId> = (0..1000).map(|_| ChunkId::random()).collect();
let first = set.extend_weighted(ids.iter().cloned().map(|id| (id, 3)));
assert_eq!(first, 3000);
let again = set.extend_weighted(ids.iter().cloned().map(|id| (id, 3)));
assert_eq!(again, 0);
assert_eq!(set.len(), 1000);
for id in &ids {
assert!(set.contains(id));
}
}
#[test]
fn try_extend_weighted_stops_at_the_first_error() {
let set = ChunkIdSet::new();
let good = ChunkId::random();
let unreached = ChunkId::random();
let items = vec![Ok((good.clone(), 7)), Err("boom"), Ok((unreached.clone(), 7))];
assert_eq!(set.try_extend_weighted(items), Err("boom"));
assert!(!set.contains(&good));
assert!(!set.contains(&unreached));
assert!(set.is_empty());
}
#[test]
fn random_ids_spread_evenly_over_shards() {
let set = ChunkIdSet::new();
let n = 1_000_000usize;
set.extend((0..n).map(|_| ChunkId::random()));
assert_eq!(set.len(), n);
let mean = n as f64 / SHARDS as f64;
for (i, len) in set.shard_lens().into_iter().enumerate() {
let ratio = len as f64 / mean;
assert!((0.8..1.2).contains(&ratio), "shard {i} holds {len}, mean {mean}");
}
}
#[test]
fn concurrent_extend_from_many_threads() {
let set = std::sync::Arc::new(ChunkIdSet::new());
let shared: Vec<ChunkId> = (0..50_000).map(|_| ChunkId::random()).collect();
let handles: Vec<_> = (0..16)
.map(|_| {
let set = std::sync::Arc::clone(&set);
let shared = shared.clone();
std::thread::spawn(move || {
set.extend(shared);
set.extend((0..10_000).map(|_| ChunkId::random()));
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(set.len(), 50_000 + 16 * 10_000);
for id in &shared {
assert!(set.contains(id));
}
}
#[test]
fn generic_keys_use_random_state() {
let set: ShardedSet<(String, u64)> = ShardedSet::new();
assert_eq!(
set.extend_weighted([
(("a".to_string(), 1), 10),
(("a".to_string(), 1), 10),
(("b".to_string(), 2), 5),
]),
15
);
assert!(set.contains(&("a".to_string(), 1)));
assert_eq!(set.len(), 2);
}
}