Skip to content

perf(functions-aggregate): optimize sliding window MIN/MAX using monotonic deques (#23826) - #23827

Merged
neilconway merged 21 commits into
apache:mainfrom
pavan51:sliding-window-min-max-monotonic-deque
Jul 27, 2026
Merged

neilconway merged 21 commits into
apache:mainfrom
pavan51:sliding-window-min-max-monotonic-deque

Conversation

@pavan51

@pavan51 pavan51 commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

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).

@github-actions github-actions Bot added the functions Changes to functions implementation label Jul 23, 2026
@pavan51 pavan51 closed this Jul 23, 2026
@pavan51 pavan51 reopened this Jul 23, 2026
@codecov-commenter

codecov-commenter commented Jul 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.65%. Comparing base (88365dd) to head (edf19f0).
⚠️ Report is 7 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@neilconway neilconway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@pavan51
pavan51 force-pushed the sliding-window-min-max-monotonic-deque branch from 82168be to 31cf348 Compare July 24, 2026 07:08
@pavan51

pavan51 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

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?

Fixed. I've resolved all the clippy::unnecessary_map_or warnings.

@pavan51

pavan51 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@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 (Int64)

  • Window Size 1000
    • Throughput (New): ~50 Million elements / sec
    • Throughput (Old): ~28 Million elements / sec
    • Delta: Old code is ~48% slower (New code is nearly 2x faster)
    • Time: Old code takes +91% longer to process the same batch.

2. Timestamps (TimestampNanosecond)

  • Window Size 1000
    • Throughput (New): ~52 Million elements / sec
    • Throughput (Old): ~26 Million elements / sec
    • Delta: Old code is ~50% slower (New code is exactly 2x faster)
    • Time: Old code takes +100% longer to process the same batch.

3. Decimals (Decimal128)

  • Window Size 1000
    • Throughput (New): ~50 Million elements / sec
    • Throughput (Old): ~25 Million elements / sec
    • Delta: Old code is ~50% slower (New code is exactly 2x faster)
    • Time: Old code takes +100% longer to process the same batch.

4. Strings (Utf8)

  • Window Size 1000
    • Throughput (New): ~16 Million elements / sec
    • Throughput (Old): ~4.6 Million elements / sec
    • Delta: Old code is ~71% slower (New code is 3.5x faster)
    • Time: Old code takes +245% longer to process the same batch.

Thanks again for the review! Please let me know if anything else is needed.

@pavan51
pavan51 requested a review from neilconway July 24, 2026 08:30

@neilconway neilconway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need Clone anymore.

Comment on lines +806 to +808
while self.deque.back().is_some_and(|back_val| back_val.1 > val) {
self.deque.pop_back();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think >= would be correct and a bit more efficient for duplicate-heavy inputs?

if i >= w {
q.pop();
}
let _res = q.max();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_seq
  • if let Some((front_seq, _)) = self.deque.front() { front_seq >= pop_seq }

@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

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
     Cloning apache/main
    Building datafusion-functions-aggregate v54.1.0 (current)
       Built [  36.944s] (current)
     Parsing datafusion-functions-aggregate v54.1.0 (current)
      Parsed [   0.046s] (current)
    Building datafusion-functions-aggregate v54.1.0 (baseline)
       Built [  30.876s] (baseline)
     Parsing datafusion-functions-aggregate v54.1.0 (baseline)
      Parsed [   0.046s] (baseline)
    Checking datafusion-functions-aggregate v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.227s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure struct_missing: pub struct removed or renamed ---

Description:
A publicly-visible struct cannot be imported by its prior path. A `pub use` may have been removed, or the struct itself may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/struct_missing.ron

Failed in:
  struct datafusion_functions_aggregate::min_max::MovingMin, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/96930e9da4aa5f9bd3751786ae50f8102ff1a527/datafusion/functions-aggregate/src/min_max.rs:770
  struct datafusion_functions_aggregate::min_max::MovingMax, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/96930e9da4aa5f9bd3751786ae50f8102ff1a527/datafusion/functions-aggregate/src/min_max.rs:914

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  69.605s] datafusion-functions-aggregate

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jul 24, 2026

@neilconway neilconway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment on the behavior for empty windows?

debug_assert!(prev.1 < curr.1);
prev = curr;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think an assertion that is this expensive is warranted, even for debug-only builds -- I'd just omit it.

Pavan added 17 commits July 25, 2026 23:44
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).
@pavan51
pavan51 force-pushed the sliding-window-min-max-monotonic-deque branch from f266712 to 0cee76d Compare July 26, 2026 04:51
@pavan51

pavan51 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

@neilconway All comments have been addressed, could you please take a look.

@pavan51
pavan51 requested a review from neilconway July 26, 2026 05:55

@getChan getChan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense to me.
please remove the temporary markdown files.

@pavan51

pavan51 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

make sense to me.
please remove the temporary markdown files.

@getChan Thank you for

make sense to me. please remove the temporary markdown files.

@getChan Thanks for the review! Done, I have removed the temporary markdown files from the PR.

@pavan51
pavan51 requested a review from getChan July 26, 2026 07:51

@neilconway neilconway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this comment.

/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Strictly increasing" is a bit misleading, since we only require PartialOrd. "in ascending order" instead?

@neilconway neilconway added the api change Changes the API exposed to users of the crate label Jul 26, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 26, 2026
@pavan51

pavan51 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

@neilconway Thanks for the review. Requested updates have been addressed and pushed, including the entry in the v55 upgrade guide.

@pavan51
pavan51 requested a review from neilconway July 26, 2026 16:27
@neilconway
neilconway added this pull request to the merge queue Jul 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jul 27, 2026
@neilconway
neilconway added this pull request to the merge queue Jul 27, 2026
Merged via the queue into apache:main with commit 8d2ffc9 Jul 27, 2026
39 checks passed
kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate auto detected api change Auto detected API change documentation Improvements or additions to documentation functions Changes to functions implementation v55.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimize sliding window MIN/MAX aggregations using monotonic deques

5 participants