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/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new d4160119db perf: avoid per-row copy in Spark hex byte encoding (#23473)
d4160119db is described below

commit d4160119db8c6b348928af100393c23783caf05e
Author: Andy Grove <[email protected]>
AuthorDate: Fri Jul 17 09:18:58 2026 -0600

    perf: avoid per-row copy in Spark hex byte encoding (#23473)
    
    ## Which issue does this PR close?
    
    N/A
    
    ## Rationale for this change
    
    The Spark `hex` function's byte-encoding path (`hex_encode_bytes`)
    filled a reusable per-row `Vec` and then copied it into a
    `StringBuilder` for every row. Since each input byte maps to exactly two
    hex characters, the output can be written once directly into a single
    value buffer, eliminating the per-row copy.
    
    ## What changes are included in this PR?
    
    Rewrote `hex_encode_bytes` to write hex digits directly into one growing
    value buffer with manually-tracked offsets and a `NullBufferBuilder`,
    constructing the `StringArray` via `new_unchecked` (the buffer contains
    only ASCII hex digits, so the existing no-validation behavior is
    preserved). The 256-entry lookup table and all other hex paths are
    unchanged.
    
    ## Are these changes tested?
    
    Existing tests (`function::math::hex`) pass unchanged.
    
    Benchmark (`datafusion/spark/benches/hex.rs`):
    
    - hex_binary/1024·4096·8192: ~13.8% / 14.3% / 13.9% faster
    - hex_utf8/1024·4096·8192: ~14.1% / 14.9% / 15.0% faster
    
    ## Are there any user-facing changes?
    
    No
    
    Co-authored-by: Andrew Lamb <[email protected]>
---
 datafusion/spark/src/function/math/hex.rs | 44 ++++++++++++++++++++++---------
 1 file changed, 32 insertions(+), 12 deletions(-)

diff --git a/datafusion/spark/src/function/math/hex.rs 
b/datafusion/spark/src/function/math/hex.rs
index c7b82d5373..a283bd8fa7 100644
--- a/datafusion/spark/src/function/math/hex.rs
+++ b/datafusion/spark/src/function/math/hex.rs
@@ -18,7 +18,8 @@
 use std::str::from_utf8_unchecked;
 use std::sync::Arc;
 
-use arrow::array::{Array, ArrayRef, StringBuilder};
+use arrow::array::{Array, ArrayRef, NullBufferBuilder, StringArray, 
StringBuilder};
+use arrow::buffer::{Buffer, OffsetBuffer};
 use arrow::datatypes::DataType;
 use arrow::{
     array::{as_dictionary_array, as_largestring_array, as_string_array},
@@ -167,40 +168,59 @@ where
     I: Iterator<Item = Option<T>>,
     T: AsRef<[u8]> + 'a,
 {
-    let mut builder = StringBuilder::with_capacity(len, len * 64);
-    let mut buffer = Vec::with_capacity(64);
     let lookup = if lowercase {
         &HEX_LOOKUP_LOWER
     } else {
         &HEX_LOOKUP_UPPER
     };
 
+    // Write hex digits directly into one growing value buffer, tracking 
offsets
+    // ourselves. Each input byte becomes exactly two output bytes, so there is
+    // no per-row `String`/`StringBuilder` copy — the hex digits are written 
once
+    // into the final buffer.
+    let mut values: Vec<u8> = Vec::with_capacity(len * 64);
+    let mut offsets: Vec<i32> = Vec::with_capacity(len + 1);
+    offsets.push(0);
+    let mut nulls = NullBufferBuilder::new(len);
+
     for v in iter {
         if let Some(b) = v {
             let bytes = b.as_ref();
-            buffer.clear();
             let additional = bytes
                 .len()
                 .checked_mul(2)
                 .ok_or_else(|| exec_datafusion_err!("hex output size 
overflow"))?;
-            buffer.try_reserve(additional).map_err(|e| {
+            values.try_reserve(additional).map_err(|e| {
                 exec_datafusion_err!(
                     "failed to reserve {additional} bytes for hex output: {e}"
                 )
             })?;
             for &byte in bytes {
-                buffer.extend_from_slice(&lookup[byte as usize]);
-            }
-            // SAFETY: buffer contains only ASCII hex digits, which are valid 
UTF-8.
-            unsafe {
-                builder.append_value(from_utf8_unchecked(&buffer));
+                values.extend_from_slice(&lookup[byte as usize]);
             }
+            nulls.append_non_null();
         } else {
-            builder.append_null();
+            nulls.append_null();
         }
+        offsets.push(
+            i32::try_from(values.len()).map_err(|_| {
+                exec_datafusion_err!("hex output exceeds i32 offset range")
+            })?,
+        );
     }
 
-    Ok(Arc::new(builder.finish()))
+    // SAFETY: the value buffer contains only ASCII hex digits (valid UTF-8) 
and
+    // the offsets are monotonically increasing and end at `values.len()`, so 
the
+    // array invariants hold. This mirrors the previous `from_utf8_unchecked`
+    // path and avoids a redundant UTF-8 validation pass over the whole buffer.
+    let array = unsafe {
+        StringArray::new_unchecked(
+            OffsetBuffer::new(offsets.into()),
+            Buffer::from_vec(values),
+            nulls.finish(),
+        )
+    };
+    Ok(Arc::new(array))
 }
 
 /// Generic hex encoding for int64 type


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to