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

alamb 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 314c52999e [arrow-select] perf: Replace `ArrayData` with direct 
`Array` construction in concat (#10377)
314c52999e is described below

commit 314c52999e8308cabd914e26e7f46340ec145660
Author: Liam Bao <[email protected]>
AuthorDate: Mon Sep 28 07:18:49 2026 -0700

    [arrow-select] perf: Replace `ArrayData` with direct `Array` construction 
in concat (#10377)
    
    # Which issue does this PR close?
    
    - Part of #9298.
    
    # Rationale for this change
    
    `concat_run_arrays` built its result via `RunArray::try_new`, which
    re-validates the run ends on every call even though they're already
    known-valid here: inputs are valid `RunArray`s, so the adjusted run ends
    are strictly increasing by construction.
    
    # What changes are included in this PR?
    
    - `concat.rs`: `concat_run_arrays` now builds the result directly via
    `RunArray::new_unchecked(data_type, RunEndBuffer::new_unchecked(...),
    values)`, skipping `try_new`'s revalidation.
    - `concat.rs`: adds an explicit overflow guard and rejects up front if
    the total doesn't fit in the run-end type `R`, before any `R::Native`
    arithmetic runs.
    - `nullif.rs`/`take.rs` changes are test-only cleanup.
    
    # Are these changes tested?
    
    Yes — existing test suite, plus a new regression test for the overflow
    guard.
    
    # Benchmark results
    
    ```
    cargo bench -p arrow --bench concatenate_kernel --features test_utils -- 
"concat run"
    ```
    this branch vs `main` (`RunArray<Int32Type>`, `n` arrays concatenated):
    
    | Case | main | this PR | Change |
    |---|---|---|---|
    | logical=32, physical=4, n=4 | 773.6 ns | 563.4 ns | **−27%** |
    | logical=1024, physical=128, n=8 | 3.33 µs | 2.68 µs | **−19%** |
    | logical=8192, physical=1024, n=10 | 25.2 µs | 20.4 µs | **−19%** |
    
    # Are there any user-facing changes?
    
    No.
---
 arrow-schema/src/error.rs           |   4 +-
 arrow-select/src/concat.rs          |  47 ++++++---
 arrow-select/src/nullif.rs          |  22 +---
 arrow-select/src/take.rs            | 194 ++++++++++++------------------------
 arrow/benches/concatenate_kernel.rs |  15 +++
 5 files changed, 117 insertions(+), 165 deletions(-)

diff --git a/arrow-schema/src/error.rs b/arrow-schema/src/error.rs
index b9818b1b92..35d480d549 100644
--- a/arrow-schema/src/error.rs
+++ b/arrow-schema/src/error.rs
@@ -60,7 +60,7 @@ pub enum ArrowError {
     CDataInterface(String),
     /// Error when a dictionary key is bigger than the key type
     DictionaryKeyOverflowError,
-    /// Error when the run end index in a REE array is bigger than the array 
length
+    /// Error when a run-end index is bigger than the run-end type
     RunEndIndexOverflowError,
     /// Error when the offset overflows.
     OffsetOverflowError(usize),
@@ -129,7 +129,7 @@ impl Display for ArrowError {
                 write!(f, "Dictionary key bigger than the key type")
             }
             ArrowError::RunEndIndexOverflowError => {
-                write!(f, "Run end encoded array index overflow error")
+                write!(f, "Run end index bigger than the run-end type")
             }
             ArrowError::OffsetOverflowError(offset) => {
                 write!(f, "Offset overflow error: {offset}")
diff --git a/arrow-select/src/concat.rs b/arrow-select/src/concat.rs
index 72dd239bbb..51b8082853 100644
--- a/arrow-select/src/concat.rs
+++ b/arrow-select/src/concat.rs
@@ -38,9 +38,9 @@ use arrow_array::cast::AsArray;
 use arrow_array::types::*;
 use arrow_array::*;
 use arrow_buffer::{
-    ArrowNativeType, BooleanBufferBuilder, MutableBuffer, NullBuffer, 
OffsetBuffer, ScalarBuffer,
+    ArrowNativeType, BooleanBufferBuilder, MutableBuffer, NullBuffer, 
OffsetBuffer, RunEndBuffer,
+    ScalarBuffer,
 };
-use arrow_data::ArrayDataBuilder;
 use arrow_data::transform::{Capacities, MutableArrayData};
 use arrow_schema::{ArrowError, DataType, FieldRef, Fields, SchemaRef};
 use std::{collections::HashSet, ops::Add, sync::Arc};
@@ -430,6 +430,12 @@ where
         return Ok(new_empty_array(arrays[0].data_type()));
     }
 
+    // Reject lengths that do not fit in `R` before any `R::Native` arithmetic 
can wrap.
+    let total_len: usize = run_arrays.iter().map(|r| r.len()).sum();
+    if R::Native::from_usize(total_len).is_none() {
+        return Err(ArrowError::RunEndIndexOverflowError);
+    }
+
     // The run ends need to be adjusted by the sum of the lengths of the 
previous arrays.
     let needed_run_end_adjustments = std::iter::once(R::default_value())
         .chain(
@@ -442,9 +448,6 @@ where
         )
         .collect::<Vec<_>>();
 
-    // This works out nicely to be the total (logical) length of the resulting 
array.
-    let total_len = needed_run_end_adjustments.last().unwrap().as_usize();
-
     let run_ends_array =
         
PrimitiveArray::<R>::from_iter_values(run_arrays.iter().enumerate().flat_map(
             move |(i, run_array)| {
@@ -463,15 +466,15 @@ where
 
     let all_values = concat(&values_slices.iter().map(|x| 
x.as_ref()).collect::<Vec<_>>())?;
 
-    let builder = ArrayDataBuilder::new(run_arrays[0].data_type().clone())
-        .len(total_len)
-        .child_data(vec![run_ends_array.into_data(), all_values.into_data()]);
+    let data_type = run_arrays[0].data_type().clone();
+    let (_, run_ends_values, _) = run_ends_array.into_parts();
 
-    // `build_unchecked` is used to avoid recursive validation of child arrays.
-    let array_data = unsafe { builder.build_unchecked() };
-    array_data.validate_data()?;
-
-    Ok(Arc::<RunArray<R>>::new(array_data.into()))
+    // Safety: inputs are valid RunArrays; adjusted run ends are strictly 
increasing
+    // and end at `total_len`. Physical length matches `all_values`.
+    let run_ends = unsafe { RunEndBuffer::new_unchecked(run_ends_values, 0, 
total_len) };
+    Ok(Arc::new(unsafe {
+        RunArray::<R>::new_unchecked(data_type, run_ends, all_values)
+    }))
 }
 
 macro_rules! dict_helper {
@@ -1889,6 +1892,24 @@ mod tests {
         assert_eq!(&[10, 20, 30, 40], values.values());
     }
 
+    #[test]
+    fn test_concat_run_array_length_overflows_run_end_type() {
+        // 20_000 + 20_000 exceeds i16::MAX.
+        let array1 = RunArray::<Int16Type>::try_new(
+            &Int16Array::from(vec![20_000]),
+            &Int16Array::from(vec![1]),
+        )
+        .unwrap();
+        let array2 = RunArray::<Int16Type>::try_new(
+            &Int16Array::from(vec![20_000]),
+            &Int16Array::from(vec![2]),
+        )
+        .unwrap();
+
+        let err = concat(&[&array1, &array2]).unwrap_err();
+        assert!(matches!(err, ArrowError::RunEndIndexOverflowError));
+    }
+
     #[test]
     fn test_concat_sliced_run_array() {
         // Slicing away first run in both arrays
diff --git a/arrow-select/src/nullif.rs b/arrow-select/src/nullif.rs
index aa09a28631..cd6d0ec4a2 100644
--- a/arrow-select/src/nullif.rs
+++ b/arrow-select/src/nullif.rs
@@ -578,31 +578,13 @@ mod tests {
 
     /// Returns a new BooleanArray with no null buffer
     fn remove_null_buffer(array: &BooleanArray) -> BooleanArray {
-        make_array(
-            array
-                .into_data()
-                .into_builder()
-                .nulls(None)
-                .build()
-                .unwrap(),
-        )
-        .as_boolean()
-        .clone()
+        BooleanArray::new(array.values().clone(), None)
     }
 
     /// Returns a new BooleanArray with a null buffer where all values are 
valid
     fn remove_null_values(array: &BooleanArray) -> BooleanArray {
         let len = array.len();
         let new_nulls = NullBuffer::from_iter(std::iter::repeat_n(true, len));
-        make_array(
-            array
-                .into_data()
-                .into_builder()
-                .nulls(Some(new_nulls))
-                .build()
-                .unwrap(),
-        )
-        .as_boolean()
-        .clone()
+        BooleanArray::new(array.values().clone(), Some(new_nulls))
     }
 }
diff --git a/arrow-select/src/take.rs b/arrow-select/src/take.rs
index 8c9c4ba3da..4ed8d856d4 100644
--- a/arrow-select/src/take.rs
+++ b/arrow-select/src/take.rs
@@ -1674,7 +1674,6 @@ mod tests {
     use super::*;
     use arrow_array::builder::*;
     use arrow_buffer::{IntervalDayTime, IntervalMonthDayNano};
-    use arrow_data::ArrayData;
     use arrow_schema::{Field, Fields, TimeUnit, UnionFields};
     use num_traits::ToPrimitive;
 
@@ -2174,18 +2173,12 @@ mod tests {
     #[test]
     fn test_take_bool_nullable_index() {
         // indices where the masked invalid elements would be out of bounds
-        let index_data = ArrayData::try_new(
-            DataType::UInt32,
-            6,
-            Some(Buffer::from_iter(vec![
+        let index = UInt32Array::new(
+            ScalarBuffer::from(vec![99, 0, 999, 1, 9999, 2]),
+            Some(NullBuffer::from(vec![
                 false, true, false, true, false, true,
             ])),
-            0,
-            vec![Buffer::from_iter(vec![99, 0, 999, 1, 9999, 2])],
-            vec![],
-        )
-        .unwrap();
-        let index = UInt32Array::from(index_data);
+        );
         test_take_boolean_arrays(
             vec![Some(true), None, Some(false)],
             &index,
@@ -2197,18 +2190,12 @@ mod tests {
     #[test]
     fn test_take_bool_nullable_index_nonnull_values() {
         // indices where the masked invalid elements would be out of bounds
-        let index_data = ArrayData::try_new(
-            DataType::UInt32,
-            6,
-            Some(Buffer::from_iter(vec![
+        let index = UInt32Array::new(
+            ScalarBuffer::from(vec![99, 0, 999, 1, 9999, 2]),
+            Some(NullBuffer::from(vec![
                 false, true, false, true, false, true,
             ])),
-            0,
-            vec![Buffer::from_iter(vec![99, 0, 999, 1, 9999, 2])],
-            vec![],
-        )
-        .unwrap();
-        let index = UInt32Array::from(index_data);
+        );
         test_take_boolean_arrays(
             vec![Some(true), Some(true), Some(false)],
             &index,
@@ -2464,20 +2451,11 @@ mod tests {
     macro_rules! test_take_list {
         ($offset_type:ty, $list_data_type:ident, $list_array_type:ident) => {{
             // Construct a value array, [[0,0,0], [-1,-2,-1], [], [2,3]]
-            let value_data = Int32Array::from(vec![0, 0, 0, -1, -2, -1, 2, 
3]).into_data();
-            // Construct offsets
-            let value_offsets: [$offset_type; 5] = [0, 3, 6, 6, 8];
-            let value_offsets = Buffer::from_slice_ref(&value_offsets);
-            // Construct a list array from the above two
-            let list_data_type =
-                
DataType::$list_data_type(Arc::new(Field::new_list_field(DataType::Int32, 
false)));
-            let list_data = ArrayData::builder(list_data_type.clone())
-                .len(4)
-                .add_buffer(value_offsets)
-                .add_child_data(value_data)
-                .build()
-                .unwrap();
-            let list_array = $list_array_type::from(list_data);
+            let values = Arc::new(Int32Array::from(vec![0, 0, 0, -1, -2, -1, 
2, 3]));
+            let value_offsets =
+                OffsetBuffer::<$offset_type>::new(ScalarBuffer::from(vec![0, 
3, 6, 6, 8]));
+            let field = Arc::new(Field::new_list_field(DataType::Int32, 
false));
+            let list_array = $list_array_type::new(Arc::clone(&field), 
value_offsets, values, None);
 
             // index returns: [[2,3], null, [-1,-2,-1], [], [0,0,0]]
             let index = UInt32Array::from(vec![Some(3), None, Some(1), 
Some(2), Some(0)]);
@@ -2487,7 +2465,7 @@ mod tests {
 
             // construct a value array with expected results:
             // [[2,3], null, [-1,-2,-1], [], [0,0,0]]
-            let expected_data = Int32Array::from(vec![
+            let expected_values = Arc::new(Int32Array::from(vec![
                 Some(2),
                 Some(3),
                 Some(-1),
@@ -2496,21 +2474,16 @@ mod tests {
                 Some(0),
                 Some(0),
                 Some(0),
-            ])
-            .into_data();
-            // construct offsets
-            let expected_offsets: [$offset_type; 6] = [0, 2, 2, 5, 5, 8];
-            let expected_offsets = Buffer::from_slice_ref(&expected_offsets);
-            // construct list array from the two
-            let expected_list_data = ArrayData::builder(list_data_type)
-                .len(5)
+            ]));
+            let expected_offsets =
+                OffsetBuffer::<$offset_type>::new(ScalarBuffer::from(vec![0, 
2, 2, 5, 5, 8]));
+            let expected_list_array = $list_array_type::new(
+                field,
+                expected_offsets,
+                expected_values,
                 // null buffer remains the same as only the indices have nulls
-                .nulls(index.nulls().cloned())
-                .add_buffer(expected_offsets)
-                .add_child_data(expected_data)
-                .build()
-                .unwrap();
-            let expected_list_array = 
$list_array_type::from(expected_list_data);
+                index.nulls().cloned(),
+            );
 
             assert_eq!(a, &expected_list_array);
         }};
@@ -2519,7 +2492,7 @@ mod tests {
     macro_rules! test_take_list_with_value_nulls {
         ($offset_type:ty, $list_data_type:ident, $list_array_type:ident) => {{
             // Construct a value array, [[0,null,0], [-1,-2,3], [null], 
[5,null]]
-            let value_data = Int32Array::from(vec![
+            let values = Arc::new(Int32Array::from(vec![
                 Some(0),
                 None,
                 Some(0),
@@ -2529,22 +2502,11 @@ mod tests {
                 None,
                 Some(5),
                 None,
-            ])
-            .into_data();
-            // Construct offsets
-            let value_offsets: [$offset_type; 5] = [0, 3, 6, 7, 9];
-            let value_offsets = Buffer::from_slice_ref(&value_offsets);
-            // Construct a list array from the above two
-            let list_data_type =
-                
DataType::$list_data_type(Arc::new(Field::new_list_field(DataType::Int32, 
true)));
-            let list_data = ArrayData::builder(list_data_type.clone())
-                .len(4)
-                .add_buffer(value_offsets)
-                .null_bit_buffer(Some(Buffer::from([0b11111111])))
-                .add_child_data(value_data)
-                .build()
-                .unwrap();
-            let list_array = $list_array_type::from(list_data);
+            ]));
+            let value_offsets =
+                OffsetBuffer::<$offset_type>::new(ScalarBuffer::from(vec![0, 
3, 6, 7, 9]));
+            let field = Arc::new(Field::new_list_field(DataType::Int32, true));
+            let list_array = $list_array_type::new(Arc::clone(&field), 
value_offsets, values, None);
 
             // index returns: [[null], null, [-1,-2,3], [2,null], [0,null,0]]
             let index = UInt32Array::from(vec![Some(2), None, Some(1), 
Some(3), Some(0)]);
@@ -2554,7 +2516,7 @@ mod tests {
 
             // construct a value array with expected results:
             // [[null], null, [-1,-2,3], [5,null], [0,null,0]]
-            let expected_data = Int32Array::from(vec![
+            let expected_values = Arc::new(Int32Array::from(vec![
                 None,
                 Some(-1),
                 Some(-2),
@@ -2564,21 +2526,16 @@ mod tests {
                 Some(0),
                 None,
                 Some(0),
-            ])
-            .into_data();
-            // construct offsets
-            let expected_offsets: [$offset_type; 6] = [0, 1, 1, 4, 6, 9];
-            let expected_offsets = Buffer::from_slice_ref(&expected_offsets);
-            // construct list array from the two
-            let expected_list_data = ArrayData::builder(list_data_type)
-                .len(5)
+            ]));
+            let expected_offsets =
+                OffsetBuffer::<$offset_type>::new(ScalarBuffer::from(vec![0, 
1, 1, 4, 6, 9]));
+            let expected_list_array = $list_array_type::new(
+                field,
+                expected_offsets,
+                expected_values,
                 // null buffer remains the same as only the indices have nulls
-                .nulls(index.nulls().cloned())
-                .add_buffer(expected_offsets)
-                .add_child_data(expected_data)
-                .build()
-                .unwrap();
-            let expected_list_array = 
$list_array_type::from(expected_list_data);
+                index.nulls().cloned(),
+            );
 
             assert_eq!(a, &expected_list_array);
         }};
@@ -2587,7 +2544,7 @@ mod tests {
     macro_rules! test_take_list_with_nulls {
         ($offset_type:ty, $list_data_type:ident, $list_array_type:ident) => {{
             // Construct a value array, [[0,null,0], [-1,-2,3], null, [5,null]]
-            let value_data = Int32Array::from(vec![
+            let values = Arc::new(Int32Array::from(vec![
                 Some(0),
                 None,
                 Some(0),
@@ -2596,22 +2553,14 @@ mod tests {
                 Some(3),
                 Some(5),
                 None,
-            ])
-            .into_data();
-            // Construct offsets
-            let value_offsets: [$offset_type; 5] = [0, 3, 6, 6, 8];
-            let value_offsets = Buffer::from_slice_ref(&value_offsets);
-            // Construct a list array from the above two
-            let list_data_type =
-                
DataType::$list_data_type(Arc::new(Field::new_list_field(DataType::Int32, 
true)));
-            let list_data = ArrayData::builder(list_data_type.clone())
-                .len(4)
-                .add_buffer(value_offsets)
-                .null_bit_buffer(Some(Buffer::from([0b11111011])))
-                .add_child_data(value_data)
-                .build()
-                .unwrap();
-            let list_array = $list_array_type::from(list_data);
+            ]));
+            let value_offsets =
+                OffsetBuffer::<$offset_type>::new(ScalarBuffer::from(vec![0, 
3, 6, 6, 8]));
+            let field = Arc::new(Field::new_list_field(DataType::Int32, true));
+            // the entry at index 2 is null
+            let list_nulls = NullBuffer::from(vec![true, true, false, true]);
+            let list_array =
+                $list_array_type::new(Arc::clone(&field), value_offsets, 
values, Some(list_nulls));
 
             // index returns: [null, null, [-1,-2,3], [5,null], [0,null,0]]
             let index = UInt32Array::from(vec![Some(2), None, Some(1), 
Some(3), Some(0)]);
@@ -2621,7 +2570,7 @@ mod tests {
 
             // construct a value array with expected results:
             // [null, null, [-1,-2,3], [5,null], [0,null,0]]
-            let expected_data = Int32Array::from(vec![
+            let expected_values = Arc::new(Int32Array::from(vec![
                 Some(-1),
                 Some(-2),
                 Some(3),
@@ -2630,25 +2579,17 @@ mod tests {
                 Some(0),
                 None,
                 Some(0),
-            ])
-            .into_data();
-            // construct offsets
-            let expected_offsets: [$offset_type; 6] = [0, 0, 0, 3, 5, 8];
-            let expected_offsets = Buffer::from_slice_ref(&expected_offsets);
-            // construct list array from the two
-            let mut null_bits: [u8; 1] = [0; 1];
-            bit_util::set_bit(&mut null_bits, 2);
-            bit_util::set_bit(&mut null_bits, 3);
-            bit_util::set_bit(&mut null_bits, 4);
-            let expected_list_data = ArrayData::builder(list_data_type)
-                .len(5)
-                // null buffer must be recalculated as both values and indices 
have nulls
-                .null_bit_buffer(Some(Buffer::from(null_bits)))
-                .add_buffer(expected_offsets)
-                .add_child_data(expected_data)
-                .build()
-                .unwrap();
-            let expected_list_array = 
$list_array_type::from(expected_list_data);
+            ]));
+            let expected_offsets =
+                OffsetBuffer::<$offset_type>::new(ScalarBuffer::from(vec![0, 
0, 0, 3, 5, 8]));
+            // null buffer must be recalculated as both values and indices 
have nulls
+            let expected_nulls = NullBuffer::from(vec![false, false, true, 
true, true]);
+            let expected_list_array = $list_array_type::new(
+                field,
+                expected_offsets,
+                expected_values,
+                Some(expected_nulls),
+            );
 
             assert_eq!(a, &expected_list_array);
         }};
@@ -2958,19 +2899,12 @@ mod tests {
     #[should_panic(expected = "index out of bounds: the len is 4 but the index 
is 1000")]
     fn test_take_list_out_of_bounds() {
         // Construct a value array, [[0,0,0], [-1,-2,-1], [2,3]]
-        let value_data = Int32Array::from(vec![0, 0, 0, -1, -2, -1, 2, 
3]).into_data();
+        let values = Arc::new(Int32Array::from(vec![0, 0, 0, -1, -2, -1, 2, 
3]));
         // Construct offsets
-        let value_offsets = Buffer::from_slice_ref([0, 3, 6, 8]);
+        let value_offsets = 
OffsetBuffer::<i32>::new(ScalarBuffer::from(vec![0, 3, 6, 8]));
         // Construct a list array from the above two
-        let list_data_type =
-            DataType::List(Arc::new(Field::new_list_field(DataType::Int32, 
false)));
-        let list_data = ArrayData::builder(list_data_type)
-            .len(3)
-            .add_buffer(value_offsets)
-            .add_child_data(value_data)
-            .build()
-            .unwrap();
-        let list_array = ListArray::from(list_data);
+        let field = Arc::new(Field::new_list_field(DataType::Int32, false));
+        let list_array = ListArray::new(field, value_offsets, values, None);
 
         let index = UInt32Array::from(vec![1000]);
 
diff --git a/arrow/benches/concatenate_kernel.rs 
b/arrow/benches/concatenate_kernel.rs
index 497c2e8f1e..6ae97c90a9 100644
--- a/arrow/benches/concatenate_kernel.rs
+++ b/arrow/benches/concatenate_kernel.rs
@@ -235,6 +235,21 @@ fn add_benchmark(c: &mut Criterion) {
             |b| b.iter(|| bench_concat_arrays(&array_refs)),
         );
     }
+
+    // (name, logical_len, physical_len, num_arrays)
+    for (name, logical, physical, n) in [
+        ("small logical=32 physical=4 x4", 32usize, 4usize, 4usize),
+        ("logical=1024 physical=128 x8", 1024, 128, 8),
+        ("logical=8192 physical=1024 x10", 8192, 1024, 10),
+    ] {
+        let arrays: Vec<RunArray<Int32Type>> = (0..n)
+            .map(|_| create_primitive_run_array::<Int32Type, 
Int32Type>(logical, physical))
+            .collect();
+        let array_refs: Vec<&dyn Array> = arrays.iter().map(|a| a as &dyn 
Array).collect();
+        c.bench_function(&format!("concat run_end i32 {name}"), |b| {
+            b.iter(|| bench_concat_arrays(&array_refs))
+        });
+    }
 }
 
 criterion_group!(benches, add_benchmark);

Reply via email to