This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11992-76139438ddd6f43159e4bf013d22238b4c82670b in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit 9ffd63d528e924d9c0faceef0027e3766b615c98 Author: Neil Conway <[email protected]> AuthorDate: Thu Oct 29 19:02:38 2026 +0000 perf: optimize building out-of-line views in `MutableArrayData` (#11992) # Which issue does this PR close? - Split out from #11656 - Related to #11770 # Rationale for this change This PR applies the optimization from #11770 to an additional code path: when `MutableArrayData` copies string views into an array that already has data buffers, the buffer index in the copied views must be updated. We previously did that by unpacking the raw view (`u128` -> `ByteView`), adding the buffer index offset, and repacking the view. It is faster to do this by using bitwise shifts and addition directly. This also avoids a data-dependent branch, which seems to have been poorly predicted on x86. Benchmarks: x86 (AMD EPYC Milan): - zip_kernels, string views, 8192 rows of 3 to 19 bytes - array vs scalar - all_true: 31.19 µs → 6.41 µs (−79.4%) - 99pct_true: 35.84 µs → 10.94 µs (−69.5%) - 90pct_true: 63.87 µs → 39.54 µs (−38.1%) - 50pct_true: 129.4 µs → 113.1 µs (−12.6%) - true_then_false: 55.02 µs → 43.06 µs (−21.7%) - 10pct_true: 96.03 µs → 92.53 µs (−3.6%) - 1pct_true: 81.66 µs → 82.20 µs (+0.7%) - all_false: 78.78 µs → 81.76 µs (+3.8%) (runs 6.7% apart) - 50pct_nulls: 112.6 µs → 107.5 µs (−4.5%) (runs 6.4% apart) - scalar vs array - all_true: 82.50 µs → 85.26 µs (+3.3%) (runs 4.4% apart) - 99pct_true: 86.52 µs → 87.81 µs (+1.5%) (runs 4.4% apart) - 90pct_true: 100.9 µs → 112 µs (+11.0%) (runs 31.5% apart) - 50pct_true: 136.6 µs → 121 µs (−11.4%) (runs 5.6% apart) - true_then_false: 58.65 µs → 47.93 µs (−18.3%) (runs 7.2% apart) - 10pct_true: 69.18 µs → 44.68 µs (−35.4%) (runs 5.1% apart) - 1pct_true: 39.66 µs → 15.05 µs (−62.1%) (runs 18.5% apart) - all_false: 30.34 µs → 7.15 µs (−76.4%) (runs 17.9% apart) - 50pct_nulls: 102.3 µs → 81.27 µs (−20.6%) (runs 8.6% apart) - array vs array - all_true: 30.94 µs → 6.82 µs (−77.9%) (runs 2.6% apart) - 99pct_true: 37.47 µs → 12.13 µs (−67.6%) (runs 2.8% apart) - 90pct_true: 71.25 µs → 45.33 µs (−36.4%) (runs 13.0% apart) - 50pct_true: 77.46 µs → 79.37 µs (+2.5%) (runs 2.9% apart) - true_then_false: 31.60 µs → 7.00 µs (−77.9%) (runs 3.5% apart) - 10pct_true: 72.32 µs → 47.72 µs (−34.0%) (runs 25.3% apart) - 1pct_true: 37.44 µs → 12.04 µs (−67.8%) (runs 5.9% apart) - all_false: 31.26 µs → 7.01 µs (−77.6%) (runs 4.9% apart) - 50pct_nulls: 77.66 µs → 78.57 µs (+1.2%) - mutable_array - mutable str_view 1024: 294.5 µs → 267.7 µs (−9.1%) - mutable str_view short slices 1024: 19.84 µs → 17.69 µs (−10.9%) (runs 4.3% apart) ARM (M4 Max): - zip_kernels, string views, 8192 rows of 3 to 19 bytes - array vs scalar - all_true: 4.27 µs → 3.55 µs (−16.9%) (runs 2.9% apart) - 99pct_true: 7.22 µs → 6.61 µs (−8.4%) (runs 5.6% apart) - 90pct_true: 23.69 µs → 22.70 µs (−4.2%) - 50pct_true: 63.98 µs → 64.23 µs (+0.4%) - true_then_false: 25.21 µs → 25.53 µs (+1.3%) (runs 3.9% apart) - 10pct_true: 53.92 µs → 56.04 µs (+3.9%) (runs 6.1% apart) - 1pct_true: 49.47 µs → 51.00 µs (+3.1%) (runs 4.0% apart) - all_false: 45.95 µs → 46.10 µs (+0.3%) - 50pct_nulls: 60.34 µs → 60.33 µs (−0.0%) - scalar vs array - all_true: 49.63 µs → 50.10 µs (+0.9%) - 99pct_true: 53.14 µs → 53.63 µs (+0.9%) - 90pct_true: 57.90 µs → 58.62 µs (+1.2%) - 50pct_true: 68.40 µs → 68.32 µs (−0.1%) - true_then_false: 28.06 µs → 28.33 µs (+1.0%) (runs 3.7% apart) - 10pct_true: 27.70 µs → 27.16 µs (−2.0%) (runs 3.4% apart) - 1pct_true: 10.63 µs → 10.11 µs (−4.9%) (runs 6.1% apart) - all_false: 4.22 µs → 3.58 µs (−15.3%) (runs 6.1% apart) - 50pct_nulls: 47.07 µs → 46.42 µs (−1.4%) (runs 2.7% apart) - array vs array - all_true: 4.40 µs → 3.59 µs (−18.3%) - 99pct_true: 7.51 µs → 6.97 µs (−7.2%) (runs 7.4% apart) - 90pct_true: 24.69 µs → 24.15 µs (−2.2%) (runs 7.6% apart) - 50pct_true: 49.95 µs → 51.71 µs (+3.5%) (runs 11.4% apart) - true_then_false: 4.50 µs → 3.64 µs (−18.9%) - 10pct_true: 24.68 µs → 23.40 µs (−5.2%) - 1pct_true: 7.63 µs → 6.79 µs (−11.1%) (runs 3.1% apart) - all_false: 4.38 µs → 3.60 µs (−17.7%) (runs 2.2% apart) - 50pct_nulls: 49.16 µs → 50.45 µs (+2.6%) (runs 4.2% apart) - mutable_array - mutable str_view 1024: 186.4 µs → 156.2 µs (−16.2%) (runs 6.3% apart) - mutable str_view short slices 1024: 10.49 µs → 10.04 µs (−4.3%) (runs 4.7% apart) # What changes are included in this PR? See above. # Are these changes tested? Yes. `test_string_view` now covers strings of 12 and 13 bytes, on either side of the inline limit, copied from the second input. # Are there any significant user-facing changes? No. --- arrow-data/src/transform/mod.rs | 22 ++++++++++++++-------- arrow/tests/array_transform.rs | 14 ++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/arrow-data/src/transform/mod.rs b/arrow-data/src/transform/mod.rs index cc6c20b2fd..ba9ad364c8 100644 --- a/arrow-data/src/transform/mod.rs +++ b/arrow-data/src/transform/mod.rs @@ -20,7 +20,7 @@ //! Provides utilities for creating, manipulating, and converting Arrow arrays //! made of primitive types, strings, and nested types. -use super::{ArrayData, ArrayDataBuilder, ByteView, data::new_buffers}; +use super::{ArrayData, ArrayDataBuilder, MAX_INLINE_VIEW_LEN, data::new_buffers}; use crate::bit_mask::set_bits; use arrow_buffer::buffer::{BooleanBuffer, NullBuffer}; use arrow_buffer::{ @@ -228,13 +228,19 @@ fn build_extend_view(array: &ArrayData, buffer_offset: u32) -> Extend<'_> { mutable .buffer1 .extend(views[start..start + len].iter().map(|v| { - let len = *v as u32; - if len <= 12 { - return *v; // Stored inline - } - let mut view = ByteView::from(*v); - view.buffer_index += buffer_offset; - view.into() + // A view of a string of 12 bytes or fewer stores the + // string inline and has no buffer index, so adding 0 + // leaves it unchanged. + let offset = if *v as u32 > MAX_INLINE_VIEW_LEN { + buffer_offset + } else { + 0 + }; + // The buffer index is bits 64 to 95 of a view. Adding + // `buffer_offset` gives the string's buffer index in the + // output, which fits in 32 bits, so the sum never carries + // into bit 96. + v + ((offset as u128) << 64) })); Ok(()) }, diff --git a/arrow/tests/array_transform.rs b/arrow/tests/array_transform.rs index 287fa684a3..17d515059c 100644 --- a/arrow/tests/array_transform.rs +++ b/arrow/tests/array_transform.rs @@ -1065,12 +1065,10 @@ fn test_extend_nulls_not_nullable() { fn test_string_view() { let a1 = StringViewArray::from(vec!["foo", "very long string over 12 bytes", "bar"]).into_data(); - let a2 = StringViewArray::from_iter(vec![ - Some("bar"), - None, - Some("long string also over 12 bytes"), - ]) - .into_data(); + // Strings of 12 bytes are stored in their views and strings of 13 bytes + // are not. + let a2 = StringViewArray::from_iter(vec![Some("twelve bytes"), None, Some("thirteen byte")]) + .into_data(); a1.validate_full().unwrap(); a2.validate_full().unwrap(); @@ -1091,10 +1089,10 @@ fn test_string_view() { assert_eq!( v, vec![ - Some("bar"), + Some("twelve bytes"), Some("very long string over 12 bytes"), Some("foo"), - Some("long string also over 12 bytes") + Some("thirteen byte") ] ) }
