Skip to content

perf: sharded OnceTable - #225

Open
Huliiiiii wants to merge 5 commits into
apache:mainfrom
Huliiiiii:issue-200
Open

perf: sharded OnceTable#225
Huliiiiii wants to merge 5 commits into
apache:mainfrom
Huliiiiii:issue-200

Conversation

@Huliiiiii

Copy link
Copy Markdown

Alternative of #205

Improvements

CPU: AMD Ryzen 9 9950X
OS: NixOS 25.11

Thread: 8

Benchmark (entry size) Baseline #205 This PR (Mutex) This PR vs. #205 parking_lot::RwLock RwLock vs. #205 RwLock vs. This PR
once_map::contended_compute_coalesced 7.027 µs 6.650 µs 6.906 µs 3.7% slower 6.666 µs 0.2% slower 3.5% faster
once_map::contended_compute_hit_same_key 964 ns 296 ns 1.113 µs 73.4% slower 312 ns 5.1% slower 3.6× faster
once_map::contended_compute_hit_disjoint (64) 1.114 µs 357 ns 110 ns 3.2× faster 103 ns 3.5× faster 6.3% faster
once_map::contended_compute_hit_disjoint (1024) 1.156 µs 296 ns 94 ns 3.1× faster 103 ns 2.9× faster 8.4% slower
once_map::contended_compute_miss_churn (64) 3.552 µs 4.979 µs 208 ns 23.9× faster 223 ns 22.4× faster 6.6% slower
once_map::contended_compute_miss_churn (1024) 3.542 µs 4.867 µs 223 ns 21.8× faster 231 ns 21.1× faster 3.4% slower
once_map::contended_compute_mixed (64) 2.589 µs 3.305 µs 163 ns 20.3× faster 166 ns 19.9× faster 1.9% slower
once_map::contended_compute_mixed (1024) 2.513 µs 3.200 µs 169 ns 18.9× faster 168 ns 19.0× faster 0.4% faster
once_map::contended_get_hit_same_key 852 ns 322 ns 960 ns 66.5% slower 638 ns 49.5% slower 1.5× faster
once_map::contended_get_hit_disjoint (64) 935 ns 464 ns 89 ns 5.2× faster 124 ns 3.8× faster 28.0% slower
once_map::contended_get_hit_disjoint (1024) 963 ns 461 ns 65 ns 7.1× faster 70 ns 6.6× faster 7.4% slower
singleflight::contended_work_coalesced 6.746 µs 6.937 µs 7.351 µs 5.6% slower 6.594 µs 4.9% faster 10.3% faster
singleflight::contended_work_same_key 1.881 µs 2.167 µs 1.719 µs 1.3× faster 1.790 µs 1.2× faster 4.0% slower
singleflight::contended_work_disjoint_churn 3.533 µs 4.780 µs 217 ns 22.0× faster 221 ns 21.7× faster 1.6% slower

Thread: 32

Benchmark (entry size) Baseline #205 This PR (Mutex) This PR vs. #205 parking_lot::RwLock RwLock vs. #205 RwLock vs. This PR
once_map::contended_compute_coalesced 28.900 µs 31.010 µs 29.900 µs 3.6% faster 46.520 µs 33.3% slower 35.7% slower
once_map::contended_compute_hit_same_key 3.915 µs 2.576 µs 6.607 µs 61.0% slower 2.097 µs 18.6% faster 3.2× faster
once_map::contended_compute_hit_disjoint (64) 5.513 µs 3.004 µs 882 ns 3.4× faster 278 ns 10.8× faster 3.2× faster
once_map::contended_compute_hit_disjoint (1024) 5.981 µs 2.869 µs 334 ns 8.6× faster 243 ns 11.8× faster 1.4× faster
once_map::contended_compute_miss_churn (64) 29.290 µs 42.010 µs 948 ns 44.3× faster 1.246 µs 33.7× faster 23.9% slower
once_map::contended_compute_miss_churn (1024) 26.960 µs 38.790 µs 988 ns 39.3× faster 1.257 µs 30.9× faster 21.4% slower
once_map::contended_compute_mixed (64) 19.780 µs 25.620 µs 822 ns 31.2× faster 872 ns 29.4× faster 5.7% slower
once_map::contended_compute_mixed (1024) 19.140 µs 24.010 µs 721 ns 33.3× faster 780 ns 30.8× faster 7.6% slower
once_map::contended_get_hit_same_key 3.428 µs 2.098 µs 5.190 µs 59.6% slower 2.954 µs 29.0% slower 1.8× faster
once_map::contended_get_hit_disjoint (64) 4.849 µs 2.755 µs 623 ns 4.4× faster 319 ns 8.6× faster 2.0× faster
once_map::contended_get_hit_disjoint (1024) 4.896 µs 2.810 µs 325 ns 8.6× faster 234 ns 12.0× faster 1.4× faster
singleflight::contended_work_coalesced 30.750 µs 28.620 µs 35.160 µs 18.6% slower 32.200 µs 11.1% slower 8.4% faster
singleflight::contended_work_same_key 12.920 µs 13.960 µs 13.120 µs 6.0% faster 17.480 µs 20.1% slower 24.9% slower
singleflight::contended_work_disjoint_churn 28.160 µs 39.170 µs 916 ns 42.8× faster 1.274 µs 30.7× faster 28.1% slower

@aisk

aisk commented Aug 27, 2026

Copy link
Copy Markdown

Verified on bare-metal Linux: AMD Ryzen 7 5700X (8C/16T), NixOS 26.11, kernel 7.2.0, rustc 1.96.0, t=8, --sample-count 100 --sample-size 10000, interleaved rounds ×3 against base (c461305) and #205's head (6c55ca4). Values are medians of the 3 round-medians; per-round variance was <5% throughout, including the churn scenarios.

scenario base this PR #205 (6c55ca4)
compute_hit_same_key 809.1 ns 810.3 ns 165.9 ns
get_hit_same_key 547.9 ns 599.0 ns 343.9 ns
compute_hit_disjoint (64) 908.3 ns 100.9 ns 168.5 ns
compute_hit_disjoint (1024) 913.0 ns 96.3 ns 174.7 ns
get_hit_disjoint (64) 715.6 ns 84.2 ns 518.9 ns
get_hit_disjoint (1024) 727.3 ns 78.8 ns 530.8 ns
compute_miss_churn (64) 3.06 µs 223.6 ns 4.05 µs
compute_miss_churn (1024) 2.91 µs 232.5 ns 3.92 µs
compute_mixed (64) 2.21 µs 175.1 ns 2.63 µs
compute_mixed (1024) 2.16 µs 179.0 ns 2.54 µs
compute_coalesced 18.41 µs 18.53 µs 18.47 µs
work_same_key 2.07 µs 1.80 µs 2.14 µs
work_disjoint_churn 3.02 µs 233.4 ns 3.74 µs
work_coalesced 18.58 µs 18.47 µs 18.67 µs
  • The headline wins reproduce: 8.5–9.5× on disjoint hits, 12–13.7× on miss_churn/mixed/work_disjoint_churn (below the 16–24× on the 9950X, still decisive).
  • The same-key penalty is milder here: get_hit_same_key −9% vs base, compute_hit_same_key on par, work_same_key +15%.
  • Same-key hits are the one weak spot vs perf: take the keyed table read lock on hits and duplicate waiters #205: every hit takes the shard mutex exclusively, so same-key traffic serializes on one shard — 810 vs 166 ns on compute hits (4.9×), 599 vs 344 ns on get (1.7×).
  • Question: the branch only contains the Mutex-shard variant; the parking_lot::RwLock column from the table doesn't seem to be in the code. Is that variant pushed anywhere?

The two approaches look complementary: sharding covers disjoint/churn, a shared reader path plus the hit shortcut from 6c55ca4 covers same-key. A per-shard reader path on top of this PR should cover every scenario — happy to help with that.

@Huliiiiii

Huliiiiii commented Aug 27, 2026

Copy link
Copy Markdown
Author

Refactor the storage of OnceMap, add a concurrent index for Ready values.
This makes the current implementation significantly superior to other solutions when using two or more threads.

Also fixed some issues in benchmarks, but the results still show excessive relative range.

Threads Benchmark Range
1 compute_hit_disjoint (64) Current 11.3%
1 compute_hit_disjoint (1024) Current 10.8%
1 compute_hit_same_key RwLock 18.7%
2 compute_hit_disjoint (64) Mutex 46.9%;RwLock 27.4%
2 compute_hit_disjoint (1024) Baseline 11.5%;RwLock 13.9%
2 compute_hit_same_key Baseline 67.2%;Mutex 33.1%
2 compute_miss_churn (64) Baseline 15.8%
2 compute_miss_churn (1024) Baseline 12.1%
2 compute_mixed (64) Baseline 10.3%
2 compute_mixed (1024) Baseline 12.0%
2 get_hit_disjoint (64) Baseline 27.8%;Mutex 53.3%;RwLock 78.4%
2 get_hit_disjoint (1024) Baseline 22.6%
2 get_hit_same_key Baseline 14.2%;Mutex 13.8%;RwLock 19.9%
8 compute_hit_disjoint (64) Mutex 29.7%;RwLock 38.7%
8 compute_hit_disjoint (1024) Baseline 12.1%;Mutex 39.2%
8 compute_hit_same_key Baseline 16.2%;Mutex 11.5%;RwLock 46.9%
8 compute_miss_churn (64) Mutex 11.8%;RwLock 12.7%
8 compute_miss_churn (1024) Baseline 12.9%;Mutex 14.0%;Current 18.8%
8 compute_mixed (64) RwLock 17.6%
8 get_hit_disjoint (64) Baseline 17.8%;Mutex 51.8%;RwLock 45.6%
8 get_hit_disjoint (1024) Baseline 14.0%;Mutex 15.5%;RwLock 13.0%;Current 28.8%
8 get_hit_same_key Baseline 18.3%;RwLock 36.5%
32 compute_hit_disjoint (64) Baseline 10.3%;Mutex 20.5%;RwLock 29.9%
32 compute_hit_disjoint (1024) Mutex 10.8%;RwLock 25.3%
32 compute_hit_same_key Baseline 19.1%;RwLock 48.5%
32 compute_miss_churn (64) Baseline 16.4%
32 compute_miss_churn (1024) Baseline 18.3%
32 compute_mixed (64) Baseline 15.0%
32 compute_mixed (1024) Baseline 14.2%
32 get_hit_disjoint (64) Baseline 15.4%;Mutex 33.3%;RwLock 41.7%
32 get_hit_disjoint (1024) Baseline 15.3%;Mutex 11.6%
32 get_hit_same_key Baseline 16.4%;RwLock 20.8%

New Result:

1 thread

Benchmark Baseline (ns) Mutex (ns) RwLock (ns) Current (ns) Mutex vs. baseline RwLock vs. baseline Current vs. baseline Current vs. Mutex Current vs. RwLock
once_map::contended_compute_hit_disjoint (64) 31.42 41.43 36.30 30.96 31.9% slower 15.5% slower On par 33.8% faster 17.2% faster
once_map::contended_compute_hit_disjoint (1024) 32.45 41.92 37.24 31.78 29.2% slower 14.8% slower 2.1% faster 31.9% faster 17.2% faster
once_map::contended_compute_hit_same_key 29.56 37.03 35.14 28.05 25.3% slower 18.9% slower 5.4% faster 32.0% faster 25.3% faster
once_map::contended_compute_miss_churn (64) 104.7 109.8 119.0 122.0 4.9% slower 13.7% slower 16.5% slower 11.1% slower 2.5% slower
once_map::contended_compute_miss_churn (1024) 104.6 114.1 121.6 124.2 9.1% slower 16.3% slower 18.7% slower 8.9% slower 2.1% slower
once_map::contended_compute_mixed (64) 69.88 78.30 80.45 78.69 12.0% slower 15.1% slower 12.6% slower On par 2.2% faster
once_map::contended_compute_mixed (1024) 73.61 80.81 82.23 80.52 9.8% slower 11.7% slower 9.4% slower On par 2.1% faster
once_map::contended_get_hit_disjoint (64) 18.29 23.93 24.16 17.02 30.8% slower 32.1% slower 7.5% faster 40.6% faster 42.0% faster
once_map::contended_get_hit_disjoint (1024) 18.29 24.05 24.58 17.08 31.5% slower 34.4% slower 7.1% faster 40.8% faster 43.9% faster
once_map::contended_get_hit_same_key 17.48 23.02 26.91 16.14 31.7% slower 53.9% slower 8.3% faster 42.6% faster 66.7% faster

2 threads

Benchmark Baseline (ns) Mutex (ns) RwLock (ns) Current (ns) Mutex vs. baseline RwLock vs. baseline Current vs. baseline Current vs. Mutex Current vs. RwLock
once_map::contended_compute_hit_disjoint (64) 134.6 53.98 37.79 33.84 2.49× faster 3.56× faster 3.98× faster 59.5% faster 11.7% faster
once_map::contended_compute_hit_disjoint (1024) 131.9 53.71 47.17 34.06 2.46× faster 2.80× faster 3.87× faster 57.7% faster 38.5% faster
once_map::contended_compute_hit_same_key 114.2 156.9 111.0 30.75 37.4% slower 2.9% faster 3.71× faster 5.10× faster 3.61× faster
once_map::contended_compute_miss_churn (64) 482.7 141.4 153.0 143.6 3.41× faster 3.15× faster 3.36× faster On par 6.5% faster
once_map::contended_compute_miss_churn (1024) 438.3 150.0 156.1 146.4 2.92× faster 2.81× faster 2.99× faster 2.5% faster 6.6% faster
once_map::contended_compute_mixed (64) 321.2 96.29 99.03 88.75 3.34× faster 3.24× faster 3.62× faster 8.5% faster 11.6% faster
once_map::contended_compute_mixed (1024) 316.1 107.9 111.3 90.82 2.93× faster 2.84× faster 3.48× faster 18.8% faster 22.6% faster
once_map::contended_get_hit_disjoint (64) 115.9 24.16 24.55 17.26 4.80× faster 4.72× faster 6.71× faster 40.0% faster 42.2% faster
once_map::contended_get_hit_disjoint (1024) 120.0 28.88 29.78 17.35 4.16× faster 4.03× faster 6.92× faster 66.5% faster 71.6% faster
once_map::contended_get_hit_same_key 95.90 126.4 109.7 16.12 31.8% slower 14.4% slower 5.95× faster 7.84× faster 6.81× faster

8 threads

Benchmark Baseline (ns) Mutex (ns) RwLock (ns) Current (ns) Mutex vs. baseline RwLock vs. baseline Current vs. baseline Current vs. Mutex Current vs. RwLock
once_map::contended_compute_hit_disjoint (64) 1050 61.67 59.41 34.11 17.03× faster 17.67× faster 30.78× faster 80.8% faster 74.2% faster
once_map::contended_compute_hit_disjoint (1024) 1133 69.98 68.24 34.61 16.19× faster 16.60× faster 32.74× faster 2.02× faster 97.2% faster
once_map::contended_compute_hit_same_key 915.0 1086 296.3 31.01 18.7% slower 3.09× faster 29.51× faster 35.02× faster 9.55× faster
once_map::contended_compute_miss_churn (64) 3713 224.5 232.1 173.2 16.54× faster 16.00× faster 21.44× faster 29.6% faster 34.0% faster
once_map::contended_compute_miss_churn (1024) 3706 225.3 234.4 172.1 16.45× faster 15.81× faster 21.53× faster 30.9% faster 36.2% faster
once_map::contended_compute_mixed (64) 2564 150.3 157.1 99.60 17.06× faster 16.32× faster 25.74× faster 50.9% faster 57.7% faster
once_map::contended_compute_mixed (1024) 2596 161.4 164.7 103.2 16.08× faster 15.76× faster 25.16× faster 56.4% faster 59.6% faster
once_map::contended_get_hit_disjoint (64) 918.8 50.55 45.70 17.51 18.18× faster 20.11× faster 52.47× faster 2.89× faster 2.61× faster
once_map::contended_get_hit_disjoint (1024) 954.1 54.43 49.86 17.47 17.53× faster 19.14× faster 54.61× faster 3.12× faster 2.85× faster
once_map::contended_get_hit_same_key 793.7 958.9 638.1 16.13 20.8% slower 24.4% faster 49.21× faster 59.45× faster 39.56× faster

32 threads

Benchmark Baseline (ns) Mutex (ns) RwLock (ns) Current (ns) Mutex vs. baseline RwLock vs. baseline Current vs. baseline Current vs. Mutex Current vs. RwLock
once_map::contended_compute_hit_disjoint (64) 5355 159.2 126.4 45.80 33.64× faster 42.37× faster 116.92× faster 3.48× faster 2.76× faster
once_map::contended_compute_hit_disjoint (1024) 6356 302.4 163.8 46.37 21.02× faster 38.80× faster 137.07× faster 6.52× faster 3.53× faster
once_map::contended_compute_hit_same_key 4351 6504 2154 39.26 49.5% slower 2.02× faster 110.83× faster 165.66× faster 54.87× faster
once_map::contended_compute_miss_churn (64) 29150 1015 1038 440.1 28.72× faster 28.08× faster 66.23× faster 2.31× faster 2.36× faster
once_map::contended_compute_miss_churn (1024) 28260 1022 1035 422.7 27.65× faster 27.30× faster 66.86× faster 2.42× faster 2.45× faster
once_map::contended_compute_mixed (64) 20140 684.0 678.3 224.5 29.44× faster 29.69× faster 89.71× faster 3.05× faster 3.02× faster
once_map::contended_compute_mixed (1024) 19570 729.3 706.2 227.6 26.83× faster 27.71× faster 85.98× faster 3.20× faster 3.10× faster
once_map::contended_get_hit_disjoint (64) 4498 123.6 110.8 25.55 36.39× faster 40.60× faster 176.05× faster 4.84× faster 4.34× faster
once_map::contended_get_hit_disjoint (1024) 4953 269.2 161.0 25.81 18.40× faster 30.76× faster 191.90× faster 10.43× faster 6.24× faster
once_map::contended_get_hit_same_key 3834 5220 2665 24.41 36.2% slower 43.9% faster 157.07× faster 213.85× faster 109.18× faster

@Huliiiiii
Huliiiiii force-pushed the issue-200 branch 11 times, most recently from a6b45fb to 9d1ee47 Compare August 28, 2026 05:25
Comment thread asyncband/src/once/once_map/table.rs Outdated
}

if entry.was_indexed.load(Ordering::Relaxed) {
self.index.remove_if_sync(&EntryIdentity(entry), |()| true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Drop entries when they are discarded

HashIndex::remove_if_sync only marks an entry unreachable and explicitly permits its memory to be reclaimed later. Because ReadyEntry owns a strong Arc<Entry<K, V>>, removing the entry from the shard does not release its key or value. The following integration test fails on this head with left: 0, right: 1, while it passes against 0.6.7:

#[test]
fn discard_drops_the_removed_value() {
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};

    use asyncband::once::OnceMap;

    #[derive(Clone)]
    struct DropCounter(Arc<AtomicUsize>);

    impl Drop for DropCounter {
        fn drop(&mut self) {
            self.0.fetch_add(1, Ordering::SeqCst);
        }
    }

    let drops = Arc::new(AtomicUsize::new(0));
    let map: OnceMap<_, _> =
        [(0, DropCounter(Arc::clone(&drops)))].into_iter().collect();

    map.discard(&0);

    assert_eq!(drops.load(Ordering::SeqCst), 1);
}

This is the documented reclamation behavior of HashIndex::remove_if_sync, but it is observable here because the deferred index entry owns the map's key and value transitively. Under insert/discard churn, resources can accumulate until the index collector—or the whole OnceMap—is dropped. Please avoid retaining a strong Arc in the deferred-reclamation index, or otherwise ensure that strong ownership is released before discard returns.

// So I use 8 as the coefficient, which is 256 / 32.
// Need to test on other machines to see if this coefficient is optimal.
// Dashmap use 4.
(std::thread::available_parallelism().map_or(1, |parallelism| parallelism.get()) * 8)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Bound or expose the eager shard allocation

Eager CPU-scaled sharding has precedent, but this exact default is unusually aggressive for OnceMap and especially singleflight::Group. DashMap uses available_parallelism * 4, eagerly creates its shards, and exposes with_shard_amount constructors so callers can trade contention for footprint. The canonical Go singleflight implementation instead lazily initializes one mutex-protected map, and scc::HashIndex, already used by this PR, starts with zero capacity.

This implementation uses available_parallelism * 8 with no override. On this 14-way host that becomes 128 #[repr(align(64))] shards. An allocator probe measured:

  • OnceMap::new() and Group::new(): 1 allocation / 8,192 bytes, versus 0 allocations in 0.6.7.
  • OnceMap::with_capacity(1): 132 allocations / 14,592 bytes, versus 1 allocation / 44 bytes in 0.6.7.

On the 32-core machine used to choose the coefficient, every empty instance starts with 256 shards and at least 16 KiB of cache-line-padded storage. The throughput results establish the benefit for a hot shared table, but do not cover construction or workloads containing many empty/small maps and groups.

Could we add construction/allocation benchmarks and either expose an explicit shard-count constructor with a more conservative or capped default, or allocate shard storage lazily? The sharding optimization itself is justified; the concern is making its most aggressive configuration an unavoidable per-instance cost.

@Huliiiiii

Huliiiiii commented Aug 28, 2026

Copy link
Copy Markdown
Author

Removed additional dependencies, use std::RwLock instead of scc::HashIndex:

32 threads

Benchmark Baseline (ns) Before (ns) Current (ns) Before vs. baseline Current vs. baseline Before vs. Current
once_map::contended_compute_hit_disjoint (64) 5355 45.80 87.51 116.92× faster 61.19× faster 1.91× faster
once_map::contended_compute_hit_disjoint (1024) 6356 46.37 76.73 137.07× faster 82.84× faster 1.65× faster
once_map::contended_compute_hit_same_key 4351 39.26 1392 110.83× faster 3.13× faster 35.46× faster
once_map::contended_compute_miss_churn (64) 29150 440.1 469.9 66.23× faster 62.03× faster 1.07× faster
once_map::contended_compute_miss_churn (1024) 28260 422.7 435.2 66.86× faster 64.94× faster 1.03× faster
once_map::contended_compute_mixed (64) 20140 224.5 282.1 89.71× faster 71.39× faster 1.26× faster
once_map::contended_compute_mixed (1024) 19570 227.6 262.6 85.98× faster 74.52× faster 1.15× faster
once_map::contended_get_hit_disjoint (64) 4498 25.55 95.87 176.05× faster 46.92× faster 3.75× faster
once_map::contended_get_hit_disjoint (1024) 4953 25.81 58.56 191.90× faster 84.58× faster 2.27× faster
once_map::contended_get_hit_same_key 3834 24.41 2132 157.07× faster 1.80× faster 87.34× faster

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants