Skip to content

perf: use Vec in ArrowBytesMap - #24071

Merged
alamb merged 3 commits into
apache:mainfrom
Punisheroot:perf/js/use-vec-in-arrow-bytes-map
Aug 4, 2026
Merged

perf: use Vec in ArrowBytesMap#24071
alamb merged 3 commits into
apache:mainfrom
Punisheroot:perf/js/use-vec-in-arrow-bytes-map

Conversation

@Punisheroot

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

ArrowBytesMap only requires growable byte storage while values are being
inserted. BufferBuilder<u8> wraps MutableBuffer, whose allocations use
64-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 capacity
operations. When the map is materialized, Buffer::from_vec transfers the
allocation into an Arrow Buffer without copying it.

What changes are included in this PR?

  • Replace the internal BufferBuilder<u8> in ArrowBytesMap with Vec<u8>.
  • Use extend_from_slice when storing new values.
  • Convert the completed Vec<u8> into an Arrow Buffer without copying.
  • Add a focused Criterion benchmark covering:
    • unique 4-byte values;
    • unique 32-byte values;
    • 32-byte values with low cardinality.

This PR intentionally changes only ArrowBytesMap. The other structures
mentioned 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 --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test -p datafusion-physical-expr-common --all-features
    • 80 unit tests passed
    • 8 doctests passed
  • cargo test -p datafusion-physical-plan group_values
    • 65 tests passed

Benchmark results

Criterion comparison against the BufferBuilder<u8> implementation at
commit f9dde71ec, using 100 samples, a 3-second warm-up, and a 5-second
measurement period:

Benchmark BufferBuilder Vec Criterion result
short_unique 306.39 us 302.80 us Within noise threshold
long_unique 410.23 us 159.08 us 61.24% lower time
long_low_cardinality 49.90 us 48.97 us No change detected

For long_unique, throughput increased by approximately 158%. Repeating the
comparison with the execution order reversed produced approximately 158.57 us
for Vec and 440.05 us for BufferBuilder.

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.

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.
@github-actions github-actions Bot added the physical-expr Changes to the physical-expr crates label Aug 3, 2026
@Punisheroot
Punisheroot marked this pull request as ready for review August 3, 2026 23:03

@Rich-T-kid Rich-T-kid 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.

@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?

@Punisheroot

Copy link
Copy Markdown
Contributor Author

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.

@Punisheroot
Punisheroot requested a review from Rich-T-kid August 4, 2026 08:54

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

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

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.89%. Comparing base (9eb31bf) to head (26405fd).
⚠️ Report is 2 commits behind head on main.

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

/// In progress arrow `Buffer` containing all values
buffer: BufferBuilder<u8>,
/// In progress buffer containing all values
buffer: Vec<u8>,

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.

💯

@alamb
alamb dismissed Rich-T-kid’s stale review August 4, 2026 20:22

Comments addressed. Please re-review

@alamb

alamb commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

This looks great -- thanks @neilconway @Punisheroot and @Rich-T-kid

@alamb

This comment was marked as outdated.

@adriangbot

This comment was marked as outdated.

@alamb

alamb commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

run benchmark arrow_bytes_map

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5184206935-1422-btqnl 6.12.85+ #1 SMP Wed Jun 17 20:31:55 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing perf/js/use-vec-in-arrow-bytes-map (26405fd) to 9eb31bf (merge-base) diff

Run configuration
run benchmark arrow_bytes_map

Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing perf/js/use-vec-in-arrow-bytes-map (26405fd) to 9eb31bf (merge-base) diff

Run configuration
run benchmark arrow_bytes_map
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                   HEAD                                    perf_js_use-vec-in-arrow-bytes-map
-----                                   ----                                    ----------------------------------
arrow_bytes_map/long_low_cardinality    1.00     74.8±0.56µs 104.5 MElem/sec    1.00     74.7±0.13µs 104.5 MElem/sec
arrow_bytes_map/long_unique             1.00    215.6±1.32µs 36.2 MElem/sec     1.00    216.0±0.32µs 36.2 MElem/sec
arrow_bytes_map/short_unique            1.00    195.8±0.64µs 39.9 MElem/sec     1.00    196.5±0.42µs 39.8 MElem/sec

Resource Usage

arrow_bytes_map — base (merge-base)

Metric Value
Wall time 90.0s
Peak memory 28.0 MiB
Avg memory 6.4 MiB
CPU user 33.8s
CPU sys 0.0s
Peak spill 0 B

arrow_bytes_map — branch

Metric Value
Wall time 85.0s
Peak memory 26.9 MiB
Avg memory 6.3 MiB
CPU user 33.6s
CPU sys 0.0s
Peak spill 0 B

File an issue against this benchmark runner

@alamb

alamb commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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
alamb enabled auto-merge August 4, 2026 20:39
@Punisheroot

Copy link
Copy Markdown
Contributor Author

@alamb The optimization was the friends we made along the way. 😄

@alamb
alamb added this pull request to the merge queue Aug 4, 2026
Merged via the queue into apache:main with commit d813358 Aug 4, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-expr Changes to the physical-expr crates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace BufferBuilder<u8> with Vec<u8>

6 participants