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 007894ef0d Reuse buffers when interleaving byte views (#11281)
007894ef0d is described below

commit 007894ef0db0eb3b4dc3226d7b490b1104352231
Author: Peter L <[email protected]>
AuthorDate: Thu Oct 1 12:40:39 2026 +0930

    Reuse buffers when interleaving byte views (#11281)
    
    # Which issue does this PR close?
    
    Fixes https://github.com/apache/arrow-rs/issues/11280
    
    And also helps with: https://github.com/apache/datafusion/pull/23565
    
    # Rationale for this change
    
    See the original issue, but, basically the interleave kernel can make
    sub-optimal string views especially when buffers are shared.
    
    # What changes are included in this PR?
    
    We use a btreemap on the seen buffers (not individual values) and use
    that to prevent us from cloning the same buffer multiple times.
    
    # Are these changes tested?
    
    Yes tested, and locally benchmarks have shown a moderate speed increase
    in some of the interleave benchmarks, on my dev machine which has an
    **AMD Ryzen 9 9950X3D** processor:
    
    | benchmark | main | this PR | change |
    
    
|-----------------------------------------------------------------------|----------|----------|--------|
    | interleave str_view(0.0) 100 [0..100, 100..230, 450..1000] | 284.5 ns
    | 268.6 ns | -5.6% |
    | interleave str_view(0.0) 400 [0..100, 100..230, 450..1000] | 706.9 ns
    | 563.9 ns | -20.2% |
    | interleave str_view(0.0) 1024 [0..100, 100..230, 450..1000] | 1.717 µs
    | 1.250 µs | -27.2% |
    | interleave str_view(0.0) 1024 [0..100, 100..230, 450..1000, 0..1000] |
    1.783 µs | 1.274 µs | -28.6% |
    
    
    # Are there any user-facing changes?
    
    No user facing changes, just an optimization.
---
 arrow-select/src/interleave.rs | 84 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 76 insertions(+), 8 deletions(-)

diff --git a/arrow-select/src/interleave.rs b/arrow-select/src/interleave.rs
index 54f4412c5b..74e29defe8 100644
--- a/arrow-select/src/interleave.rs
+++ b/arrow-select/src/interleave.rs
@@ -26,10 +26,13 @@ use arrow_array::types::*;
 use arrow_array::*;
 use arrow_buffer::bit_mask::set_bits;
 use arrow_buffer::bit_util;
-use arrow_buffer::{ArrowNativeType, BooleanBuffer, MutableBuffer, NullBuffer, 
OffsetBuffer};
+use arrow_buffer::{
+    ArrowNativeType, BooleanBuffer, Buffer, MutableBuffer, NullBuffer, 
OffsetBuffer,
+};
 use arrow_data::ByteView;
 use arrow_data::transform::MutableArrayData;
 use arrow_schema::{ArrowError, DataType, FieldRef, Fields};
+use std::collections::BTreeMap;
 use std::sync::Arc;
 
 macro_rules! primitive_helper {
@@ -314,8 +317,17 @@ fn interleave_views<T: ByteViewType>(
         offsets.push(total_buffers);
     }
 
+    // Marks a buffer in `buffer_to_new_index` that has not yet been 
referenced.
+    //
+    // The view's buffer index is a signed 32-bit integer in the Arrow 
specification,
+    // so `u32::MAX` can never be a valid buffer index
+    const UNASSIGNED: u32 = u32::MAX;
+
     // contains the mapping from old buffer index to new buffer index
-    let mut buffer_to_new_index = vec![None; total_buffers];
+    let mut buffer_to_new_index = vec![UNASSIGNED; total_buffers];
+
+    // Contains the index in `buffers` of each buffer already emitted, keyed 
by identity
+    let mut seen_buffers = BTreeMap::new();
 
     let views: Vec<u128> = indices
         .iter()
@@ -329,12 +341,15 @@ fn interleave_views<T: ByteViewType>(
             // value is big enough to be in a variadic buffer
             let view = ByteView::from(*view);
             let buffer_to_new_idx = offsets[*array_idx] + view.buffer_index as 
usize;
-            let new_buffer_idx: u32 =
-                *buffer_to_new_index[buffer_to_new_idx].get_or_insert_with(|| {
-                    buffers.push(array.data_buffers()[view.buffer_index as 
usize].clone());
-                    (buffers.len() - 1) as u32
-                });
-            view.with_buffer_index(new_buffer_idx).as_u128()
+            let new_buffer_idx = &mut buffer_to_new_index[buffer_to_new_idx];
+            if *new_buffer_idx == UNASSIGNED {
+                *new_buffer_idx = push_buffer(
+                    &mut seen_buffers,
+                    &mut buffers,
+                    &array.data_buffers()[view.buffer_index as usize],
+                );
+            }
+            view.with_buffer_index(*new_buffer_idx).as_u128()
         })
         .collect();
 
@@ -344,6 +359,24 @@ fn interleave_views<T: ByteViewType>(
     Ok(Arc::new(array))
 }
 
+/// Returns the index of `buffer` in `buffers`, adding it if not already 
present.
+///
+/// Multiple input arrays, e.g. slices of the same array, may reference the 
same
+/// buffer, so this checks by identity to ensure it is only emitted once
+#[inline(never)]
+fn push_buffer(
+    seen: &mut BTreeMap<(*const u8, usize), u32>,
+    buffers: &mut Vec<Buffer>,
+    buffer: &Buffer,
+) -> u32 {
+    *seen
+        .entry((buffer.as_ptr(), buffer.len()))
+        .or_insert_with(|| {
+            buffers.push(buffer.clone());
+            (buffers.len() - 1) as u32
+        })
+}
+
 fn interleave_struct(
     fields: &Fields,
     values: &[&dyn Array],
@@ -1674,6 +1707,41 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_interleave_views_shared_buffers() {
+        let long = |i: usize| format!("long_string_not_inlined_{i}");
+        let base = StringViewArray::from_iter_values((0..10).map(long));
+        assert_eq!(base.data_buffers().len(), 1);
+
+        // Slices share the same buffers
+        let a = base.slice(0, 5);
+        let b = base.slice(5, 5);
+        // A distinct buffer list containing the same underlying buffer
+        let c = StringViewArray::try_new(base.views().clone(), 
base.data_buffers().to_vec(), None)
+            .unwrap();
+
+        let indices = &[(0, 1), (1, 2), (2, 3), (0, 4), (1, 0), (2, 9)];
+        let values = interleave(&[&a, &b, &c], indices).unwrap();
+        let result = values.as_string_view();
+        assert_eq!(result.data_buffers().len(), 1);
+
+        let expected: Vec<_> = [1, 7, 3, 4, 5, 
9].into_iter().map(long).collect();
+        let actual: Vec<_> = result.iter().map(|x| 
x.unwrap().to_string()).collect();
+        assert_eq!(actual, expected);
+
+        // Many slices, exceeding the linear scan threshold
+        let slices: Vec<_> = (0..40).map(|i| base.slice(i % 10, 1)).collect();
+        let arrays: Vec<&dyn Array> = slices.iter().map(|x| x as &dyn 
Array).collect();
+        let indices: Vec<_> = (0..40).rev().map(|i| (i, 0)).collect();
+        let values = interleave(&arrays, &indices).unwrap();
+        let result = values.as_string_view();
+        assert_eq!(result.data_buffers().len(), 1);
+
+        let expected: Vec<_> = (0..40).rev().map(|i| long(i % 10)).collect();
+        let actual: Vec<_> = result.iter().map(|x| 
x.unwrap().to_string()).collect();
+        assert_eq!(actual, expected);
+    }
+
     #[test]
     fn test_interleave_views_multiple_buffers() {
         let str1 = "very_long_string_from_first_buffer".as_bytes();

Reply via email to