perf(sorted_set): compute union size from split drops instead of recounting - #3829
Conversation
Coverage Report for CI Build 6217Coverage decreased (-0.006%) to 90.718%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
…unting
Resolves the TODO in union: after merging, the result size was
recomputed by a full each() traversal of the merged tree. An element
common to both sets is dropped exactly once, at the found pivot of the
split, so union now counts those drops and derives the size
arithmetically: size = |self| + |src| - dups.
Rebased reimplementation: when this PR was first written, union's
`split` was the only splitter and gained an Int drop-count component.
main has since grown `split_member` (the split-based difference/
intersection/symmetric_difference rewrite), whose found flag carries
exactly the same information - so union now uses `split_member`, and
the now-unused `split` is deleted along with its whitebox test, which
is replaced by an equivalent `split_member` test pinning found for
present, absent, below-range, above-range and empty splits.
`split_member`'s result also becomes a `#valtype` struct
(`SplitResult`: left/found/right) instead of a 3-tuple, keeping the
per-node result stack-allocated on native (same pattern as
`JsonNumberScan` in json/lex_number.mbt). This benefits every
split-based operation, not just union.
Benchmarks (moon bench --release, n=10K sets, means of 10x batches):
union, main -> this branch (recount removed + valtype):
50% overlap native 518 -> 429us (-17%), js 297 -> 222us (-25%),
wasm-gc 197 -> 158us (-20%)
disjoint native 382 -> 304us (-20%), js 261 -> 153us (-41%),
wasm-gc 154 -> 96us (-38%)
identical native 592 -> 520us (-12%), js 323 -> 277us (-14%),
wasm-gc 232 -> 193us (-17%)
valtype struct vs tuple (isolated, all split-based ops):
native -5% to -9% across union/difference/intersection;
js flat; wasm-gc within noise (+/-2%).
Tests: union exact-size assertions for overlapping, disjoint and
identical operands cross-checked against an element count; the
split_member whitebox test above; the existing invariant and
quickcheck suites. Mutation-verified: double-counting dups fails 8
tests including the exact-size test and the whitebox invariant
scenarios.
Partially addresses #3824 (the defensive copy_tree of both operands is
inherent to the mutable node design and stays).
Codex CLI review (xhigh, first-round sign-off on the rebased
reimplementation): the dups induction re-verified on split_member
(disjoint fragments, pivot counted exactly once, empty-side arms cannot
hide common elements), sizes read from unmutated copies, SplitResult
preserves tuple semantics at all four call sites, no split callers
remain, interface byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Codex CLI <codex@openai.com>
3386b64 to
f0aacb8
Compare
Codex CLI review — post-rebase (
|
There was a problem hiding this comment.
Pull request overview
This PR optimizes @sorted_set’s SortedSet::union by removing the post-merge each() traversal used to recompute the result size, addressing the performance TODO noted in #3824. It derives the union size as |self| + |src| - dups, where dups is counted during split_member pivots, and refactors split_member to return a #valtype result struct (replacing the old tuple and deleting the now-unused split helper).
Changes:
- Compute
unionresult size via counted duplicate pivots (found) instead of recounting viaeach(). - Replace tuple-returning
split_memberwith a#valtypeSplitResultstruct and update all internal call sites; delete the unusedsplitimplementation. - Add a regression test asserting
union’s stored size matches actual element count for overlap/disjoint/identical cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| sorted_set/set.mbt | Removes union recount pass by counting split_member.found duplicates; refactors split result to a #valtype struct and deletes unused split. |
| sorted_set/set_test.mbt | Adds regression coverage ensuring union.length() remains exact and matches a real traversal count. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Partially addresses #3824 — resolves the
TODO: optimize this. Avoid counting the size of the setinunion.Before
After building the merged tree,
unionrecomputed the result size with a fulleach()traversal (an extra O(n+m) pass over the freshly built tree).After
An element common to both sets is dropped exactly once, at the found pivot of the split.
unioncounts those drops and derives the size arithmetically:|self| + |src| − dups. The recount pass is gone.Rebased reimplementation (2026-08-20)
When this PR was first written,
union'ssplitwas the only splitter, and it gained anIntdrop-count component. main has since grownsplit_member(the split-baseddifference/intersection/symmetric_differencerewrite), whosefoundflag carries exactly the same information — so the rebase re-implements the intent on today's machinery:unionusessplit_memberand countsfoundpivots.splitis deleted; its whitebox test is replaced by an equivalentsplit_membertest pinningfoundfor present/absent/below-range/above-range/empty splits.split_memberreturns a#valtypestruct (SplitResult { left; found; right }) instead of a 3-tuple, keeping the per-node result stack-allocated on native — same pattern asJsonNumberScaninjson/lex_number.mbt. This benefits every split-based operation, not just union.Benchmarks (
moon bench --release, n=10K sets)union, main → this branch (recount removal + valtype):#valtypestruct vs tuple, isolated (affects union/difference/intersection/symmetric_difference): native −5% to −9%, js flat, wasm-gc within ±2% noise.Tests
Union exact-size assertions for overlapping/disjoint/identical operands cross-checked against an element count; the
split_memberwhitebox test; the existing invariant and quickcheck suites all exercise the new path. Mutation-verified: double-countingdupsfails 8 tests.Review
Reviewed by Codex CLI (
xhigh) on the rebased reimplementation, first-round sign-off: the dups induction re-verified onsplit_member(disjoint fragments, pivot counted exactly once, empty-side arms cannot hide common elements), sizes read from unmutated copies,SplitResultpreserves tuple semantics at all four call sites, nosplitcallers remain, generated interface byte-identical. (The original pre-rebase version carried its own sign-off with the equivalent induction.)Signed-off-by: Codex CLI codex@openai.com
Validation
moon test: 7463/7463 (sorted_set 71/71)moon check --warn-list +unnecessary_annotationclean,moon fmtapplied, no.mbtichanges🤖 Generated with Claude Code