Skip to content

perf: reuse quantile summary buffers during merge#4932

Open
peterxcli wants to merge 1 commit into
apache:mainfrom
peterxcli:perf/reduce-approx-percentile-byte
Open

perf: reuse quantile summary buffers during merge#4932
peterxcli wants to merge 1 commit into
apache:mainfrom
peterxcli:perf/reduce-approx-percentile-byte

Conversation

@peterxcli

Copy link
Copy Markdown
Member

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::merge takes &self / &other and allocates a fresh summary, so merge_batch reallocates on every incoming digest.
  • with_head_buffer_inserted rebuilds the whole sampled vector on every flush.

What changes are included in this PR?

  • Reuse scratch buffers in QuantileSummaries during flush, compression, and merge.
  • Change summary merging to update in place while preserving the first-digest move fast path.
  • Update memory accounting and add a regression test for buffer reuse.

How are these changes tested?

benchmark

result:

  • merge_batch: (15GB -> 11GB)
    • upstream: image
    • pr: image
  • with_head_buffer_inserted: (10GB -> 1GB)
    • upstream: image
    • pr: image

@andygrove

Copy link
Copy Markdown
Member

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 merge against the old return-value version and the behavior looks preserved, including the non-commutative operand order and the memory accounting via heap_size. Two small suggestions, neither a blocker.

1. Defensive clear() for consistency

compress_immut defensively clears its output buffer before filling it, but with_head_buffer_inserted and merge take sampled_buffer and only reserve before pushing:

let mut new_samples = std::mem::take(&mut self.sampled_buffer);
new_samples.reserve(self.sampled.len() + sorted.len());

This is correct today because sampled_buffer is always empty on entry. It does leave a bit of a trap though. If that invariant ever slips in a future change, this path would silently append the stale elements (it pushes after reserve) and corrupt the result rather than fail loudly. A new_samples.clear() right after the take is essentially free and keeps this consistent with compress_immut. Same thought for merged_sampled in merge.

2. A multi-merge test

The new flush_and_merge_reuse_sampled_buffers test locks in the pointer swap, which is great. merge_is_within_bound only does a single merge though, and merge_batch folds many digests through the reused buffers in sequence. Would you consider adding a test that merges three or more summaries in place and checks the query result against the exact percentile? That would exercise the buffer reuse across repeated merges, which is exactly where a reuse bug would surface.

Review assisted by an LLM.

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.

Reduce allocations in approx_percentile QuantileSummaries merge and flush paths

2 participants