Skip to content

perf(weighted_frequency): rescore retained candidates without copying them - #227

Closed
zzylol wants to merge 1 commit into
fix/weighted-frequency-shape-and-weight-checksfrom
perf/weighted-frequency-update-without-candidate-clones
Closed

zzylol wants to merge 1 commit into
fix/weighted-frequency-shape-and-weight-checksfrom
perf/weighted-frequency-update-without-candidate-clones

Conversation

@zzylol

@zzylol zzylol commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Why

With a candidate heap, WeightedFrequency::update is the per-sample hot path of a keyed top-k build. Each call cloned every retained candidate (its identity tuple and its hash key) to rebuild the heap, and scoring each candidate collected its row estimates into a fresh Vec. At capacity 100 that is about 200 heap allocations per sample.

What

update returns the same state as before, about twice as fast at capacity 100. No interface changes.

Stacked on #226 (shape and weight checks), which is stacked on #225. Retarget to main after those merge.

How

  • The update moves the candidates out of the heap (std::mem::take(..).into_vec()) instead of cloning them, and adds the new key only when it is not already retained. Before, the duplicate was added and then dropped by the sort-and-dedup in retain, which keeps the earlier entry, so the result is the same.
  • Scoring takes a caller-owned buffer: retain reuses one buffer for every candidate's CountSketch row estimates, and Count-Min takes the row minimum with no buffer.
  • Every retained score is still re-read after each update, since the update may have moved cells the candidates share.

Key code interfaces added / modified

None. (estimate_key_with is a private helper.)

Before this PR

Release build, 200,000 updates over 1,000 two-label Utf8 identities, width 2048, depth 5:

algorithm capacity per update
CMS 100 65.9 us
CountSketch 100 76.9 us
CountSketch 0 518 ns

After this PR

algorithm capacity per update
CMS 100 32.6 us
CountSketch 100 43.2 us
CountSketch 0 509 ns

The remaining cost is rescoring: each update re-hashes every retained key (capacity x depth hashes) and re-sorts the candidates. Caching each candidate's row cells (not serialized) would remove the hashing at capacity x depth x 16 bytes of memory; that is left as a follow-up.

Verification

  • planner_v1_snapshots_remain_compatible (golden bytes) and every existing weighted-frequency test pass unchanged.
  • New retained_scores_track_every_update: on an 8 x 3 shape with 7 keys and capacity 3 (heavy collisions), after each of 60 updates every retained score equals the point estimate and no key is retained twice. It passes before and after this change; it guards the rescoring behavior the optimization must keep.
  • The table above comes from a temporary #[ignore] benchmark test run with cargo test --release, removed before commit.
  • cargo fmt --all -- --check; cargo clippy --workspace --all-targets --all-features --locked -- -D warnings.

… them

With a candidate heap, every update cloned each retained candidate
(its identity tuple and hash key) to rebuild the heap, and scoring each
candidate collected its row estimates into a fresh Vec. At capacity 100
with two-label Utf8 identities that was about 66 us (CMS) and 77 us
(CountSketch) per update in a release build.

The update now moves the candidates out of the heap, adds the new key
only when it is not already retained, and scores them with one reused
buffer (Count-Min takes the row minimum without any buffer). The same
benchmark runs at about 33 us and 43 us. Every retained score is still
re-read after each update, so the candidates and their scores are
unchanged; the golden snapshots still match.
@zzylol

zzylol commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Closing: main now provides f64 Count-Min and Count Sketch with heaps (#232), ASAPv1 serialization as the only format (#241) and TupleKey group-by keys (#237), so WeightedFrequency's own kernel and ASAP-WFREQ-1 format are redundant. The remaining gaps (shape and weight checks, an atomic checked f64 update, a full clear, opt-in rescoring of retained candidates, and estimated_memory_bytes) move to new PRs on main's heap sketches. The old head is kept on backup/perf/weighted-frequency-update-without-candidate-clones-20261020-161432.

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