perf: reuse quantile summary buffers during merge - #4932
Conversation
|
Thanks for this, the buffer reuse is a clean win and the before/after allocation flamegraphs make the impact easy to see. I traced the in-place 1. Defensive
let mut new_samples = std::mem::take(&mut self.sampled_buffer);
new_samples.reserve(self.sampled.len() + sorted.len());This is correct today because 2. A multi-merge test The new Review assisted by an LLM. |
|
@andygrove Thanks for the review, addressed all of your suggestion! please take another look. |
|
Thanks for the revisions. Both points from last round are handled well. The defensive I wanted to be confident the in-place merge did not perturb results, so I built a differential harness that fingerprints One thing I would like to settle before merging. Since
If the high-cardinality case does regress, one option is to drop the Review assisted by an LLM. |
|
Hi @andygrove, thanks for pointing this out. After investigating, I plan to change the implementation to use a native For retained buffer memory after compression/merge, let:
The approximate memory shape is:
Each group now retains only its compressed summary. The raw workspace is reused across all groups, and the head buffer is released after flushing. For the high-cardinality benchmark—1,048,576 rows, 100,000 groups, and roughly 10 values per group—the buffer payload is approximately 56 MiB for the previous PR design versus 24 MiB for the shared-scratch design, around 2.3× lower. This is a capacity model rather than measured process RSS; the raw scratch component itself goes from roughly 100,000 buffers to one. In my current local change, the seeded high-cardinality benchmark also improved from 558 ms on Assisted by an LLM. |
2c884f4 to
c9653e0
Compare
Which issue does this PR close?
Closes #4874.
Rationale for this change
The native QuantileSummaries port behind approx_percentile / percentile_approx allocates more than necessary on its hot paths:
QuantileSummaries::mergetakes&self/&otherand allocates a fresh summary, somerge_batchreallocates on every incoming digest.with_head_buffer_insertedrebuilds the wholesampledvector on every flush.What changes are included in this PR?
How are these changes tested?
benchmark
result:
merge_batch: (15GB -> 11GB)with_head_buffer_inserted: (10GB -> 1GB)