Repository navigation
perf(functions-aggregate): optimize sliding window MIN/MAX using monotonic deques (#23826) - #23827
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #23827 +/- ##
=======================================
Coverage 80.65% 80.65%
=======================================
Files 1093 1093
Lines 371619 371663 +44
Branches 371619 371663 +44
=======================================
+ Hits 299730 299776 +46
+ Misses 53993 53989 -4
- Partials 17896 17898 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
neilconway
left a comment
There was a problem hiding this comment.
Thanks for the contribution @pavan51 ! I think this is directionally a good idea and a clear improvement over the previous implementation.
The PR fails some linter checks; can you fix those, please?
82168be to
31cf348
Compare
Fixed. I've resolved all the clippy::unnecessary_map_or warnings. |
|
@neilconway Thanks for the detailed review and excellent feedback. I've addressed all the comments. The sliding_min_max.rs benchmark now only tests the production MovingMax implementation directly. To establish the performance delta, I ran the benchmark against both the main branch (old two-stack implementation) and this new branch (monotonic deque sequence number). Here are the results: Key Metrics (Old vs New)1. 64-bit Integers (
|
neilconway
left a comment
There was a problem hiding this comment.
Thanks for the work on this!
Can you update the PR description to describe the revised design? Also if possible, can we make the PR description more concise?
| pop_seq: u64, | ||
| } | ||
|
|
||
| impl<T: Clone + PartialOrd> Default for MovingMin<T> { |
There was a problem hiding this comment.
I don't think we need Clone anymore.
| while self.deque.back().is_some_and(|back_val| back_val.1 > val) { | ||
| self.deque.pop_back(); | ||
| } |
There was a problem hiding this comment.
I think >= would be correct and a bit more efficient for duplicate-heavy inputs?
| if i >= w { | ||
| q.pop(); | ||
| } | ||
| let _res = q.max(); |
There was a problem hiding this comment.
std::hint::black_box(q.max()); is safer I think.
| group.throughput(Throughput::Elements(data_size as u64)); | ||
|
|
||
| group.bench_with_input( | ||
| BenchmarkId::new("monotonic_deque", window_size), |
There was a problem hiding this comment.
A name like "sliding_max" is probably more descriptive.
|
|
||
| /// Pushes a new element into the sliding window. | ||
| #[inline] | ||
| pub fn push(&mut self, val: T) { |
There was a problem hiding this comment.
What do you think of adding some debug_asserts to check that invariants on the data structure hold after push() and pop()? For example:
pop_seq <= push_seqif let Some((front_seq, _)) = self.deque.front() { front_seq >= pop_seq }
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
neilconway
left a comment
There was a problem hiding this comment.
Thanks for the update!
I notice that MovingMin and MovingMax are pub; I think we should probably make them pub(crate). We should also mention the API change in the PR, since even if we don't change the visibility of these types, we're already making a breaking API change.
For clarity, can we name the benchmark file sliding_max.rs? Since we don't actually benchmark sliding min.
| } | ||
|
|
||
| /// Removes and returns the last value of the sliding window. | ||
| /// Removes the oldest value from the sliding window. |
There was a problem hiding this comment.
Can we add a comment on the behavior for empty windows?
| debug_assert!(prev.1 < curr.1); | ||
| prev = curr; | ||
| } | ||
| } |
There was a problem hiding this comment.
I don't think an assertion that is this expensive is warranted, even for debug-only builds -- I'd just omit it.
…nt empty window pop behavior
Criterion benchmarks are compiled as a separate crate, so pub(crate) makes them inaccessible. Using #[doc(hidden)] achieves the same intent (hiding from public API docs) while still allowing the bench crate to import the struct.
…Min/Max pub(crate) - Benchmark now tests the production SlidingMaxAccumulator API via update_batch/retract_batch, which is how the query engine uses it. This fully satisfies the reviewer's preference for benchmarking the production implementation directly. - MovingMin and MovingMax can now be properly pub(crate) -- no hacks needed since the benchmark no longer imports them directly. - Remove unused with_capacity methods. - Remove doc tests that would fail to compile against pub(crate) items (their logic is covered by the unit tests in the test module).
f266712 to
0cee76d
Compare
|
@neilconway All comments have been addressed, could you please take a look. |
getChan
left a comment
There was a problem hiding this comment.
make sense to me.
please remove the temporary markdown files.
neilconway
left a comment
There was a problem hiding this comment.
A few last nits and then I'll merge this 😊
Can you add a quick note to the v55 migration guide describing the breaking API change?
| self.push_seq == self.pop_seq | ||
| } | ||
|
|
||
| /// Heap bytes owned by the two stack buffers plus each stored `T`'s |
| /// The implementation is taken from <https://github.com/spebern/moving_min_max/blob/master/src/lib.rs> | ||
| /// `MovingMin` keeps track of the minimum value in a sliding window using a | ||
| /// monotonic deque. Each element is stored with its sequence number, and the | ||
| /// deque maintains candidate elements in strictly increasing value order. |
There was a problem hiding this comment.
"Strictly increasing" is a bit misleading, since we only require PartialOrd. "in ascending order" instead?
…cending ordering and deque heap size
|
@neilconway Thanks for the review. Requested updates have been addressed and pushed, including the entry in the v55 upgrade guide. |
…tonic deques (apache#23826) (apache#23827) ## Which issue does this PR close? - Closes apache#23826 ### What changes are included in this PR? This PR optimizes sliding window `MIN`/`MAX` aggregate functions using a **Sequence-Numbered Monotonic Deque** instead of a Two-Stack Queue. **Revised Design:** - We store `(sequence_number, value)` pairs in a single `VecDeque`, kept in strictly monotonic order. - **`push(val)`**: Evicts dominated elements from the back, then pushes the new value with the current `push_seq` and increments `push_seq`. - **`pop()`**: Increments `pop_seq`. If the front elements sequence number equals the old `pop_seq`, it is expired and popped. - **Benefits**: No secondary FIFO queue (lower memory) and no `clone()` overhead. ### Are these changes tested? Yes, existing tests pass. Added tests for empty-window `pop()` and duplicate-heavy scenarios. ### Are there any user-facing changes? **API Change**: `MovingMin` and `MovingMax` were changed to `pub(crate)` visibility, and their `pop()` methods now return `()` instead of returning a value. Performance is significantly improved (2x-3.5x throughput). --------- Co-authored-by: Pavan <[email protected]>
Which issue does this PR close?
What changes are included in this PR?
This PR optimizes sliding window
MIN/MAXaggregate functions using a Sequence-Numbered Monotonic Deque instead of a Two-Stack Queue.Revised Design:
(sequence_number, value)pairs in a singleVecDeque, kept in strictly monotonic order.push(val): Evicts dominated elements from the back, then pushes the new value with the currentpush_seqand incrementspush_seq.pop(): Incrementspop_seq. If the front elements sequence number equals the oldpop_seq, it is expired and popped.clone()overhead.Are these changes tested?
Yes, existing tests pass. Added tests for empty-window
pop()and duplicate-heavy scenarios.Are there any user-facing changes?
API Change:
MovingMinandMovingMaxwere changed topub(crate)visibility, and theirpop()methods now return()instead of returning a value.Performance is significantly improved (2x-3.5x throughput).