This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new d813358848 perf: use Vec in ArrowBytesMap (#24071)
d813358848 is described below

commit d813358848a555474a7af25c79ead10ba1c68cac
Author: JS <[email protected]>
AuthorDate: Tue Aug 4 22:52:30 2026 +0200

    perf: use Vec in ArrowBytesMap (#24071)
    
    ## Which issue does this PR close?
    
    - Closes #13867.
    
    ## 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.
    
    Co-authored-by: Andrew Lamb <[email protected]>
---
 datafusion/physical-expr-common/src/binary_map.rs | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/datafusion/physical-expr-common/src/binary_map.rs 
b/datafusion/physical-expr-common/src/binary_map.rs
index ad184d6500..44ca35c7f8 100644
--- a/datafusion/physical-expr-common/src/binary_map.rs
+++ b/datafusion/physical-expr-common/src/binary_map.rs
@@ -19,12 +19,12 @@
 //! StringArray / LargeStringArray / BinaryArray / LargeBinaryArray.
 
 use arrow::array::{
-    Array, ArrayRef, BufferBuilder, GenericBinaryArray, GenericStringArray,
-    NullBufferBuilder, OffsetSizeTrait,
+    Array, ArrayRef, GenericBinaryArray, GenericStringArray, NullBufferBuilder,
+    OffsetSizeTrait,
     cast::AsArray,
     types::{ByteArrayType, GenericBinaryType, GenericStringType},
 };
-use arrow::buffer::{NullBuffer, OffsetBuffer, ScalarBuffer};
+use arrow::buffer::{Buffer, NullBuffer, OffsetBuffer, ScalarBuffer};
 use arrow::datatypes::DataType;
 use datafusion_common::hash_utils::RandomState;
 use datafusion_common::hash_utils::create_hashes;
@@ -218,8 +218,8 @@ where
     map: hashbrown::hash_table::HashTable<Entry<O, V>>,
     /// Total size of the map in bytes
     map_size: usize,
-    /// In progress arrow `Buffer` containing all values
-    buffer: BufferBuilder<u8>,
+    /// In progress buffer containing all values
+    buffer: Vec<u8>,
     /// Offsets into `buffer` for each distinct  value. These offsets as used
     /// directly to create the final `GenericBinaryArray`. The `i`th string is
     /// stored in the range `offsets[i]..offsets[i+1]` in `buffer`. Null values
@@ -248,7 +248,7 @@ where
             output_type,
             map: 
hashbrown::hash_table::HashTable::with_capacity(INITIAL_MAP_CAPACITY),
             map_size: 0,
-            buffer: BufferBuilder::new(INITIAL_BUFFER_CAPACITY),
+            buffer: Vec::with_capacity(INITIAL_BUFFER_CAPACITY),
             offsets: vec![O::default()], // first offset is always 0
             random_state: RandomState::default(),
             hashes_buffer: vec![],
@@ -405,7 +405,7 @@ where
                     // Put the small values into buffer and offsets so it 
appears
                     // the output array, but store the actual bytes inline for
                     // comparison
-                    self.buffer.append_slice(value);
+                    self.buffer.extend_from_slice(value);
                     self.offsets.push(O::usize_as(self.buffer.len()));
                     let payload = make_payload_fn(Some(value));
                     let new_header = Entry {
@@ -433,7 +433,7 @@ where
                     // Need to compare the bytes in the buffer
                     // SAFETY: buffer is only appended to, and we correctly 
inserted values and offsets
                     let existing_value =
-                        unsafe { 
self.buffer.as_slice().get_unchecked(header.range()) };
+                        unsafe { self.buffer.get_unchecked(header.range()) };
                     value == existing_value
                 });
 
@@ -446,7 +446,7 @@ where
                     // appears the output array, and store that offset
                     // so the bytes can be compared if needed
                     let offset = self.buffer.len(); // offset of start for data
-                    self.buffer.append_slice(value);
+                    self.buffer.extend_from_slice(value);
                     self.offsets.push(O::usize_as(self.buffer.len()));
 
                     let payload = make_payload_fn(Some(value));
@@ -488,7 +488,7 @@ where
             map: _,
             map_size: _,
             offsets,
-            mut buffer,
+            buffer,
             random_state: _,
             hashes_buffer: _,
             null,
@@ -502,7 +502,7 @@ where
         // SAFETY: the offsets were constructed correctly in `insert_if_new` --
         // monotonically increasing, overflows were checked.
         let offsets = unsafe { 
OffsetBuffer::new_unchecked(ScalarBuffer::from(offsets)) };
-        let values = buffer.finish();
+        let values = Buffer::from_vec(buffer);
 
         match output_type {
             OutputType::Binary => {


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

Reply via email to