Skip to content

Commit 3ebef02

Browse files
committed
perf: avoid per-row copy in Spark hex byte encoding
1 parent 3e058f0 commit 3ebef02

1 file changed

Lines changed: 32 additions & 12 deletions

File tree

  • datafusion/spark/src/function/math

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

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

21-
use arrow::array::{Array, ArrayRef, StringBuilder};
21+
use arrow::array::{Array, ArrayRef, NullBufferBuilder, StringArray, StringBuilder};
22+
use arrow::buffer::{Buffer, OffsetBuffer};
2223
use arrow::datatypes::DataType;
2324
use arrow::{
2425
array::{as_dictionary_array, as_largestring_array, as_string_array},
@@ -166,40 +167,59 @@ where
166167
I: Iterator<Item = Option<T>>,
167168
T: AsRef<[u8]> + 'a,
168169
{
169-
let mut builder = StringBuilder::with_capacity(len, len * 64);
170-
let mut buffer = Vec::with_capacity(64);
171170
let lookup = if lowercase {
172171
&HEX_LOOKUP_LOWER
173172
} else {
174173
&HEX_LOOKUP_UPPER
175174
};
176175

176+
// Write hex digits directly into one growing value buffer, tracking offsets
177+
// ourselves. Each input byte becomes exactly two output bytes, so there is
178+
// no per-row `String`/`StringBuilder` copy — the hex digits are written once
179+
// into the final buffer.
180+
let mut values: Vec<u8> = Vec::with_capacity(len * 64);
181+
let mut offsets: Vec<i32> = Vec::with_capacity(len + 1);
182+
offsets.push(0);
183+
let mut nulls = NullBufferBuilder::new(len);
184+
177185
for v in iter {
178186
if let Some(b) = v {
179187
let bytes = b.as_ref();
180-
buffer.clear();
181188
let additional = bytes
182189
.len()
183190
.checked_mul(2)
184191
.ok_or_else(|| exec_datafusion_err!("hex output size overflow"))?;
185-
buffer.try_reserve(additional).map_err(|e| {
192+
values.try_reserve(additional).map_err(|e| {
186193
exec_datafusion_err!(
187194
"failed to reserve {additional} bytes for hex output: {e}"
188195
)
189196
})?;
190197
for &byte in bytes {
191-
buffer.extend_from_slice(&lookup[byte as usize]);
192-
}
193-
// SAFETY: buffer contains only ASCII hex digits, which are valid UTF-8.
194-
unsafe {
195-
builder.append_value(from_utf8_unchecked(&buffer));
198+
values.extend_from_slice(&lookup[byte as usize]);
196199
}
200+
nulls.append_non_null();
197201
} else {
198-
builder.append_null();
202+
nulls.append_null();
199203
}
204+
offsets.push(
205+
i32::try_from(values.len()).map_err(|_| {
206+
exec_datafusion_err!("hex output exceeds i32 offset range")
207+
})?,
208+
);
200209
}
201210

202-
Ok(Arc::new(builder.finish()))
211+
// SAFETY: the value buffer contains only ASCII hex digits (valid UTF-8) and
212+
// the offsets are monotonically increasing and end at `values.len()`, so the
213+
// array invariants hold. This mirrors the previous `from_utf8_unchecked`
214+
// path and avoids a redundant UTF-8 validation pass over the whole buffer.
215+
let array = unsafe {
216+
StringArray::new_unchecked(
217+
OffsetBuffer::new(offsets.into()),
218+
Buffer::from_vec(values),
219+
nulls.finish(),
220+
)
221+
};
222+
Ok(Arc::new(array))
203223
}
204224

205225
/// Generic hex encoding for int64 type

0 commit comments

Comments
 (0)