Skip to content

Commit af3fafa

Browse files
Optimize Spark hex null handling
Reuse the input null buffer and split nullable and no-null encoding paths to avoid rebuilding validity and per-value Option checks. Add pointer-reuse and no-null regressions plus dedicated Criterion cases.
1 parent 68d5874 commit af3fafa

2 files changed

Lines changed: 138 additions & 66 deletions

File tree

datafusion/spark/benches/hex.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,21 @@ fn criterion_benchmark(c: &mut Criterion) {
135135
run_benchmark(c, "hex_utf8", size, Arc::new(data));
136136
}
137137

138+
for &size in &sizes {
139+
let data = generate_utf8_data(size, 0.0);
140+
run_benchmark(c, "hex_utf8_no_nulls", size, Arc::new(data));
141+
}
142+
138143
for &size in &sizes {
139144
let data = generate_binary_data(size, null_density);
140145
run_benchmark(c, "hex_binary", size, Arc::new(data));
141146
}
142147

148+
for &size in &sizes {
149+
let data = generate_binary_data(size, 0.0);
150+
run_benchmark(c, "hex_binary_no_nulls", size, Arc::new(data));
151+
}
152+
143153
for &size in &sizes {
144154
let data = generate_int64_dict_data(size, null_density);
145155
run_benchmark(c, "hex_int64_dict", size, Arc::new(data));

datafusion/spark/src/function/math/hex.rs

Lines changed: 128 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use std::str::from_utf8_unchecked;
1919
use std::sync::Arc;
2020

21-
use arrow::array::{Array, ArrayRef, NullBufferBuilder, StringArray, StringBuilder};
21+
use arrow::array::{Array, ArrayAccessor, ArrayRef, StringArray, StringBuilder};
2222
use arrow::buffer::{Buffer, OffsetBuffer};
2323
use arrow::datatypes::DataType;
2424
use arrow::{
@@ -111,21 +111,40 @@ impl ScalarUDFImpl for SparkHex {
111111
}
112112
}
113113

114+
#[inline]
115+
fn append_hex_bytes(
116+
values: &mut Vec<u8>,
117+
bytes: &[u8],
118+
case: HexCase,
119+
) -> Result<i32, DataFusionError> {
120+
let additional = bytes
121+
.len()
122+
.checked_mul(2)
123+
.ok_or_else(|| exec_datafusion_err!("hex output size overflow"))?;
124+
values.try_reserve(additional).map_err(|e| {
125+
exec_datafusion_err!("failed to reserve {additional} bytes for hex output: {e}")
126+
})?;
127+
encode_bytes_into(bytes, case, values);
128+
i32::try_from(values.len())
129+
.map_err(|_| exec_datafusion_err!("hex output exceeds i32 offset range"))
130+
}
131+
114132
/// Generic hex encoding for byte array types
115-
fn hex_encode_bytes<'a, I, T>(
116-
iter: I,
133+
fn hex_encode_bytes<'a, A, T>(
134+
array: &A,
117135
lowercase: bool,
118-
len: usize,
119136
) -> Result<ArrayRef, DataFusionError>
120137
where
121-
I: Iterator<Item = Option<T>>,
122-
T: AsRef<[u8]> + 'a,
138+
A: ArrayAccessor<Item = &'a T>,
139+
T: AsRef<[u8]> + ?Sized + 'a,
123140
{
124141
let case = if lowercase {
125142
HexCase::Lower
126143
} else {
127144
HexCase::Upper
128145
};
146+
let len = array.len();
147+
let nulls = array.nulls().cloned();
129148

130149
// Write hex digits directly into one growing value buffer, tracking offsets
131150
// ourselves. Each input byte becomes exactly two output bytes, so there is
@@ -134,30 +153,25 @@ where
134153
let mut values: Vec<u8> = Vec::with_capacity(len * 64);
135154
let mut offsets: Vec<i32> = Vec::with_capacity(len + 1);
136155
offsets.push(0);
137-
let mut nulls = NullBufferBuilder::new(len);
138156

139-
for v in iter {
140-
if let Some(b) = v {
141-
let bytes = b.as_ref();
142-
let additional = bytes
143-
.len()
144-
.checked_mul(2)
145-
.ok_or_else(|| exec_datafusion_err!("hex output size overflow"))?;
146-
values.try_reserve(additional).map_err(|e| {
147-
exec_datafusion_err!(
148-
"failed to reserve {additional} bytes for hex output: {e}"
149-
)
150-
})?;
151-
encode_bytes_into(bytes, case, &mut values);
152-
nulls.append_non_null();
153-
} else {
154-
nulls.append_null();
157+
if let Some(ref nulls) = nulls {
158+
for i in 0..len {
159+
if nulls.is_valid(i) {
160+
// SAFETY: `i` is in bounds and the validity buffer marks it valid.
161+
let bytes = unsafe { array.value_unchecked(i) }.as_ref();
162+
offsets.push(append_hex_bytes(&mut values, bytes, case)?);
163+
} else {
164+
offsets.push(i32::try_from(values.len()).map_err(|_| {
165+
exec_datafusion_err!("hex output exceeds i32 offset range")
166+
})?);
167+
}
168+
}
169+
} else {
170+
for i in 0..len {
171+
// SAFETY: `i` is in bounds and no null buffer means every value is valid.
172+
let bytes = unsafe { array.value_unchecked(i) }.as_ref();
173+
offsets.push(append_hex_bytes(&mut values, bytes, case)?);
155174
}
156-
offsets.push(
157-
i32::try_from(values.len()).map_err(|_| {
158-
exec_datafusion_err!("hex output exceeds i32 offset range")
159-
})?,
160-
);
161175
}
162176

163177
// SAFETY: the value buffer contains only ASCII hex digits (valid UTF-8) and
@@ -168,7 +182,7 @@ where
168182
StringArray::new_unchecked(
169183
OffsetBuffer::new(offsets.into()),
170184
Buffer::from_vec(values),
171-
nulls.finish(),
185+
nulls,
172186
)
173187
};
174188
Ok(Arc::new(array))
@@ -227,51 +241,27 @@ pub fn compute_hex(
227241
}
228242
DataType::Utf8 => {
229243
let array = as_string_array(array);
230-
Ok(ColumnarValue::Array(hex_encode_bytes(
231-
array.iter(),
232-
lowercase,
233-
array.len(),
234-
)?))
244+
Ok(ColumnarValue::Array(hex_encode_bytes(&array, lowercase)?))
235245
}
236246
DataType::Utf8View => {
237247
let array = as_string_view_array(array)?;
238-
Ok(ColumnarValue::Array(hex_encode_bytes(
239-
array.iter(),
240-
lowercase,
241-
array.len(),
242-
)?))
248+
Ok(ColumnarValue::Array(hex_encode_bytes(&array, lowercase)?))
243249
}
244250
DataType::LargeUtf8 => {
245251
let array = as_largestring_array(array);
246-
Ok(ColumnarValue::Array(hex_encode_bytes(
247-
array.iter(),
248-
lowercase,
249-
array.len(),
250-
)?))
252+
Ok(ColumnarValue::Array(hex_encode_bytes(&array, lowercase)?))
251253
}
252254
DataType::Binary => {
253255
let array = as_binary_array(array)?;
254-
Ok(ColumnarValue::Array(hex_encode_bytes(
255-
array.iter(),
256-
lowercase,
257-
array.len(),
258-
)?))
256+
Ok(ColumnarValue::Array(hex_encode_bytes(&array, lowercase)?))
259257
}
260258
DataType::LargeBinary => {
261259
let array = as_large_binary_array(array)?;
262-
Ok(ColumnarValue::Array(hex_encode_bytes(
263-
array.iter(),
264-
lowercase,
265-
array.len(),
266-
)?))
260+
Ok(ColumnarValue::Array(hex_encode_bytes(&array, lowercase)?))
267261
}
268262
DataType::FixedSizeBinary(_) => {
269263
let array = as_fixed_size_binary_array(array)?;
270-
Ok(ColumnarValue::Array(hex_encode_bytes(
271-
array.iter(),
272-
lowercase,
273-
array.len(),
274-
)?))
264+
Ok(ColumnarValue::Array(hex_encode_bytes(&array, lowercase)?))
275265
}
276266
DataType::Dictionary(key_type, _) => {
277267
if **key_type != DataType::Int32 {
@@ -291,27 +281,27 @@ pub fn compute_hex(
291281
}
292282
DataType::Utf8 => {
293283
let arr = as_string_array(dict_values);
294-
hex_encode_bytes(arr.iter(), lowercase, arr.len())?
284+
hex_encode_bytes(&arr, lowercase)?
295285
}
296286
DataType::LargeUtf8 => {
297287
let arr = as_largestring_array(dict_values);
298-
hex_encode_bytes(arr.iter(), lowercase, arr.len())?
288+
hex_encode_bytes(&arr, lowercase)?
299289
}
300290
DataType::Utf8View => {
301291
let arr = as_string_view_array(dict_values)?;
302-
hex_encode_bytes(arr.iter(), lowercase, arr.len())?
292+
hex_encode_bytes(&arr, lowercase)?
303293
}
304294
DataType::Binary => {
305295
let arr = as_binary_array(dict_values)?;
306-
hex_encode_bytes(arr.iter(), lowercase, arr.len())?
296+
hex_encode_bytes(&arr, lowercase)?
307297
}
308298
DataType::LargeBinary => {
309299
let arr = as_large_binary_array(dict_values)?;
310-
hex_encode_bytes(arr.iter(), lowercase, arr.len())?
300+
hex_encode_bytes(&arr, lowercase)?
311301
}
312302
DataType::FixedSizeBinary(_) => {
313303
let arr = as_fixed_size_binary_array(dict_values)?;
314-
hex_encode_bytes(arr.iter(), lowercase, arr.len())?
304+
hex_encode_bytes(&arr, lowercase)?
315305
}
316306
_ => {
317307
return exec_err!(
@@ -465,7 +455,8 @@ mod test {
465455
// is reachable only via `spark_sha2_hex`, which has no in-workspace
466456
// caller, so it otherwise has no coverage. Drive it directly here.
467457
let input = StringArray::from(vec![Some("hi"), Some("bye"), None, Some("rust")]);
468-
let result = super::hex_encode_bytes(input.iter(), true, input.len()).unwrap();
458+
let input_ref = &input;
459+
let result = super::hex_encode_bytes(&input_ref, true).unwrap();
469460
let result = as_string_array(&result);
470461

471462
let expected =
@@ -495,6 +486,56 @@ mod test {
495486
assert_eq!(strings.value(0), expected);
496487
}
497488

489+
#[test]
490+
fn test_spark_hex_binary_no_nulls() {
491+
let input = BinaryArray::from(vec![
492+
b"".as_slice(),
493+
b"\x00\x7f\x80\xff".as_slice(),
494+
b"DataFusion".as_slice(),
495+
]);
496+
497+
let result = super::spark_hex(&[ColumnarValue::Array(Arc::new(input))]).unwrap();
498+
let array = match result {
499+
ColumnarValue::Array(array) => array,
500+
_ => panic!("Expected array"),
501+
};
502+
let strings = as_string_array(&array);
503+
504+
assert_eq!(strings.nulls(), None);
505+
assert_eq!(
506+
strings,
507+
&StringArray::from(vec!["", "007F80FF", "44617461467573696F6E"])
508+
);
509+
}
510+
511+
#[test]
512+
fn test_spark_hex_binary_reuses_input_nulls() {
513+
let input = BinaryArray::from(vec![
514+
Some(b"skip".as_slice()),
515+
None,
516+
Some(b"\x00\xff".as_slice()),
517+
Some(b"hex".as_slice()),
518+
None,
519+
])
520+
.slice(1, 4);
521+
let input_nulls = input.nulls().unwrap().clone();
522+
523+
let result = super::spark_hex(&[ColumnarValue::Array(Arc::new(input))]).unwrap();
524+
let array = match result {
525+
ColumnarValue::Array(array) => array,
526+
_ => panic!("Expected array"),
527+
};
528+
let strings = as_string_array(&array);
529+
let output_nulls = strings.nulls().unwrap();
530+
531+
assert_eq!(output_nulls, &input_nulls);
532+
assert!(output_nulls.inner().ptr_eq(input_nulls.inner()));
533+
assert_eq!(
534+
strings,
535+
&StringArray::from(vec![None, Some("00FF"), Some("686578"), None])
536+
);
537+
}
538+
498539
#[test]
499540
fn test_spark_hex_int64() {
500541
let int_array = Int64Array::from(vec![Some(1), Some(2), None, Some(3)]);
@@ -540,4 +581,25 @@ mod test {
540581

541582
assert_eq!(&expected, result);
542583
}
584+
585+
#[test]
586+
fn test_dict_binary_values_null() {
587+
let keys = Int32Array::from(vec![Some(0), None, Some(1)]);
588+
let vals = BinaryArray::from(vec![Some(b"hi".as_slice()), None]);
589+
// [b"hi", null, null]
590+
let dict = DictionaryArray::new(keys, Arc::new(vals));
591+
592+
let result = super::spark_hex(&[ColumnarValue::Array(Arc::new(dict))]).unwrap();
593+
let result = match result {
594+
ColumnarValue::Array(array) => array,
595+
_ => panic!("Expected array"),
596+
};
597+
let result = as_dictionary_array(&result).unwrap();
598+
599+
let keys = Int32Array::from(vec![Some(0), None, Some(1)]);
600+
let vals = StringArray::from(vec![Some("6869"), None]);
601+
let expected = DictionaryArray::new(keys, Arc::new(vals));
602+
603+
assert_eq!(&expected, result);
604+
}
543605
}

0 commit comments

Comments
 (0)