tanmay442 opened a new pull request, #26536:
URL: https://github.com/apache/datafusion/pull/26536

   ## Which issue does this PR close?
   
   - N/A
   
   ## Rationale for this change
   
   This is a performance change only. The rows a query returns, the error
   messages, the log lines and the row ordering are all unchanged.
   
   `first_value(x ORDER BY y)` and `last_value(x ORDER BY y)` get slow as the
   number of groups in the query grows. The grouped accumulator keeps a 
scoreboard
   with one bit per group to record which group already has a winning row in the
   current batch (`datafusion/functions-aggregate/src/first_last.rs:414`). At 
the
   end of the batch it tested every one of those bits to find the groups that 
had
   a winner, so the work per batch grew with the number of groups in the query
   rather than with the number of groups the batch touched.
   
   The scan was over `total_num_groups` bits per batch. A group-by over a 
million
   groups spends most of its aggregate time in it, even though each input batch
   touches only a few thousand groups.
   
   ## What changes are included in this PR?
   
   1. `get_filtered_extreme_of_each_group`
      (`datafusion/functions-aggregate/src/first_last.rs:586`) collected the
      winners by walking the whole winner index array and testing the scoreboard
      bit of every group. It now calls `scoreboard_winners`
      (`datafusion/functions-aggregate/src/first_last.rs:668`), which reads the
      scoreboard a word at a time and takes the trailing zeros of each word. The
      scan goes from one bit test per group to one word read per 64 groups, plus
      one extraction per group that has a winner.
   
   2. `update_bench_many_groups`
      (`datafusion/functions-aggregate/benches/first_last.rs:270`) is a new
      criterion case that allocates a million groups and touches a few thousand 
of
      them per batch. The existing cases in this file touch every group in every
      batch, so they cannot show this change. The contributor guide suggests a
      separate pull request for a new benchmark, and I will split it if that is
      preferred.
   
   The numbers come from criterion with interleaved ABBA runs and 20 samples
   per side. The intervals are percentile bootstraps over the median difference.
   On the new `update_bench_many_groups` benchmark:
   
   | case | before | after | delta | 95% CI |
   |---|---|---|---|---|
   | `groups=1000000, touched=64`, first_value | 62.73 ms | 16.80 ms | -73.2% | 
[-73.6%, -73.0%] |
   | `groups=1000000, touched=64`, last_value | 63.65 ms | 17.63 ms | -72.3% | 
[-72.6%, -71.9%] |
   | `groups=1000000, touched=8192`, first_value | 91.52 ms | 45.69 ms | -50.1% 
| [-50.4%, -49.5%] |
   | `groups=1000000, touched=8192`, last_value | 91.75 ms | 45.68 ms | -50.2% 
| [-50.6%, -49.6%] |
   
   The benchmarks that touch every group in the batch are unchanged. Over the
   eight update and merge cases the median delta is -1.4%, the range is -3.4% to
   +0.6%. The eight `evaluate_bench` cases do not touch the changed code, run at
   14 to 17 us, and scatter between -5.6% and +2.7%. The same binary run against
   a copy of itself measures +0.02% median, so that scatter is measurement 
noise.
   
   The saving per batch is proportional to the number of allocated groups and 
does
   not depend on how many groups the batch touches. The bytes read are the same
   either way, so this is an instruction-count win and not a memory-traffic win.
   
   ### Why the behaviour is unchanged
   
   - The scoreboard reset, the row loop, the comparator and the state updates 
are
     untouched. Only the collection of the winners changed.
   - The old code walked the winner index array in order and kept the groups
     whose scoreboard bit was set. The new code walks the scoreboard in the same
     order and finds the same groups, so both return the winners in ascending
     group order.
   - The scoreboard is a `BooleanBufferBuilder`, so its bytes are reachable
     through `as_slice()`. The bits past the logical length of the scoreboard 
are
     zero, because the scoreboard is rebuilt with `append_n(len, false)` before
     each batch, so the scan cannot find a group past the end.
   - The result does not depend on the byte order. `u64::from_le_bytes` puts the
     first byte in the low bits on every platform, and arrow packs bits least
     significant first within a byte, so bit `i` of the scoreboard is bit `i % 
64`
     of word `i / 64` on every platform.
   - The callers update the state per group, so the order of the winners does 
not
     affect the result.
   
   No public API change. `scoreboard_winners` is a private method on a private
   struct, and the accumulator gains no field and no allocation.
   
   ## What is the testing strategy for this PR?
   
   `cargo test -p datafusion-functions-aggregate --lib` passes, 254 tests, and
   `cargo test --profile=ci --test sqllogictests` passes, 535 of 535 test files.
   
   `ci/scripts/rust_fmt.sh` and `ci/scripts/rust_clippy.sh` are clean.
   `cargo doc --document-private-items --no-deps -p 
datafusion-functions-aggregate`
   with `RUSTDOCFLAGS="-D warnings"` is clean. `typos` and the `hawkeye` license
   header check pass on the two changed files, and
   `ci/scripts/check_large_files.sh --base origin/main --head HEAD` passes for
   this commit range.
   
   One test is added, `scoreboard_winners_cover_every_word`
   (`datafusion/functions-aggregate/src/first_last.rs:3004`). It puts groups at
   index 0, 63, 64 and 99,999, so it covers the first word of the scoreboard, 
the
   second word and the last partial word, which holds fewer than 64 groups. It
   asserts the winning value of each group after two batches, and that a group
   that was never seen stays null. Removing the partial word handling makes the
   test fail, so it covers the new code.
   
   The full workspace suite takes about 40 minutes and I did not run it. I have 
no
   query level benchmark for this change yet, so the numbers above are at the
   accumulator level.
   
   ## Are there any user-facing changes?
   
   No. The winners are collected in the same ascending order as before, so every
   query returns the same rows. There is no public API change.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to