Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit 00b74fb

Browse files
authored
feat(hash_index): optimize garbage collection strategies (#212)
Instead of rebuilding the whole container, store the current epoch value when an entry is removed, and drop the entry when the global epoch has advanced at least twice.
1 parent 7e19bdf commit 00b74fb

8 files changed

Lines changed: 206 additions & 153 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
## Version 3
55

6+
3.4.6
7+
8+
* Improve `HashIndex` entry slot recycling strategies.
9+
610
3.4.5
711

812
* Minor `Hash*` performance improvements.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "scc"
33
description = "A collection of high-performance containers providing both asynchronous and synchronous interfaces"
44
documentation = "https://docs.rs/scc"
5-
version = "3.4.5"
5+
version = "3.4.6"
66
authors = ["wvwwvwwv <wvwwvwwv@me.com>"]
77
edition = "2024"
88
rust-version = "1.85.0"
@@ -19,7 +19,7 @@ members = [".", "examples", "extended_tests"]
1919
equivalent = { version = "1.0", optional = true }
2020
loom = { version = "0.7", optional = true, features = ["checkpoint"] }
2121
saa = "5.3"
22-
sdd = "4.4"
22+
sdd = "4.5"
2323
serde = { version = "1.0", optional = true }
2424

2525
[features]

README.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,7 @@ let future_remove = hashset.remove_async(&1);
149149

150150
### Entry lifetime
151151

152-
`HashIndex` does not drop removed entries immediately; instead, they are dropped when one of the following conditions is met.
153-
154-
1. `HashIndex` is cleared or resized.
155-
2. Buckets full of removed entries occupy 50% of the capacity.
156-
157-
Those conditions do not guarantee that the removed entry will be dropped within a definite period of time; therefore, `HashIndex` would not be an optimal choice if the workload is write-heavy and the entry size is large.
152+
`HashIndex` does not drop removed entries immediately, instead, they are dropped when the bucket is accessed again after [`sdd`](https://crates.io/crates/sdd) has ensured that there are no potential readers of those entries. This implies that a removed entry can remain as long as there are potential readers or the bucket is not accessed. This makes `HashIndex` not an optimal choice if the workload is write-heavy and the entry size is large.
158153

159154
### Examples
160155

src/hash_index.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,9 @@ where
949949
while entry_ptr.move_to_next(&locked_bucket.writer) {
950950
let (k, v) = locked_bucket.entry_mut(&mut entry_ptr);
951951
if !pred(k, v) {
952-
locked_bucket.writer.mark_removed(&mut entry_ptr);
952+
locked_bucket
953+
.writer
954+
.mark_removed(&mut entry_ptr, &Guard::new());
953955
*removed = true;
954956
}
955957
}
@@ -990,7 +992,7 @@ where
990992
while entry_ptr.move_to_next(&locked_bucket.writer) {
991993
let (k, v) = locked_bucket.entry_mut(&mut entry_ptr);
992994
if !pred(k, v) {
993-
locked_bucket.writer.mark_removed(&mut entry_ptr);
995+
locked_bucket.writer.mark_removed(&mut entry_ptr, &guard);
994996
*removed = true;
995997
}
996998
}
@@ -1181,7 +1183,7 @@ where
11811183
}
11821184
}
11831185

1184-
/// Deallocates the supplied bucket array.
1186+
/// Deallocates garbage bucket arrays if they are unreachable.
11851187
fn dealloc_garbage(&self) {
11861188
let guard = Guard::new();
11871189
let head_ptr = self.garbage_chain.load(Acquire, &guard);
@@ -1207,7 +1209,7 @@ where
12071209
}
12081210
}
12091211
} else {
1210-
guard.accelerate();
1212+
guard.set_has_garbage();
12111213
}
12121214
}
12131215
}
@@ -1367,6 +1369,7 @@ where
13671369

13681370
#[inline]
13691371
fn defer_reclaim(&self, bucket_array: Shared<BucketArray<K, V, (), INDEX>>, guard: &Guard) {
1372+
guard.accelerate();
13701373
self.reclaim_memory();
13711374
self.garbage_epoch.swap(u8::from(guard.epoch()), Release);
13721375
let (Some(prev_head), _) = self
@@ -1707,7 +1710,11 @@ where
17071710
pub async fn remove_and_async(self) -> Option<OccupiedEntry<'h, K, V, H>> {
17081711
let hashindex = self.hashindex;
17091712
let mut entry_ptr = self.entry_ptr.clone();
1710-
self.locked_bucket.writer.mark_removed(&mut entry_ptr);
1713+
let guard = Guard::new();
1714+
self.locked_bucket
1715+
.writer
1716+
.mark_removed(&mut entry_ptr, &guard);
1717+
self.locked_bucket.set_has_garbage(&guard);
17111718
if let Some(locked_bucket) = self
17121719
.locked_bucket
17131720
.next_async(hashindex, &mut entry_ptr)
@@ -1755,7 +1762,11 @@ where
17551762
pub fn remove_and_sync(self) -> Option<Self> {
17561763
let hashindex = self.hashindex;
17571764
let mut entry_ptr = self.entry_ptr.clone();
1758-
self.locked_bucket.writer.mark_removed(&mut entry_ptr);
1765+
let guard = Guard::new();
1766+
self.locked_bucket
1767+
.writer
1768+
.mark_removed(&mut entry_ptr, &guard);
1769+
self.locked_bucket.set_has_garbage(&guard);
17591770
if let Some(locked_bucket) = self.locked_bucket.next_sync(hashindex, &mut entry_ptr) {
17601771
return Some(OccupiedEntry {
17611772
hashindex,
@@ -1875,7 +1886,11 @@ where
18751886
let entry_ptr = self
18761887
.locked_bucket
18771888
.insert(u64::from(partial_hash), (key, val));
1878-
self.locked_bucket.writer.mark_removed(&mut self.entry_ptr);
1889+
let guard = Guard::new();
1890+
self.locked_bucket
1891+
.writer
1892+
.mark_removed(&mut self.entry_ptr, &guard);
1893+
self.locked_bucket.set_has_garbage(&guard);
18791894
self.entry_ptr = entry_ptr;
18801895
}
18811896
}
@@ -2050,7 +2065,7 @@ where
20502065

20512066
let guard = Guard::new();
20522067
if let Some(current_array) = self.hashindex.bucket_array(&guard) {
2053-
self.try_shrink_or_rebuild(current_array, 0, &guard);
2068+
self.try_shrink(current_array, 0, &guard);
20542069
}
20552070
}
20562071
}

src/hash_map.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2377,7 +2377,7 @@ where
23772377

23782378
let guard = Guard::new();
23792379
if let Some(current_array) = self.hashmap.bucket_array(&guard) {
2380-
self.try_shrink_or_rebuild(current_array, 0, &guard);
2380+
self.try_shrink(current_array, 0, &guard);
23812381
}
23822382
}
23832383
}

src/hash_table.rs

Lines changed: 46 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -220,28 +220,6 @@ where
220220
num_entries * (current_array.len() / (sample_size * 2))
221221
}
222222

223-
/// Checks whether rebuilding the entire hash table is required.
224-
#[inline]
225-
fn check_rebuild(current_array: &BucketArray<K, V, L, TYPE>, sampling_index: usize) -> bool {
226-
let sample_size = current_array.sample_size();
227-
let sample_1 = sampling_index & (!(sample_size - 1));
228-
let sample_2 = if sample_1 == 0 {
229-
current_array.len() - sample_size
230-
} else {
231-
0
232-
};
233-
let mut num_buckets_to_rebuild = 0;
234-
for i in (sample_1..sample_1 + sample_size).chain(sample_2..(sample_2 + sample_size)) {
235-
if current_array.bucket(i).need_rebuild() {
236-
num_buckets_to_rebuild += 1;
237-
if num_buckets_to_rebuild >= sample_size {
238-
return true;
239-
}
240-
}
241-
}
242-
false
243-
}
244-
245223
/// Peeks an entry from the [`HashTable`].
246224
#[inline]
247225
fn peek_entry<'g, Q>(&self, key: &Q, guard: &'g Guard) -> Option<&'g (K, V)>
@@ -707,8 +685,11 @@ where
707685
}
708686

709687
if removed {
688+
if TYPE == INDEX {
689+
async_guard.guard().set_has_garbage();
690+
}
710691
if let Some(current_array) = self.bucket_array(async_guard.guard()) {
711-
self.try_shrink_or_rebuild(current_array, 0, async_guard.guard());
692+
self.try_shrink(current_array, 0, async_guard.guard());
712693
}
713694
}
714695
}
@@ -773,8 +754,11 @@ where
773754
}
774755

775756
if removed {
757+
if TYPE == INDEX {
758+
guard.set_has_garbage();
759+
}
776760
if let Some(current_array) = self.bucket_array(guard) {
777-
self.try_shrink_or_rebuild(current_array, 0, guard);
761+
self.try_shrink(current_array, 0, guard);
778762
}
779763
}
780764
}
@@ -1296,43 +1280,24 @@ where
12961280
}
12971281
}
12981282

1299-
/// Tries to shrink the [`HashTable`] to fit the estimated number of entries or rebuild it to
1300-
/// optimize the storage.
1301-
fn try_shrink_or_rebuild(
1302-
&self,
1303-
current_array: &BucketArray<K, V, L, TYPE>,
1304-
index: usize,
1305-
guard: &Guard,
1306-
) {
1283+
/// Tries to shrink the [`HashTable`] to fit the estimated number of entries it to optimize the
1284+
/// storage.
1285+
fn try_shrink(&self, current_array: &BucketArray<K, V, L, TYPE>, index: usize, guard: &Guard) {
13071286
if !current_array.has_linked_array() {
13081287
let minimum_capacity = self.minimum_capacity();
1309-
if TYPE == INDEX || current_array.num_slots() > minimum_capacity {
1288+
if current_array.num_slots() > minimum_capacity {
13101289
// Try to shrink if the estimated load factor is less than `1/8`.
13111290
let shrink_threshold = current_array.sample_size() * BUCKET_LEN / 8;
1312-
// Try to rebuild if half the samples need to be rebuilt.
1313-
let rebuild_threshold = (current_array.sample_size() / 2).max(1);
13141291
let mut num_entries = 0;
1315-
let mut num_buckets_to_rebuild = 0;
13161292
for i in 0..current_array.sample_size() {
13171293
let bucket = current_array.bucket((index + i) % current_array.len());
13181294
num_entries += bucket.len();
1319-
if num_entries >= shrink_threshold
1320-
&& (TYPE != INDEX
1321-
|| num_buckets_to_rebuild + (current_array.sample_size() - i)
1322-
< rebuild_threshold)
1323-
{
1295+
if num_entries >= shrink_threshold {
13241296
// Early exit.
13251297
return;
13261298
}
1327-
if TYPE == INDEX && bucket.need_rebuild() {
1328-
num_buckets_to_rebuild += 1;
1329-
if num_buckets_to_rebuild >= rebuild_threshold {
1330-
self.try_resize(current_array, index, guard);
1331-
return;
1332-
}
1333-
}
13341299
}
1335-
if TYPE != INDEX || num_entries <= shrink_threshold {
1300+
if num_entries <= shrink_threshold {
13361301
self.try_resize(current_array, index, guard);
13371302
}
13381303
}
@@ -1393,10 +1358,7 @@ where
13931358

13941359
let try_resize = new_capacity != capacity;
13951360
let try_drop_table = estimated_num_entries == 0 && minimum_capacity == 0;
1396-
let try_rebuild =
1397-
TYPE == INDEX && !try_resize && Self::check_rebuild(current_array, sampling_index);
1398-
1399-
if !try_resize && !try_drop_table && !try_rebuild {
1361+
if !try_resize && !try_drop_table {
14001362
// Nothing to do.
14011363
return;
14021364
}
@@ -1454,7 +1416,7 @@ where
14541416
self.defer_reclaim(bucket_array, guard);
14551417
}
14561418
}
1457-
} else if try_resize || try_rebuild {
1419+
} else if try_resize {
14581420
let new_bucket_array = unsafe {
14591421
Shared::new_unchecked(BucketArray::<K, V, L, TYPE>::new(
14601422
new_capacity,
@@ -1550,11 +1512,13 @@ impl<K: Eq + Hash, V, L: LruList, const TYPE: char> LockedBucket<K, V, L, TYPE>
15501512
H: BuildHasher,
15511513
{
15521514
let removed = self.writer.remove(self.data_block, entry_ptr);
1553-
self.try_shrink_or_rebuild(hash_table);
1515+
if self.len() == 0 {
1516+
self.try_shrink(hash_table, &Guard::new());
1517+
}
15541518
removed
15551519
}
15561520

1557-
/// Removes the entry and tries to shrink or rebuild the container.
1521+
/// Removes the entry and tries to shrink the container.
15581522
#[inline]
15591523
pub(crate) fn mark_removed<H, T: HashTable<K, V, H, L, TYPE>>(
15601524
self,
@@ -1565,26 +1529,36 @@ impl<K: Eq + Hash, V, L: LruList, const TYPE: char> LockedBucket<K, V, L, TYPE>
15651529
{
15661530
debug_assert_eq!(TYPE, INDEX);
15671531

1568-
self.writer.mark_removed(entry_ptr);
1569-
self.try_shrink_or_rebuild(hash_table);
1532+
let guard = Guard::new();
1533+
self.writer.mark_removed(entry_ptr, &guard);
1534+
self.set_has_garbage(&guard);
1535+
if self.writer.len() == 0 {
1536+
self.try_shrink(hash_table, &guard);
1537+
}
15701538
}
15711539

1572-
/// Tries to shrink or rebuild the container.
1540+
/// Sets that there can be a garbage entry in the bucket so the epoch should be advanced.
15731541
#[inline]
1574-
pub(crate) fn try_shrink_or_rebuild<H, T: HashTable<K, V, H, L, TYPE>>(self, hash_table: &T)
1542+
pub(crate) const fn set_has_garbage(&self, guard: &Guard) {
1543+
let sample_size = self.bucket_array().sample_size();
1544+
if self.bucket_index % (sample_size * sample_size) == 0 {
1545+
guard.set_has_garbage();
1546+
}
1547+
}
1548+
1549+
/// Tries to shrink the container.
1550+
#[inline]
1551+
pub(crate) fn try_shrink<H, T: HashTable<K, V, H, L, TYPE>>(self, hash_table: &T, guard: &Guard)
15751552
where
15761553
H: BuildHasher,
15771554
{
1578-
if (TYPE == INDEX && self.writer.need_rebuild()) || self.writer.len() == 0 {
1579-
let guard = Guard::new();
1580-
if let Some(current_array) = hash_table.bucket_array(&guard) {
1581-
if ptr::eq(current_array, self.bucket_array()) {
1582-
let bucket_index = self.bucket_index;
1583-
drop(self);
1584-
1585-
// Tries to shrink or rebuild the container after unlocking the bucket.
1586-
hash_table.try_shrink_or_rebuild(current_array, bucket_index, &guard);
1587-
}
1555+
if let Some(current_array) = hash_table.bucket_array(guard) {
1556+
if ptr::eq(current_array, self.bucket_array()) {
1557+
let bucket_index = self.bucket_index;
1558+
drop(self);
1559+
1560+
// Tries to shrink the container after unlocking the bucket.
1561+
hash_table.try_shrink(current_array, bucket_index, guard);
15881562
}
15891563
}
15901564
}
@@ -1607,7 +1581,7 @@ impl<K: Eq + Hash, V, L: LruList, const TYPE: char> LockedBucket<K, V, L, TYPE>
16071581
let len = self.bucket_array().len();
16081582

16091583
if self.writer.len() == 0 {
1610-
self.try_shrink_or_rebuild(hash_table);
1584+
self.try_shrink(hash_table, &Guard::new());
16111585
} else {
16121586
drop(self);
16131587
}
@@ -1649,7 +1623,7 @@ impl<K: Eq + Hash, V, L: LruList, const TYPE: char> LockedBucket<K, V, L, TYPE>
16491623
let len = self.bucket_array().len();
16501624

16511625
if self.writer.len() == 0 {
1652-
self.try_shrink_or_rebuild(hash_table);
1626+
self.try_shrink(hash_table, &Guard::new());
16531627
} else {
16541628
drop(self);
16551629
}

0 commit comments

Comments
 (0)