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

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new d6b0bd7ecc Expose `&Arc[Buffer]` in 
`GenericByteViewArray::data_buffers` for cheaper cloning (#10708)
d6b0bd7ecc is described below

commit d6b0bd7ecc857bce0c3b0f97ccae17d5fc3726c4
Author: Yiming Qiao <[email protected]>
AuthorDate: Mon Aug 24 13:05:32 2026 +0800

    Expose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for cheaper 
cloning (#10708)
    
    # Which issue does this PR close?
    
    - Related to #10692.
    
    # Rationale for this change
    
    This change follows directly from the existing ByteView design history:
    
    - #6408 traced slow ByteView slicing to allocating and cloning the
    backing-buffer list. The discussion specifically identified `take` as
    another operation that would benefit from sharing this list.
    - #6427 distinguished two related but separate concerns: very large
    buffer lists may indicate missing GC or deduplication, but avoiding
    allocation when an operation retains the complete list is independently
    worthwhile.
    - #6808 demonstrated why eager deduplication in a general-purpose path
    is a different tradeoff: it added substantial overhead to common
    operations, and review favored treating ByteView compaction more
    holistically.
    - #9016 subsequently changed `GenericByteViewArray` to store the
    collection as `Arc<[Buffer]>`, making it possible to share the complete
    list without allocation.
    
    The downstream impact is concrete.
    [apache/datafusion#16206](https://github.com/apache/datafusion/issues/16206)
    describes hash joins where concatenating build-side batches produces
    ByteView payload columns with many backing buffers, and constructing
    join output repeatedly calls `take`. In that pattern, cloning and later
    dropping every buffer handle can become a significant part of execution
    time. Arrow issue #10692 provides a compact multi-stage `take` and
    `BatchCoalescer` reproduction of the same ownership-metadata
    amplification.
    
    `take_byte_view` and `filter_byte_view` do not yet use this shared
    representation. They rebuild the collection with
    `data_buffers().to_vec()`, allocating a new collection and cloning every
    `Buffer`. This makes the ownership bookkeeping for selection O(number of
    backing buffers), despite retaining exactly the same complete buffer
    list.
    
    Selection already copies the chosen views while leaving their payloads
    zero-copy. The cost of retaining the unchanged backing-buffer collection
    should therefore not grow with the number of entries in that collection.
    This PR completes that narrow part of the earlier design while leaving
    buffer canonicalization as a separate problem.
    
    # What changes are included in this PR?
    
    - Change `GenericByteViewArray::data_buffers()` to return
    `&Arc<[Buffer]>`, allowing callers to inspect the buffers as before or
    clone the collection's `Arc` in O(1).
    - Change `GenericByteViewArray::new_unchecked` to accept `Arc<[Buffer]>`
    directly, making shared versus newly constructed buffer-list ownership
    explicit at each call site.
    - Use `Arc::clone` in the ByteView `take` and `filter` kernels instead
    of rebuilding the collection.
    - Replace three additional `data_buffers().to_vec()` call sites found by
    the stricter constructor signature with shared `Arc` clones.
    - Verify for both StringView and BinaryView that selection results share
    the input buffer collection.
    
    This PR only removes repeated ownership-metadata cloning. It retains the
    same complete collection of backing buffers as before. It does not prune
    or deduplicate buffer entries, remap views, run GC, or copy
    string/binary payloads. The broader buffer-fragmentation problem
    described in #10692 remains separate.
    
    # Are these changes tested?
    
    ```shell
    cargo fmt --all -- --check
    cargo check --workspace --all-targets
    cargo test -p arrow-array -p arrow-select -p arrow-row -p arrow-ipc
    cargo test -p parquet --test arrow_reader invalid_utf8
    cargo clippy -p arrow-array -p arrow-select -p arrow-row -p arrow-ipc -p 
parquet --all-targets --all-features -- -D warnings
    cargo doc -p arrow-array --no-deps
    ```
    
    A temporary local Criterion benchmark was used to validate the
    asymptotic behavior. It takes 8,192 views distributed across a varying
    number of buffer entries. The entries share one immutable payload
    allocation, isolating collection-ownership cost from payload size.
    `main` and this PR were built in separate Cargo target directories on an
    Intel Xeon Platinum 8474C:
    
    | Buffer entries | `main` | This PR | Speedup |
    |---:|---:|---:|---:|
    | 1 | 5.4900 us | 5.4619 us | 1.01x |
    | 16 | 5.7342 us | 5.4577 us | 1.05x |
    | 256 | 9.2806 us | 5.4518 us | 1.70x |
    | 4,096 | 65.774 us | 5.4584 us | 12.05x |
    
    Existing normal-size benchmarks did not regress:
    
    | Benchmark | `main` | This PR |
    |---|---:|---:|
    | `take stringview 512` | 504.73 ns | 439.40 ns |
    | `take stringview 1024` | 872.36 ns | 773.48 ns |
    | `filter context mixed string view (kept 1/2)` | 73.406 us | 72.083 us
    |
    
    # Are there any user-facing changes?
    
    There are two public signature changes. `data_buffers()` now returns
    `&Arc<[Buffer]>` instead of `&[Buffer]`; slice methods remain available
    through deref, while direct `for` iteration can use `.iter()`.
    `new_unchecked` now requires `Arc<[Buffer]>`; callers constructing a
    `Vec<Buffer>` can convert it with `.into()`. ByteView selection results
    now share the immutable backing-buffer collection instead of allocating
    an equivalent collection. Logical values, null handling, buffer indexes,
    payload lifetimes, and GC behavior are unchanged.
    
    # AI assistance
    
    I used OpenAI Codex to help inspect the relevant implementation history,
    draft the patch and tests, and prepare the benchmark harness and PR
    text. I reviewed the implementation and benchmark methodology and ran
    the checks above locally.
---
 arrow-array/src/array/byte_view_array.rs           | 27 +++++++++++-----------
 .../src/builder/generic_bytes_view_builder.rs      |  4 ++--
 arrow-array/src/ffi.rs                             |  2 +-
 arrow-ipc/src/reader.rs                            |  2 +-
 arrow-row/src/variable.rs                          |  2 +-
 arrow-select/src/coalesce/byte_view.rs             |  5 ++--
 arrow-select/src/filter.rs                         |  5 +++-
 arrow-select/src/interleave.rs                     |  2 +-
 arrow-select/src/take.rs                           |  8 ++++---
 parquet/src/arrow/buffer/view_buffer.rs            |  5 ++--
 parquet/tests/arrow_reader/invalid_utf8.rs         |  2 +-
 11 files changed, 36 insertions(+), 28 deletions(-)

diff --git a/arrow-array/src/array/byte_view_array.rs 
b/arrow-array/src/array/byte_view_array.rs
index af390789a2..7a7e94d111 100644
--- a/arrow-array/src/array/byte_view_array.rs
+++ b/arrow-array/src/array/byte_view_array.rs
@@ -238,14 +238,11 @@ impl<T: ByteViewType + ?Sized> GenericByteViewArray<T> {
     /// # Safety
     ///
     /// Safe if [`Self::try_new`] would not error
-    pub unsafe fn new_unchecked<U>(
+    pub unsafe fn new_unchecked(
         views: ScalarBuffer<u128>,
-        buffers: U,
+        buffers: Arc<[Buffer]>,
         nulls: Option<NullBuffer>,
-    ) -> Self
-    where
-        U: Into<Arc<[Buffer]>>,
-    {
+    ) -> Self {
         if cfg!(feature = "force_validate") {
             return Self::new(views, buffers, nulls);
         }
@@ -254,7 +251,7 @@ impl<T: ByteViewType + ?Sized> GenericByteViewArray<T> {
             data_type: T::DATA_TYPE,
             phantom: Default::default(),
             views,
-            buffers: buffers.into(),
+            buffers,
             nulls,
         }
     }
@@ -300,9 +297,13 @@ impl<T: ByteViewType + ?Sized> GenericByteViewArray<T> {
         &self.views
     }
 
-    /// Returns the buffers storing string data
+    /// Returns the shared collection of buffers storing non-inline string or 
binary data.
+    ///
+    /// The returned `Arc` can be cloned to share the buffers with another 
array without
+    /// allocating a new collection or cloning the individual buffers. To 
consume this
+    /// array and take ownership of its buffers, use [`Self::into_parts`].
     #[inline]
-    pub fn data_buffers(&self) -> &[Buffer] {
+    pub fn data_buffers(&self) -> &Arc<[Buffer]> {
         &self.buffers
     }
 
@@ -543,7 +544,7 @@ impl<T: ByteViewType + ?Sized> GenericByteViewArray<T> {
             return unsafe {
                 GenericByteViewArray::new_unchecked(
                     self.views().clone(),
-                    vec![], // empty data blocks
+                    Arc::from([]), // empty data blocks
                     nulls,
                 )
             };
@@ -558,7 +559,7 @@ impl<T: ByteViewType + ?Sized> GenericByteViewArray<T> {
             return unsafe {
                 GenericByteViewArray::new_unchecked(
                     self.views().clone(),
-                    vec![], // empty data blocks
+                    Arc::from([]), // empty data blocks
                     nulls,
                 )
             };
@@ -657,7 +658,7 @@ impl<T: ByteViewType + ?Sized> GenericByteViewArray<T> {
         let views_scalar = ScalarBuffer::from(views_buf);
 
         // SAFETY: views_scalar, data_blocks, and nulls are correctly aligned 
and sized
-        unsafe { GenericByteViewArray::new_unchecked(views_scalar, 
data_blocks, nulls) }
+        unsafe { GenericByteViewArray::new_unchecked(views_scalar, 
data_blocks.into(), nulls) }
     }
 
     /// Copy the i‑th view into `data_buf` if it refers to an out‑of‑line 
buffer.
@@ -1635,7 +1636,7 @@ mod tests {
             gced.data_buffers().len()
         );
         // No output buffer may exceed the cap.
-        for buf in gced.data_buffers() {
+        for buf in gced.data_buffers().iter() {
             assert!(buf.len() <= max_buffer_size, "buffer exceeded max size");
         }
         // Every value (inline, large, and null) is unchanged and in order.
diff --git a/arrow-array/src/builder/generic_bytes_view_builder.rs 
b/arrow-array/src/builder/generic_bytes_view_builder.rs
index 12fe7e735b..b68c5d2352 100644
--- a/arrow-array/src/builder/generic_bytes_view_builder.rs
+++ b/arrow-array/src/builder/generic_bytes_view_builder.rs
@@ -500,7 +500,7 @@ impl<T: ByteViewType + ?Sized> GenericByteViewBuilder<T> {
         }
         let views = std::mem::take(&mut self.views_buffer);
         // SAFETY: valid by construction
-        unsafe { GenericByteViewArray::new_unchecked(views.into(), completed, 
nulls) }
+        unsafe { GenericByteViewArray::new_unchecked(views.into(), 
completed.into(), nulls) }
     }
 
     /// Builds the [`GenericByteViewArray`] without resetting the builder
@@ -514,7 +514,7 @@ impl<T: ByteViewType + ?Sized> GenericByteViewBuilder<T> {
         let views = ScalarBuffer::new(views, 0, len);
         let nulls = self.null_buffer_builder.finish_cloned();
         // SAFETY: valid by construction
-        unsafe { GenericByteViewArray::new_unchecked(views, completed, nulls) }
+        unsafe { GenericByteViewArray::new_unchecked(views, completed.into(), 
nulls) }
     }
 
     /// Returns the current null buffer as a slice
diff --git a/arrow-array/src/ffi.rs b/arrow-array/src/ffi.rs
index 42c175f33b..d09c077c65 100644
--- a/arrow-array/src/ffi.rs
+++ b/arrow-array/src/ffi.rs
@@ -1815,7 +1815,7 @@ mod tests_from_ffi {
     #[cfg(not(feature = "force_validate"))]
     fn test_utf8_view_ffi_from_dangling_pointer() {
         let empty = GenericByteViewBuilder::<StringViewType>::new().finish();
-        let buffers = empty.data_buffers().to_vec();
+        let buffers = Arc::clone(empty.data_buffers());
         let nulls = empty.nulls().cloned();
 
         // Create a dangling pointer to a view buffer with zero length.
diff --git a/arrow-ipc/src/reader.rs b/arrow-ipc/src/reader.rs
index c42cc5d31c..7d757b534f 100644
--- a/arrow-ipc/src/reader.rs
+++ b/arrow-ipc/src/reader.rs
@@ -3604,7 +3604,7 @@ mod tests {
         let array = unsafe {
             StringViewArray::new_unchecked(
                 binary_view_array.views().clone(),
-                binary_view_array.data_buffers().to_vec(),
+                Arc::clone(binary_view_array.data_buffers()),
                 binary_view_array.nulls().cloned(),
             )
         };
diff --git a/arrow-row/src/variable.rs b/arrow-row/src/variable.rs
index f196e0e220..2dfc1807b6 100644
--- a/arrow-row/src/variable.rs
+++ b/arrow-row/src/variable.rs
@@ -375,7 +375,7 @@ fn decode_binary_view_inner<const VALIDATE_UTF8: bool>(
 
     // SAFETY:
     // Valid by construction above
-    unsafe { BinaryViewArray::new_unchecked(views.into(), [values.into()], 
nulls) }
+    unsafe { BinaryViewArray::new_unchecked(views.into(), 
[values.into()].into(), nulls) }
 }
 
 /// Decodes a binary view array from `rows` with the provided `options`
diff --git a/arrow-select/src/coalesce/byte_view.rs 
b/arrow-select/src/coalesce/byte_view.rs
index feddf29fa7..a6697bfdb5 100644
--- a/arrow-select/src/coalesce/byte_view.rs
+++ b/arrow-select/src/coalesce/byte_view.rs
@@ -501,8 +501,9 @@ impl<B: ByteViewType> InProgressArray for 
InProgressByteViewArray<B> {
 
         // Safety: we created valid views and buffers above and the
         // input arrays had value data and nulls
-        let new_array =
-            unsafe { GenericByteViewArray::<B>::new_unchecked(views.into(), 
buffers, nulls) };
+        let new_array = unsafe {
+            GenericByteViewArray::<B>::new_unchecked(views.into(), 
buffers.into(), nulls)
+        };
         Ok(Arc::new(new_array))
     }
 
diff --git a/arrow-select/src/filter.rs b/arrow-select/src/filter.rs
index be2fb18918..80be38d401 100644
--- a/arrow-select/src/filter.rs
+++ b/arrow-select/src/filter.rs
@@ -934,7 +934,7 @@ fn filter_byte_view<T: ByteViewType>(
 ) -> GenericByteViewArray<T> {
     let new_view_buffer = filter_native(array.views(), predicate);
     let views = ScalarBuffer::new(new_view_buffer, 0, predicate.count);
-    let buffers = array.data_buffers().to_vec();
+    let buffers = Arc::clone(array.data_buffers());
     let nulls = predicate.filter_nulls(array.nulls());
 
     // SAFETY: each view is copied unchanged from `array.views()` and `buffers`
@@ -1297,6 +1297,9 @@ mod tests {
             let actual = filter(&array, &predicate).unwrap();
 
             assert_eq!(actual.len(), 3);
+            let actual_buffers = actual.as_byte_view::<T>().data_buffers();
+            let input_buffers = array.data_buffers();
+            assert!(Arc::ptr_eq(actual_buffers, input_buffers));
 
             let expected = {
                 // ["hello", null, "large payload over 12 bytes"]
diff --git a/arrow-select/src/interleave.rs b/arrow-select/src/interleave.rs
index 6d5033b1d4..494fdb2931 100644
--- a/arrow-select/src/interleave.rs
+++ b/arrow-select/src/interleave.rs
@@ -340,7 +340,7 @@ fn interleave_views<T: ByteViewType>(
         .collect();
 
     let array = unsafe {
-        GenericByteViewArray::<T>::new_unchecked(views.into(), buffers, 
interleaved.nulls)
+        GenericByteViewArray::<T>::new_unchecked(views.into(), buffers.into(), 
interleaved.nulls)
     };
     Ok(Arc::new(array))
 }
diff --git a/arrow-select/src/take.rs b/arrow-select/src/take.rs
index 36cdb81e76..b66dbf5463 100644
--- a/arrow-select/src/take.rs
+++ b/arrow-select/src/take.rs
@@ -633,10 +633,9 @@ fn take_byte_view<T: ByteViewType, IndexType: 
ArrowPrimitiveType>(
 ) -> Result<GenericByteViewArray<T>, ArrowError> {
     let new_views = take_native(array.views(), indices);
     let new_nulls = take_nulls(array.nulls(), indices);
+    let buffers = Arc::clone(array.data_buffers());
     // Safety:  array.views was valid, and take_native copies only valid 
values, and verifies bounds
-    Ok(unsafe {
-        GenericByteViewArray::new_unchecked(new_views, 
array.data_buffers().to_vec(), new_nulls)
-    })
+    Ok(unsafe { GenericByteViewArray::new_unchecked(new_views, buffers, 
new_nulls) })
 }
 
 /// `take` implementation for list arrays
@@ -1798,6 +1797,9 @@ mod tests {
         let actual = take(&array, &index, None).unwrap();
 
         assert_eq!(actual.len(), index.len());
+        let actual_buffers = actual.as_byte_view::<T>().data_buffers();
+        let input_buffers = array.data_buffers();
+        assert!(Arc::ptr_eq(actual_buffers, input_buffers));
 
         let expected = {
             // ["large payload over 12 bytes", null, "world", "large payload 
over 12 bytes", "lulu", null]
diff --git a/parquet/src/arrow/buffer/view_buffer.rs 
b/parquet/src/arrow/buffer/view_buffer.rs
index 9670d0e9bb..61458eec0b 100644
--- a/parquet/src/arrow/buffer/view_buffer.rs
+++ b/parquet/src/arrow/buffer/view_buffer.rs
@@ -57,14 +57,15 @@ impl ViewBuffer {
         let len = self.views.len();
         let views = ScalarBuffer::from(self.views);
         let nulls = null_buffer.and_then(|b| 
NullBuffer::from_unsliced_buffer(b, len));
+        let buffers = self.buffers.into();
         match data_type {
             ArrowType::Utf8View => {
                 // Safety: views were created correctly, and checked that the 
data is utf8 when building the buffer
-                unsafe { Arc::new(StringViewArray::new_unchecked(views, 
self.buffers, nulls)) }
+                unsafe { Arc::new(StringViewArray::new_unchecked(views, 
buffers, nulls)) }
             }
             ArrowType::BinaryView => {
                 // Safety: views were created correctly
-                unsafe { Arc::new(BinaryViewArray::new_unchecked(views, 
self.buffers, nulls)) }
+                unsafe { Arc::new(BinaryViewArray::new_unchecked(views, 
buffers, nulls)) }
             }
             _ => panic!("Unsupported data type: {data_type}"),
         }
diff --git a/parquet/tests/arrow_reader/invalid_utf8.rs 
b/parquet/tests/arrow_reader/invalid_utf8.rs
index 1124737a7f..ff1adac374 100644
--- a/parquet/tests/arrow_reader/invalid_utf8.rs
+++ b/parquet/tests/arrow_reader/invalid_utf8.rs
@@ -125,7 +125,7 @@ fn test_invalid_utf8_string_view_array() {
             let array = unsafe {
                 StringViewArray::new_unchecked(
                     array.views().clone(),
-                    array.data_buffers().to_vec(),
+                    Arc::clone(array.data_buffers()),
                     array.nulls().cloned(),
                 )
             };

Reply via email to