perf: use Vec in ArrowBytesMap - #24071
Conversation
Replace BufferBuilder<u8> with Vec<u8> for accumulating values in ArrowBytesMap, converting the Vec into an Arrow Buffer without copying when materializing the output array. Add Criterion benchmarks covering short unique values, long unique values, and long values with low cardinality.
There was a problem hiding this comment.
@Punisheroot thank you for the PR! generally its better to make a PR for the benchmarks first so that a maintainer can run the benchmarks to compare it to main.
Could you split this PR in two?
|
Thanks, that makes sense. I opened #24078 with the benchmark only. Since this PR already contains the identical benchmark files, once #24078 lands those files will drop out of this PR's diff, leaving only the Vec optimization. This also avoids rewriting the existing reviewed branch history. Please let me know if you would prefer me to remove the benchmark files from this branch immediately instead. |
neilconway
left a comment
There was a problem hiding this comment.
Nice! This is a simple and nicely-targeted fix.
It might be nice to elaborate slightly in the PR description about why it is that extending an over-aligned buffer is more expensive than extending a Vec.
Running the benchmarks locally (macOS + M4 Max), I see a much smaller performance improvement (~2.5% on long_unique), but I'd imagine that is due to differences in allocator behavior.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #24071 +/- ##
==========================================
- Coverage 80.90% 80.89% -0.01%
==========================================
Files 1102 1102
Lines 376295 376295
Branches 376295 376295
==========================================
- Hits 304426 304409 -17
- Misses 53755 53767 +12
- Partials 18114 18119 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| /// In progress arrow `Buffer` containing all values | ||
| buffer: BufferBuilder<u8>, | ||
| /// In progress buffer containing all values | ||
| buffer: Vec<u8>, |
|
This looks great -- thanks @neilconway @Punisheroot and @Rich-T-kid |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
|
run benchmark arrow_bytes_map |
|
🤖 Benchmark running (GKE) | trigger CPU Details (lscpu)Comparing perf/js/use-vec-in-arrow-bytes-map (26405fd) to 9eb31bf (merge-base) diff Run configurationrun benchmark arrow_bytes_mapResults will be posted here when complete File an issue against this benchmark runner |
|
🤖 Benchmark completed (GKE) | trigger Instance: Comparing perf/js/use-vec-in-arrow-bytes-map (26405fd) to 9eb31bf (merge-base) diff Run configurationrun benchmark arrow_bytes_mapCPU Details (lscpu)Details
Resource Usagearrow_bytes_map — base (merge-base)
arrow_bytes_map — branch
File an issue against this benchmark runner |
|
FWIW I think this PR is good to merge anyways as it simplifies the code, even if we don't see any significant performance change |
|
@alamb The optimization was the friends we made along the way. 😄 |
Which issue does this PR close?
BufferBuilder<u8>withVec<u8>#13867.Rationale for this change
ArrowBytesMaponly requires growable byte storage while values are beinginserted.
BufferBuilder<u8>wrapsMutableBuffer, whose allocations use64-byte alignment. This alignment is unnecessary for byte storage and makes
buffer growth more expensive.
A
Vec<u8>provides the required append, lookup, length, and capacityoperations. When the map is materialized,
Buffer::from_vectransfers theallocation into an Arrow
Bufferwithout copying it.What changes are included in this PR?
BufferBuilder<u8>inArrowBytesMapwithVec<u8>.extend_from_slicewhen storing new values.Vec<u8>into an ArrowBufferwithout copying.This PR intentionally changes only
ArrowBytesMap. The other structuresmentioned in #13867 are left for separate follow-up PRs.
Are these changes tested?
Yes.
Validation performed on Ubuntu 24.04 under WSL2 with Rust 1.97.0:
cargo fmt --all --checkcargo clippy --all-targets --all-features -- -D warningscargo test -p datafusion-physical-expr-common --all-featurescargo test -p datafusion-physical-plan group_valuesBenchmark results
Criterion comparison against the
BufferBuilder<u8>implementation atcommit
f9dde71ec, using 100 samples, a 3-second warm-up, and a 5-secondmeasurement period:
short_uniquelong_uniquelong_low_cardinalityFor
long_unique, throughput increased by approximately 158%. Repeating thecomparison with the execution order reversed produced approximately 158.57 us
for
Vecand 440.05 us forBufferBuilder.No stable performance regression was observed in the short-value or
low-cardinality cases.
Are there any user-facing changes?
No. This is an internal implementation and performance change with no public
API or behavior changes.