The epoch pin landed in beta.14 (7d0faaf, efdbe41). beta.14 was never adopted because of the multimap regression, so beta.15 is the first release anyone runs with it, and partition_ref is materially slower than the number the docs still quote.
Measured
partition_ref on 64 resident partitions, same keys, release, M4 Max:
| threads |
beta.13 |
beta.15 |
|
| 1 |
1.61 ns |
7.78 ns |
4.8x |
| 2 |
0.64 ns |
4.10 ns |
6.4x |
| 4 |
0.60 ns |
6.28 ns |
10.5x |
| 8 |
1.61 ns |
10.06 ns |
6.2x |
Single-threaded in a tighter loop it is 1.17 ns to 3.45 ns. contains, which takes no pin, is unaffected (1.18 ns to 0.59 ns).
docs/partitioned-tables-implementation.md still says:
| partition_ref | 0.71 ns | 0.79 ns |
partition_ref returns a borrow, touches no refcount, and is flat across the same sweep. Use it on the tick path.
That is now off by roughly 10x, and it is the sentence pointing trading at this call.
Where the cost is
Not crossbeam. EpochDomain::pin runs on every read:
LOCALS.with(|locals| {
let mut locals = locals.borrow_mut(); // RefCell write borrow
if let Some(pos) = locals.iter().position(..) // linear scan
{
if pos != 0 { locals.swap(0, pos); } // LRU move-to-front, a write
return locals[0].handle.pin();
}
...
So a TLS access, a RefCell write borrow, a linear scan and a move-to-front write happen before the actual pin, on the hot path, per call.
Two cheap fixes, in order
- Drop the move-to-front. With a small cap the scan is already cheap, and the swap is the only reason the hot path needs
borrow_mut rather than borrow. Removing it takes a write off every read.
- Add a one-entry fast path. A
Cell<(domain_id, *const LocalHandle)> checked before touching the Vec covers the overwhelmingly common case of one table being read repeatedly, and falls through to the existing cache otherwise.
A third option, skipping the pin entirely when nothing has ever been retired, is tempting but not sound as stated: a reader that observes "no retirements" and then loads the slot can still race a remove that publishes afterwards, because a reader outside the domain is invisible to the reclaimer. It would need readers to remain observable some other way, which is what epochs already are.
Reproducer
worktable!(name: Price, persist: false, partition_by: symbol_id: u16,
columns: { exchange_id: u8 primary_key, bid: f64 });
let set = Arc::new(PricePartitions::new());
for k in 0..64u16 { set.partition_or_create(k).unwrap(); }
// N threads, then: for i in 0..1_000_000 { black_box(set.partition_ref((i % 64) as u16)); }
Whatever the fix, the doc table needs correcting either way: right now it tells the tick path to use a call that is 10x its documented cost.
The epoch pin landed in beta.14 (
7d0faaf,efdbe41). beta.14 was never adopted because of the multimap regression, so beta.15 is the first release anyone runs with it, andpartition_refis materially slower than the number the docs still quote.Measured
partition_refon 64 resident partitions, same keys, release, M4 Max:Single-threaded in a tighter loop it is 1.17 ns to 3.45 ns.
contains, which takes no pin, is unaffected (1.18 ns to 0.59 ns).docs/partitioned-tables-implementation.mdstill says:That is now off by roughly 10x, and it is the sentence pointing trading at this call.
Where the cost is
Not crossbeam.
EpochDomain::pinruns on every read:So a TLS access, a
RefCellwrite borrow, a linear scan and a move-to-front write happen before the actual pin, on the hot path, per call.Two cheap fixes, in order
borrow_mutrather thanborrow. Removing it takes a write off every read.Cell<(domain_id, *const LocalHandle)>checked before touching theVeccovers the overwhelmingly common case of one table being read repeatedly, and falls through to the existing cache otherwise.A third option, skipping the pin entirely when nothing has ever been retired, is tempting but not sound as stated: a reader that observes "no retirements" and then loads the slot can still race a
removethat publishes afterwards, because a reader outside the domain is invisible to the reclaimer. It would need readers to remain observable some other way, which is what epochs already are.Reproducer
Whatever the fix, the doc table needs correcting either way: right now it tells the tick path to use a call that is 10x its documented cost.