sunchao commented on code in PR #7106:
URL: https://github.com/apache/datafusion-comet/pull/7106#discussion_r4324642892


##########
native/spark-expr/src/conversion_funcs/cast.rs:
##########
@@ -876,7 +878,63 @@ fn cast_binary_to_string<O: OffsetSizeTrait>(
 
     let num_rows = input.len();
     let offsets = input.value_offsets();
-    let value_bytes = offsets[num_rows].as_usize() - offsets[0].as_usize();
+    let start = offsets[0].as_usize();
+    let end = offsets[num_rows].as_usize();
+    // Bytes of the non-null rows. A null row can still span bytes, for 
example after `nullif`.
+    let live_bytes = match input.nulls().filter(|nulls| nulls.null_count() > 
0) {
+        Some(nulls) => nulls
+            .valid_indices()
+            .map(|i| offsets[i + 1].as_usize() - offsets[i].as_usize())
+            .sum(),
+        None => end - start,
+    };
+
+    // Check the bytes the rows span once. If they are valid UTF-8, reuse the 
input's value buffer;
+    // otherwise copy the rows in the valid prefix and decode the rest. Skip 
this when null rows
+    // span more than half of those bytes: decoding the live rows one at a 
time then reads less
+    // and returns a compact buffer instead of keeping the null rows' bytes 
alive.
+    let mut valid_prefix = "";
+    if matches!(
+        spark_cast_options.binary_output_style,
+        None | Some(BinaryOutputStyle::Utf8)
+    ) && 2 * live_bytes >= end - start
+    {
+        let data = input.value_data();
+        match std::str::from_utf8(&data[start..end]) {
+            Ok(valid) => {
+                // An offset splits a code point only if it points at a 
continuation byte.
+                if offsets.iter().all(|offset| {
+                    let offset = offset.as_usize();
+                    offset == end || !matches!(data[offset], 0x80..=0xBF)
+                }) {
+                    // Rebase a slice's offsets onto the narrowed value buffer.
+                    let offsets = if start == 0 {
+                        input.offsets().clone()
+                    } else {
+                        OffsetBuffer::from_lengths(input.offsets().lengths())
+                    };
+                    // SAFETY: the value buffer is valid UTF-8 and every 
offset falls on a char
+                    // boundary, which is what `GenericStringArray::try_new` 
checks.
+                    let strings = unsafe {
+                        GenericStringArray::<O>::new_unchecked(
+                            offsets,
+                            input.values().slice_with_length(start, end - 
start),

Review Comment:
   [P2] [P2] Compact slices that retain oversized backing allocations. For a 
grouped binary key cast to string before a partition-local sort, such as 
`SELECT CAST(k AS STRING) AS s FROM t GROUP BY k SORT BY s`, aggregate output 
can arrive as slices of one large array. `slice_with_length` narrows the view 
but retains the original allocation and its capacity. The previous cast copied 
each slice into a compact buffer. The new output makes DataFusion’s sorter 
charge the large backing allocation for every incoming batch, causing avoidable 
spilling. With 262,144 valid 64-byte keys, 8,192-row batches and a 64 MiB pool, 
the base produced zero spills while this head produced 16 and spilled every 
output row. Each 512 KiB slice retained a 16 MiB value allocation. Could the 
fast path compact slices when retained allocation capacity is 
disproportionately larger than their visible payload, while retaining zero-copy 
conversion for appropriately sized arrays?
   
   Evidence: Ran `LD_LIBRARY_PATH=/usr/lib/jvm/java-21-openjdk-amd64/lib/server 
/tmp/pr7106-4a62-this-review-actual-cast-memory`. Source: 
`/tmp/pr7106-4a62-this-review-actual-cast-memory.rs`. The bounded DataFusion 
55.2.0 probe executes AggregateExec → ProjectionExec → SortExec over 262,144 
distinct valid 64-byte binary keys. The projection wraps the exact-head 
compiled Comet `spark_cast`; the baseline helper was verified byte-for-byte 
against base 39380cca813bc0b861353de2cb53af960dd2a87f. Both returned identical 
262,144-row sorted results. Base value capacity per batch was 524,288 bytes 
with spill_count=0. Head capacity was 16,777,216 bytes with spill_count=16 and 
all rows spilled. Arrow 59.3.0 `Buffer::slice_with_length` clones the 
underlying allocation, and DataFusion’s sorter reserves buffer capacity 
separately for each batch. A disposable variant gating reuse on retained 
capacity reproduced zero spills with identical output. This measures allocation 
accounting and actual s
 pill behavior, not whole-query performance.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to