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/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new ef22fc44f3 perf: Optimize appending out-of-line views in `concat`, 
`BatchCoalescer` (#11770)
ef22fc44f3 is described below

commit ef22fc44f3a92b142c1583261418326e565811a9
Author: Neil Conway <[email protected]>
AuthorDate: Tue Oct 27 05:29:35 2026 +0000

    perf: Optimize appending out-of-line views in `concat`, `BatchCoalescer` 
(#11770)
    
    # Which issue does this PR close?
    
    - N/A
    
    # Rationale for this change
    
    When views are appended to an array that already has data buffers, the
    view of each out-of-line string must have its buffer index increased by
    the number of buffers already in the array. The obvious way to do this
    is to unpack the view (`u128` -> `ByteView`), update the `buffer_index`
    field, and re-pack it. During the course of working on #11656, we found
    that it is significantly faster to instead update the corresponding bits
    in the raw view directly, via shifting and addition.
    
    This PR applies this optimization to two additional places:
    `GenericByteViewBuilder::append_array`, which `concat` uses for string
    and binary view arrays, and
    `InProgressByteViewArray::append_views_and_update_buffer_index`, which
    is used by `BatchCoalescer`.
    
    # What changes are included in this PR?
    
    See above. We also add a sanity-check that the output buffer count fits
    in 32 bits (which will always be true in practice; the previous behavior
    of `GenericByteViewBuilder::append_array` was to silently truncate the
    buffer count in this situation). The benchmark improvements for `concat`
    and `coalesce_kernels` landed separately in #11768. This PR also passes
    the RNG seed to the `Utf8` and `Dictionary(Int32, Utf8)` inputs of
    `coalesce_kernels`, which previously generated the same strings for
    every batch.
    
    # Benchmarks
    
    - AMD EPYC Milan (x86)
      - coalesce_kernels, filter keeping 1% of rows
        - single_utf8view, no nulls: 3.30 ms → 3.20 ms (−3.0%)
    - single_utf8view (string_len=13..40), no nulls: 3.29 ms → 3.23 ms
    (−2.0%) (runs 3% apart)
    - single_binaryview, no nulls: 3.30 ms → 3.17 ms (−4.1%) (runs 6% apart)
    - mixed_utf8view (max_string_len=20), no nulls: 1.78 ms → 1.73 ms
    (−2.6%)
    - mixed_binaryview (max_string_len=20), no nulls: 1.86 ms → 1.81 ms
    (−2.9%) (runs 7% apart)
        - single_utf8view, 10% nulls: 4.93 ms → 4.80 ms (−2.8%)
    - single_utf8view (string_len=13..40), 10% nulls: 5.05 ms → 4.89 ms
    (−3.2%)
        - single_binaryview, 10% nulls: 4.92 ms → 4.89 ms (−0.5%)
    - mixed_utf8view (max_string_len=20), 10% nulls: 3.09 ms → 3.05 ms
    (−1.3%)
    - mixed_binaryview (max_string_len=20), 10% nulls: 3.08 ms → 3.06 ms
    (−0.4%)
      - coalesce_kernels, filter keeping 80% of rows
        - single_utf8view, no nulls: 868.1 µs → 783.3 µs (−9.8%)
    - single_utf8view (string_len=13..40), no nulls: 867.8 µs → 800.2 µs
    (−7.8%) (runs 4% apart)
    - single_binaryview, no nulls: 868.5 µs → 793.2 µs (−8.7%) (runs 2.9%
    apart)
    - mixed_utf8view (max_string_len=20), no nulls: 730.6 µs → 701.1 µs
    (−4.0%)
    - mixed_binaryview (max_string_len=20), no nulls: 1.50 ms → 1.50 ms
    (+0.3%) (copies its strings, so doesn't reach the changed code)
        - single_utf8view, 10% nulls: 1.01 ms → 922.8 µs (−8.7%)
    - single_utf8view (string_len=13..40), 10% nulls: 1.02 ms → 949.7 µs
    (−6.5%)
        - single_binaryview, 10% nulls: 1.02 ms → 921.9 µs (−10.0%)
    - mixed_utf8view (max_string_len=20), 10% nulls: 900.1 µs → 869.6 µs
    (−3.4%)
    - mixed_binaryview (max_string_len=20), 10% nulls: 895.8 µs → 873.1 µs
    (−2.5%)
      - coalesce_kernels, take of 80% of rows
    - single_utf8view, no nulls: 824.6 µs → 757.0 µs (−8.2%) (runs 2.8%
    apart)
    - single_binaryview, no nulls: 830.5 µs → 737.2 µs (−11.2%) (runs 4%
    apart)
    - mixed_utf8view (max_string_len=20), no nulls: 563.8 µs → 515.6 µs
    (−8.5%) (runs 2.4% apart)
    - mixed_binaryview (max_string_len=20), no nulls: 1.43 ms → 1.43 ms
    (+0.2%) (copies its strings, so doesn't reach the changed code)
    - single_utf8view, 10% nulls: 1.17 ms → 1.06 ms (−10.2%) (runs 7% apart)
    - single_binaryview, 10% nulls: 1.15 ms → 1.05 ms (−8.5%) (runs 4%
    apart)
    - mixed_utf8view (max_string_len=20), 10% nulls: 924.3 µs → 885.9 µs
    (−4.2%) (runs 3% apart)
    - mixed_binaryview (max_string_len=20), 10% nulls: 919.9 µs → 906.8 µs
    (−1.4%) (runs 6% apart)
      - concat of 10 string view arrays of 8192 rows
        - all_inline str_len=12, no nulls: 31.57 µs → 31.54 µs (−0.1%)
        - str_len=20, no nulls: 74.88 µs → 48.63 µs (−35.1%)
        - str_len=128, no nulls: 75.02 µs → 49.00 µs (−34.7%)
        - mixed str_len=3..20, no nulls: 73.95 µs → 48.69 µs (−34.2%)
        - all_inline str_len=12, 20% nulls: 32.44 µs → 32.39 µs (−0.2%)
        - str_len=20, 20% nulls: 76.59 µs → 50.01 µs (−34.7%)
        - str_len=128, 20% nulls: 76.39 µs → 49.99 µs (−34.6%)
    - mixed str_len=3..20, 20% nulls: 75.54 µs → 49.92 µs (−33.9%) (runs 4%
    apart)
    - Apple M4 Max (ARM)
      - coalesce_kernels, filter keeping 1% of rows
    - single_utf8view, no nulls: 1.62 ms → 1.51 ms (−6.9%) (runs 4% apart)
    - single_utf8view (string_len=13..40), no nulls: 1.62 ms → 1.54 ms
    (−5.0%) (runs 4% apart)
    - single_binaryview, no nulls: 1.57 ms → 1.48 ms (−5.7%) (runs 2.4%
    apart)
    - mixed_utf8view (max_string_len=20), no nulls: 1.08 ms → 1.04 ms
    (−3.3%)
    - mixed_binaryview (max_string_len=20), no nulls: 1.12 ms → 1.05 ms
    (−6.1%) (runs 5% apart)
    - single_utf8view, 10% nulls: 2.44 ms → 2.43 ms (−0.3%) (runs 2.2%
    apart)
    - single_utf8view (string_len=13..40), 10% nulls: 2.49 ms → 2.45 ms
    (−1.4%) (runs 2.5% apart)
    - single_binaryview, 10% nulls: 2.45 ms → 2.47 ms (+1.0%) (runs 6%
    apart)
    - mixed_utf8view (max_string_len=20), 10% nulls: 2.61 ms → 2.54 ms
    (−2.6%)
    - mixed_binaryview (max_string_len=20), 10% nulls: 2.58 ms → 2.53 ms
    (−2.2%)
      - coalesce_kernels, filter keeping 80% of rows
        - single_utf8view, no nulls: 562.9 µs → 519.8 µs (−7.7%)
    - single_utf8view (string_len=13..40), no nulls: 562.7 µs → 520.0 µs
    (−7.6%)
        - single_binaryview, no nulls: 562.5 µs → 520.3 µs (−7.5%)
    - mixed_utf8view (max_string_len=20), no nulls: 535.7 µs → 517.7 µs
    (−3.4%)
    - mixed_binaryview (max_string_len=20), no nulls: 1.10 ms → 1.11 ms
    (+1.7%) (copies its strings, so doesn't reach the changed code)
        - single_utf8view, 10% nulls: 618.9 µs → 578.6 µs (−6.5%)
    - single_utf8view (string_len=13..40), 10% nulls: 617.9 µs → 574.5 µs
    (−7.0%)
        - single_binaryview, 10% nulls: 621.6 µs → 574.5 µs (−7.6%)
    - mixed_utf8view (max_string_len=20), 10% nulls: 600.8 µs → 581.4 µs
    (−3.2%)
    - mixed_binaryview (max_string_len=20), 10% nulls: 602.1 µs → 581.4 µs
    (−3.4%)
      - coalesce_kernels, take of 80% of rows
        - single_utf8view, no nulls: 458.1 µs → 397.7 µs (−13.2%)
    - single_binaryview, no nulls: 448.5 µs → 389.3 µs (−13.2%) (runs 3.0%
    apart)
    - mixed_utf8view (max_string_len=20), no nulls: 311.2 µs → 290.2 µs
    (−6.7%)
    - mixed_binaryview (max_string_len=20), no nulls: 995.3 µs → 999.1 µs
    (+0.4%) (copies its strings, so doesn't reach the changed code)
    - single_utf8view, 10% nulls: 641.5 µs → 560.2 µs (−12.7%) (runs 4%
    apart)
        - single_binaryview, 10% nulls: 639.6 µs → 545.5 µs (−14.7%)
    - mixed_utf8view (max_string_len=20), 10% nulls: 468.9 µs → 450.5 µs
    (−3.9%)
    - mixed_binaryview (max_string_len=20), 10% nulls: 470.5 µs → 448.9 µs
    (−4.6%)
      - concat of 10 string view arrays of 8192 rows
        - all_inline str_len=12, no nulls: 15.67 µs → 15.61 µs (−0.4%)
        - str_len=20, no nulls: 31.13 µs → 27.40 µs (−12.0%)
        - str_len=128, no nulls: 31.20 µs → 27.55 µs (−11.7%)
        - mixed str_len=3..20, no nulls: 30.80 µs → 27.24 µs (−11.6%)
        - all_inline str_len=12, 20% nulls: 16.16 µs → 16.10 µs (−0.4%)
        - str_len=20, 20% nulls: 31.77 µs → 28.10 µs (−11.5%)
        - str_len=128, 20% nulls: 31.84 µs → 28.28 µs (−11.2%)
        - mixed str_len=3..20, 20% nulls: 31.33 µs → 27.92 µs (−10.9%)
    
    # Are these changes tested?
    
    Covered by existing tests.
    
    # Are there any significant user-facing changes?
    
    Only in a corner case: if the output buffer count exceeds `u32::MAX`, we
    now panic rather than silently truncating the count.
---
 .../src/builder/generic_bytes_view_builder.rs      | 28 ++++++++++++++-----
 arrow-select/src/coalesce/byte_view.rs             | 25 ++++++++++++-----
 arrow/benches/coalesce_kernels.rs                  | 32 ++++++++++++++++------
 3 files changed, 62 insertions(+), 23 deletions(-)

diff --git a/arrow-array/src/builder/generic_bytes_view_builder.rs 
b/arrow-array/src/builder/generic_bytes_view_builder.rs
index 6ff209ef74..3c21b8624b 100644
--- a/arrow-array/src/builder/generic_bytes_view_builder.rs
+++ b/arrow-array/src/builder/generic_bytes_view_builder.rs
@@ -224,6 +224,10 @@ impl<T: ByteViewType + ?Sized> GenericByteViewBuilder<T> {
     /// Appends an array to the builder.
     /// This will flush any in-progress block and append the data buffers
     /// and add the (adapted) views.
+    ///
+    /// # Panics
+    ///
+    /// Panics if the builder would hold more than `u32::MAX` data buffers
     pub fn append_array(&mut self, array: &GenericByteViewArray<T>) {
         self.flush_in_progress();
         // keep original views if this array is the first to be added or if 
there are no data buffers (all inline views)
@@ -231,18 +235,28 @@ impl<T: ByteViewType + ?Sized> GenericByteViewBuilder<T> {
         let starting_buffer = self.completed.len() as u32;
 
         self.completed.extend(array.data_buffers().iter().cloned());
+        assert!(
+            u32::try_from(self.completed.len()).is_ok(),
+            "Too many blocks"
+        );
 
         if keep_views {
             self.views_buffer.extend_from_slice(array.views());
         } else {
             self.views_buffer.extend(array.views().iter().map(|v| {
-                let mut byte_view = ByteView::from(*v);
-                if byte_view.length > MAX_INLINE_VIEW_LEN {
-                    // Small views (<=12 bytes) are inlined, so only need to 
update large views
-                    byte_view.buffer_index += starting_buffer;
-                }
-
-                byte_view.as_u128()
+                // 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 {
+                    starting_buffer
+                } else {
+                    0
+                };
+                // The buffer index is bits 64 to 95 of a view. Adding
+                // `starting_buffer` gives the string's buffer index in the
+                // builder, which fits in 32 bits, so the sum never carries
+                // into bit 96.
+                v + ((offset as u128) << 64)
             }));
         }
 
diff --git a/arrow-select/src/coalesce/byte_view.rs 
b/arrow-select/src/coalesce/byte_view.rs
index 085bf1f1e7..468aab0b52 100644
--- a/arrow-select/src/coalesce/byte_view.rs
+++ b/arrow-select/src/coalesce/byte_view.rs
@@ -194,8 +194,12 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
             self.size_of_completed_buffers_from_current_source += buffers_size;
         }
 
-        let starting_buffer: u32 = self.completed.len().try_into().expect("too 
many buffers");
+        let starting_buffer = self.completed.len() as u32;
         self.completed.extend_from_slice(buffers);
+        assert!(
+            u32::try_from(self.completed.len()).is_ok(),
+            "too many buffers"
+        );
 
         if starting_buffer == 0 {
             // If there are no buffers, we can just use the views as is
@@ -203,12 +207,19 @@ impl<B: ByteViewType> InProgressByteViewArray<B> {
         } else {
             // If there are buffers, we need to update the buffer index
             let updated_views = views.iter().map(|v| {
-                let mut byte_view = ByteView::from(*v);
-                if byte_view.length > MAX_INLINE_VIEW_LEN {
-                    // Small views (<=12 bytes) are inlined, so only need to 
update large views
-                    byte_view.buffer_index += starting_buffer;
-                }
-                byte_view.as_u128()
+                // 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 {
+                    starting_buffer
+                } else {
+                    0
+                };
+                // The buffer index is bits 64 to 95 of a view. Adding
+                // `starting_buffer` 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)
             });
 
             self.views.extend(updated_views);
diff --git a/arrow/benches/coalesce_kernels.rs 
b/arrow/benches/coalesce_kernels.rs
index 974d2a4bb5..faaee8f6e6 100644
--- a/arrow/benches/coalesce_kernels.rs
+++ b/arrow/benches/coalesce_kernels.rs
@@ -1447,10 +1447,17 @@ impl DataStreamBuilder {
                 )
                 .with_timezone(Arc::clone(tz)),
             ),
-            DataType::Utf8 => Arc::new(create_string_array::<i32>(
-                self.batch_size,
-                self.null_density,
-            )), // TODO seed
+            // Strings of 0 to 399 bytes; `max_string_len` doesn't apply
+            DataType::Utf8 => Arc::new(
+                create_string_array_with_len_range_and_prefix_and_seed::<i32>(
+                    self.batch_size,
+                    self.null_density,
+                    0,
+                    399,
+                    "",
+                    seed,
+                ),
+            ),
             DataType::Binary => 
Arc::new(create_binary_array_with_len_range_and_prefix_and_seed::<
                 i32,
             >(
@@ -1482,11 +1489,18 @@ impl DataStreamBuilder {
                 if key_type.as_ref() == &DataType::Int32
                     && value_type.as_ref() == &DataType::Utf8 =>
             {
-                Arc::new(create_string_dict_array::<Int32Type>(
-                    self.batch_size,
-                    self.null_density,
-                    self.max_string_len,
-                )) // TODO seed
+                Arc::new(
+                    
create_string_array_with_len_range_and_prefix_and_seed::<i32>(
+                        self.batch_size,
+                        self.null_density,
+                        self.max_string_len,
+                        self.max_string_len,
+                        "",
+                        seed,
+                    )
+                    .iter()
+                    .collect::<DictionaryArray<Int32Type>>(),
+                )
             }
             DataType::FixedSizeBinary(size) => 
Arc::new(create_fixed_size_binary_array(
                 self.batch_size,

Reply via email to