Skip to content

Remove RandomMultiPair and make (key, value) the only entry identity - #13

Merged
pathscale merged 3 commits into
masterfrom
remove-random-multipair
Sep 1, 2026
Merged

pathscale merged 3 commits into
masterfrom
remove-random-multipair

Conversation

@pathscale

Copy link
Copy Markdown
Owner

Closes the quadratic-insert regression in 0.0.8 by removing the representation that caused it.

The regression

RandomMultiPair::insert_cdc_into scans every entry already stored under the key on each insert, because the value does not participate in the order and a stored entry cannot be located by it. O(n) per insert, quadratic to fill one key.

Reproducer, no WorkTable involved:

let m: BTreeMultiMap<String, u64> = BTreeMultiMap::new();
for i in 0..n { m.insert("one-generation".to_string(), i); }
values under one key 0.0.6 0.0.7 0.0.8 this branch
1,000 288 ns 259 ns 21.95 us 877 ns
4,000 325 ns 320 ns 87.13 us 746 ns
16,000 408 ns 416 ns 356.31 us 333 ns

Distinct keys recover as well, 5.57 us to 285 ns.

This reaches consumers hard. AgentCode indexes a generation under one snapshot_id, so 14,400 rows land on one key: a one-file incremental update went from 698 ms on beta.13 to 15.1 s on beta.14, and marginal cost per row from 9.04 us to 330 us. The microbenchmark above predicts 318 us/row at 14,400, which accounts for the production figure to within 4%.

Why removal rather than repair

Both problems were inherent to ordering by (key, random discriminator):

  • Before 0.0.8, Ord consulted value equality before the discriminator, which is not transitive. Two prior audits already flagged this as P1.
  • 0.0.8 made the order lawful, which necessarily removed the value from it, which necessarily made insert a scan.

OrdMultiPair has neither problem by construction: identity is the (key, value) pair, lexicographically ordered, so the order is lawful and a stored entry is found by binary search. Duplicate replacement falls out of it.

It is also never slower. Measured across sizes from two values per key upward, Ord won everywhere, by 1.83x at n=2 and 68x at n=4096. There is no size at which the random representation paid for itself.

Nothing consumes RandomMultiPair: not WorkTable, not any consumer in this organisation. Only this crate's own tests referenced it.

What changed

  • RandomMultiPair deleted; MultiPair is now OrdMultiPair.
  • The reasoning is recorded on MultiPair so the type is not reintroduced.
  • One test asserted that replacing a logically equal pair preserved its discriminator. That property no longer exists, so the test goes with the type. Two others built nodes in discriminator order and now build them in (key, value) order.
  • Version 0.0.9.

Tests

106 pass with concurrent,cdc,multimap, 34 with default features. The doc-test failures on this crate are pre-existing and identical before and after.

Note for WorkTable

WorkTable needs a matching change, which is small: MultiPairRecreate no longer takes a discriminator, and the reconstruction path stops synthesising one. There is one open decision there, which is that the persisted index format currently encodes insertion order within a key while (key, value) order is required. Detail in the companion issue.

RandomMultiPair ordered entries by (key, random discriminator) so that a V which was only
PartialEq could still be stored in a multimap. Both of the things that follow from that
were inherent to the design rather than bugs sitting on top of it.

Its Ord consulted value equality before the discriminator, which is not transitive, so
binary search over nodes was unreliable, same-key churn accumulated duplicate logical
pairs, and once same-key nodes split the skip map held entry keys comparing Equal, which
corrupted routing and livelocked the split retry loop. Making the order lawful, as 0.0.8
did, meant the value stopped participating in it, so a stored entry could no longer be
found by its value and insert had to scan every entry sharing the key. That is O(n) per
insert and quadratic to fill one key.

Measured, one key, values inserted one at a time:

  values     0.0.7      0.0.8     this
   1,000    259 ns   21.95 us   877 ns
   4,000    320 ns   87.13 us   746 ns
  16,000    416 ns  356.31 us   333 ns

356 us per insert at 16,000 values, against 333 ns here. The distinct-key case recovers
too, 5.57 us to 285 ns. OrdMultiPair was never slower at any size, including two values
per key, so there was no size at which the random representation paid for itself.

OrdMultiPair, whose identity is the (key, value) pair lexicographically ordered, is now
the only representation. It is a lawful total order, a stored entry is located by binary
search, and duplicate replacement falls out of the order rather than needing a scan. The
niche the random representation served, a V that is PartialEq but not Ord, does not
justify a second representation: wrap such a value in a newtype with a total order.

The type is gone rather than deprecated, and the reasoning is recorded on MultiPair so it
is not reintroduced. One test asserted that replacing a logically equal pair preserved its
discriminator; that property no longer exists and the test goes with the type. Two tests
built nodes in discriminator order and now build them in (key, value) order.
meh added 2 commits September 1, 2026 14:38
…d it

remove_where, remove_where_inner and remove_where_cdc walked a node looking for the first
element satisfying a predicate. Only RandomMultiPair used them: its Ord did not include
the value, so removing a logical (key, value) pair meant finding it by scanning. Ord
identity removes by binary search on the pair itself, so nothing reaches them now.

That is one more unbounded scan gone from the set alongside the one in insert, and the
three tests that exercised it go with it.
The tests that exercised the random representation are gone, and with them the only uses
of ChangeEvent and OrdMultiPair inside the set test module.
@pathscale
pathscale merged commit 05a74bd into master Sep 1, 2026
5 checks passed
@pathscale
pathscale deleted the remove-random-multipair branch September 1, 2026 08:01
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.

1 participant