Repository navigation
perf: reuse cached dictionary value hashes in vectorized_append - #25185
Conversation
f0bcd4f to
e33ef31
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #25185 +/- ##
========================================
Coverage 81.92% 81.92%
========================================
Files 1132 1132
Lines 421192 421360 +168
Branches 421192 421360 +168
========================================
+ Hits 345041 345190 +149
- Misses 55756 55769 +13
- Partials 20395 20401 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
run benchmark dictionary_group_values |
1 similar comment
|
run benchmark dictionary_group_values |
|
🤖 Benchmark running (GKE) | trigger CPU Details (lscpu)Comparing perf/dict-group-values-cache-hashes (e33ef31) to 0da2151 (merge-base) diff Run configurationrun benchmark dictionary_group_valuesResults will be posted here when complete File an issue against this benchmark runner |
|
🤖 Benchmark running (GKE) | trigger CPU Details (lscpu)Comparing perf/dict-group-values-cache-hashes (e33ef31) to 0da2151 (merge-base) diff Run configurationrun benchmark dictionary_group_valuesResults will be posted here when complete File an issue against this benchmark runner |
|
🤖 Benchmark completed (GKE) | trigger Instance: Comparing perf/dict-group-values-cache-hashes (e33ef31) to 0da2151 (merge-base) diff Run configurationrun benchmark dictionary_group_valuesCPU Details (lscpu)Details
Resource Usagedictionary_group_values — base (merge-base)
dictionary_group_values — branch
File an issue against this benchmark runner |
|
🤖 Benchmark completed (GKE) | trigger Instance: Comparing perf/dict-group-values-cache-hashes (e33ef31) to 0da2151 (merge-base) diff Run configurationrun benchmark dictionary_group_valuesCPU Details (lscpu)Details
Resource Usagedictionary_group_values — base (merge-base)
dictionary_group_values — branch
File an issue against this benchmark runner |
|
there is no perf difference entirely due to how the benchmarks are set up. in the benchmarks the values array are created fresh for each if maintainers are interested in seeing what the performance gains this would mean for parquet files we may need to wait for #24227 to be over the finish line so we can run @adriangbot with the flag enabled. |
|
dictionary value arrays are commonly re-used in prod workloads, this inneficiency is something we noticed in our service, we saw the following scenario: with becuase there is no group ordering the partial aggregate goes through improvements are visible with this benchmark |
jayzhan211
left a comment
There was a problem hiding this comment.
Thanks @LiaCastaneda! I think this PR is good to merge. I also noticed another potential improvement, so I've filed a follow-up issue #25219
|
Thanks @jayzhan211, if we could also merge #25198 to showcase the results, it would be awsome |
|
I will merge those 2 PRs |
|
🤔 which check failed? |
…#25198) ## Which issue does this PR close? [Related to apache#24822. No issue closed.](apache#25185) ## Rationale for this change The benchmarks in this file build a fresh values array per batch and cap cardinality at the batch size. Neither shape matches a real plan: `take` and `filter` clone the values `Arc` and rewrite only the keys, so batches downstream of a repartition share one values array, and because it is never compacted its cardinality can far exceed the rows in a batch. Per-batch work that scales with dictionary cardinality is invisible under the current parameters. ## What changes are included in this PR? Adds `bench_shared_values_arc`: 32 batches of 8192 rows sharing one values array, at cardinalities 8192, 100k and 500k. ## What is the testing strategy for this PR? Benchmark-only. Ran the bench locally and `cargo clippy --all-targets -D warnings`. ## Are there any user-facing changes? No
|
Sometimes it needs a retry — not sure what's going on. |
…he#25185) ## Which issue does this PR close? - Closes #. ## Rationale for this change `vectorized_append` is the path taken when we have non streaming hash aggregations (when there is no ordering in the `group by`), right now it re hashed the entire dictionary values on every batch unlike append_val which that only does this if the values array was already seen before. ## What changes are included in this PR? `vectorized_append` now reuses the cached value hashes when a batch carries the same dictionary values array as the previous one (this is the common case of a repartition or filter), instead of re hashing the whole dictionary and re-initialising the map on every batch. This extends the existing `Arc::ptr_eq` cache from apache#24418, which only covered the scalar `append_val` path. ## What is the testing strategy for this PR? All dict tests pass, I have a PR for a benchmark that exercises this path -> apache#25198 ## Are there any user-facing changes? no, this is a pure perf PR (cherry picked from commit a0631ed)
…he#25185) (#185) ## Which issue does this PR close? - Closes #. ## Rationale for this change `vectorized_append` is the path taken when we have non streaming hash aggregations (when there is no ordering in the `group by`), right now it re hashed the entire dictionary values on every batch unlike append_val which that only does this if the values array was already seen before. ## What changes are included in this PR? `vectorized_append` now reuses the cached value hashes when a batch carries the same dictionary values array as the previous one (this is the common case of a repartition or filter), instead of re hashing the whole dictionary and re-initialising the map on every batch. This extends the existing `Arc::ptr_eq` cache from apache#24418, which only covered the scalar `append_val` path. ## What is the testing strategy for this PR? All dict tests pass, I have a PR for a benchmark that exercises this path -> apache#25198 ## Are there any user-facing changes? no, this is a pure perf PR (cherry picked from commit a0631ed)
…che#25652) ## Which issue does this PR close? - Closes apache#25219. ## Rationale for this change Follow-up to apache#25185. `vectorized_equal_to` was left untouched and still rebuilt the same `val_idx -> inner_slot` map from scratch on every batch. ## What changes are included in this PR? - Added `lookup_inner_slot`, which resolves a single `val_idx` to its slot in `inner` and memorizes the result in `val_to_inner`. - Changed `vectorized_equal_to` to call `sync_value_cache` and resolve slots lazily per row. - Removed `build_lookup_table`, the `rhs_rows.len() < num_distinct` guard and `equal_to_per_row`. ## What is the testing strategy for this PR? Covered by the existing `dictionary.rs` unit tests. ## Are there any user-facing changes? No. This is an internal performance change with no public API or behaviour change.
…#25198) ## Which issue does this PR close? [Related to apache#24822. No issue closed.](apache#25185) ## Rationale for this change The benchmarks in this file build a fresh values array per batch and cap cardinality at the batch size. Neither shape matches a real plan: `take` and `filter` clone the values `Arc` and rewrite only the keys, so batches downstream of a repartition share one values array, and because it is never compacted its cardinality can far exceed the rows in a batch. Per-batch work that scales with dictionary cardinality is invisible under the current parameters. ## What changes are included in this PR? Adds `bench_shared_values_arc`: 32 batches of 8192 rows sharing one values array, at cardinalities 8192, 100k and 500k. ## What is the testing strategy for this PR? Benchmark-only. Ran the bench locally and `cargo clippy --all-targets -D warnings`. ## Are there any user-facing changes? No
…he#25185) ## Which issue does this PR close? - Closes #. ## Rationale for this change `vectorized_append` is the path taken when we have non streaming hash aggregations (when there is no ordering in the `group by`), right now it re hashed the entire dictionary values on every batch unlike append_val which that only does this if the values array was already seen before. ## What changes are included in this PR? `vectorized_append` now reuses the cached value hashes when a batch carries the same dictionary values array as the previous one (this is the common case of a repartition or filter), instead of re hashing the whole dictionary and re-initialising the map on every batch. This extends the existing `Arc::ptr_eq` cache from apache#24418, which only covered the scalar `append_val` path. ## What is the testing strategy for this PR? All dict tests pass, I have a PR for a benchmark that exercises this path -> apache#25198 ## Are there any user-facing changes? no, this is a pure perf PR
…che#25652) ## Which issue does this PR close? - Closes apache#25219. ## Rationale for this change Follow-up to apache#25185. `vectorized_equal_to` was left untouched and still rebuilt the same `val_idx -> inner_slot` map from scratch on every batch. ## What changes are included in this PR? - Added `lookup_inner_slot`, which resolves a single `val_idx` to its slot in `inner` and memorizes the result in `val_to_inner`. - Changed `vectorized_equal_to` to call `sync_value_cache` and resolve slots lazily per row. - Removed `build_lookup_table`, the `rhs_rows.len() < num_distinct` guard and `equal_to_per_row`. ## What is the testing strategy for this PR? Covered by the existing `dictionary.rs` unit tests. ## Are there any user-facing changes? No. This is an internal performance change with no public API or behaviour change.
## Which issue does this PR close? - Part of apache#24117. ## Rationale for this change The existing dictionary benchmark only measures pre-encoded dictionaries with cardinality expressed as a fraction of row count. That is not enough to derive a safe automatic encoding policy: native dictionaries, per-batch dictionaries, `Utf8View`, plain strings, and runtime encoding have materially different costs, and the crossover also depends on absolute NDV, value width, and batch layout. I rebased this PR onto current `main` after apache#25185 merged and reran the relevant matrix, so the evidence includes the new cached dictionary-value-hash path. The crossover remains clear; the cache optimization does not make dictionary grouping universally profitable. Current-main results (`700ad9cf2`, release/LTO, Apple M5 Max, Rust 1.98.1; 10M rows, 8,192-row input batches, one target partition; median of iterations 2-6 after two warmups; output normalized back to `Utf8`): | value bytes | NDV | `Utf8View` | dict/shared | vs view | dict/per-batch | vs view | |---:|---:|---:|---:|---:|---:|---:| | 16 | 1,000 | 50.29 ms | 43.48 ms | -13.5% | 43.52 ms | -13.5% | | 16 | 10,000 | 54.24 ms | 91.61 ms | +68.9% | 94.03 ms | +73.3% | | 64 | 1,000 | 72.65 ms | 47.02 ms | -35.3% | 47.69 ms | -34.4% | | 64 | 10,000 | 79.69 ms | 115.85 ms | +45.4% | 115.04 ms | +44.4% | These are local measurements, not a proposed universal threshold. They show why apache#24117 needs a cost decision based on absolute NDV and value width rather than a blanket dictionary rewrite. Runtime encoding also lost in every earlier measured case, which is why the planned optimizer work targets native Parquet dictionary reads rather than inserting a runtime cast. ## What changes are included in this PR? - Compare pre-encoded dictionaries, `Utf8View`, plain strings, and runtime dictionary casts. - Configure absolute NDV, value width, and input batch size. - Model shared dictionary values and independent per-batch values. - Optionally normalize dictionary group keys back to `Utf8` in the output so output-type preservation is included in the measurement. - Preserve the existing default benchmark behavior and case names. - Reject invalid benchmark configurations instead of silently changing the requested workload. - Verify that all encodings produce identical grouped counts. This PR only establishes the benchmark surface needed to evaluate apache#24117; it does not add an optimizer rule or select a universal threshold. ## What is the testing strategy for this PR? On rebased head `5fb605471`: - `cargo test -p datafusion-benchmarks --lib` (153 passed) - `cargo test -p datafusion-benchmarks --lib --no-default-features` (153 passed) - `cargo clippy -p datafusion-benchmarks --all-targets --no-deps -- -D warnings` - `cargo clippy -p datafusion-benchmarks --all-targets --all-features --no-deps -- -D warnings` - `cargo fmt --all -- --check` - Current-main CLI benchmark matrix above; every case returned exactly the requested NDV ## Are there any user-facing changes? No DataFusion runtime or public API behavior changes. This only adds benchmark CLI controls. --------- Co-authored-by: kosiew <kosiew@gmail.com>
Which issue does this PR close?
Rationale for this change
vectorized_appendis the path taken when we have non streaming hash aggregations (when there is no ordering in thegroup by), right now it re hashed the entire dictionary values on every batch unlike append_val which that only does this if the values array was already seen before.What changes are included in this PR?
vectorized_appendnow reuses the cached value hashes when a batch carries the same dictionary values array as the previous one (this is the common case of a repartition or filter), instead of re hashing the whole dictionary and re-initialising the map on every batch. This extends the existingArc::ptr_eqcache from #24418, which only covered the scalarappend_valpath.What is the testing strategy for this PR?
All dict tests pass, I have a PR for a benchmark that exercises this path -> #25198
Are there any user-facing changes?
no, this is a pure perf PR