Skip to content

Commit 04c49fb

Browse files
andygroveclaude
andcommitted
perf: pre-downcast struct fields for native C2R
Pre-downcast all struct field columns into TypedElements at batch initialization time (in TypedArray::from_array). This eliminates per-row type dispatch overhead for struct fields. Performance improvement for struct types: - Before: 272ms (0.8X of Spark) - After: 220ms (1.0X of Spark, matching Spark performance) The pre-downcast pattern is now consistently applied to: - Top-level columns (TypedArray) - Array/List elements (TypedElements) - Map keys/values (TypedElements) - Struct fields (TypedElements) - NEW Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 64c5212 commit 04c49fb

1 file changed

Lines changed: 71 additions & 8 deletions

File tree

native/core/src/execution/columnar_to_row.rs

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ enum TypedArray<'a> {
6565
LargeString(&'a LargeStringArray),
6666
Binary(&'a BinaryArray),
6767
LargeBinary(&'a LargeBinaryArray),
68-
Struct(&'a StructArray, arrow::datatypes::Fields),
68+
Struct(&'a StructArray, arrow::datatypes::Fields, Vec<TypedElements<'a>>),
6969
List(&'a ListArray, arrow::datatypes::FieldRef),
7070
LargeList(&'a LargeListArray, arrow::datatypes::FieldRef),
7171
Map(&'a MapArray, arrow::datatypes::FieldRef),
@@ -162,12 +162,20 @@ impl<'a> TypedArray<'a> {
162162
CometError::Internal("Failed to downcast to LargeBinaryArray".to_string())
163163
})?,
164164
)),
165-
DataType::Struct(fields) => Ok(TypedArray::Struct(
166-
array.as_any().downcast_ref::<StructArray>().ok_or_else(|| {
165+
DataType::Struct(fields) => {
166+
let struct_arr = array.as_any().downcast_ref::<StructArray>().ok_or_else(|| {
167167
CometError::Internal("Failed to downcast to StructArray".to_string())
168-
})?,
169-
fields.clone(),
170-
)),
168+
})?;
169+
// Pre-downcast all struct fields once
170+
let typed_fields: Vec<TypedElements> = fields
171+
.iter()
172+
.enumerate()
173+
.map(|(idx, field)| {
174+
TypedElements::from_array(struct_arr.column(idx), field.data_type())
175+
})
176+
.collect();
177+
Ok(TypedArray::Struct(struct_arr, fields.clone(), typed_fields))
178+
}
171179
DataType::List(field) => Ok(TypedArray::List(
172180
array.as_any().downcast_ref::<ListArray>().ok_or_else(|| {
173181
CometError::Internal("Failed to downcast to ListArray".to_string())
@@ -217,7 +225,7 @@ impl<'a> TypedArray<'a> {
217225
TypedArray::LargeString(arr) => arr.is_null(row_idx),
218226
TypedArray::Binary(arr) => arr.is_null(row_idx),
219227
TypedArray::LargeBinary(arr) => arr.is_null(row_idx),
220-
TypedArray::Struct(arr, _) => arr.is_null(row_idx),
228+
TypedArray::Struct(arr, _, _) => arr.is_null(row_idx),
221229
TypedArray::List(arr, _) => arr.is_null(row_idx),
222230
TypedArray::LargeList(arr, _) => arr.is_null(row_idx),
223231
TypedArray::Map(arr, _) => arr.is_null(row_idx),
@@ -317,7 +325,9 @@ impl<'a> TypedArray<'a> {
317325
buffer.extend(std::iter::repeat_n(0u8, padding));
318326
Ok(len)
319327
}
320-
TypedArray::Struct(arr, fields) => write_struct_to_buffer(buffer, arr, row_idx, fields),
328+
TypedArray::Struct(arr, fields, typed_fields) => {
329+
write_struct_to_buffer_typed(buffer, arr, row_idx, fields, typed_fields)
330+
}
321331
TypedArray::List(arr, field) => write_list_to_buffer(buffer, arr, row_idx, field),
322332
TypedArray::LargeList(arr, field) => {
323333
write_large_list_to_buffer(buffer, arr, row_idx, field)
@@ -1840,10 +1850,63 @@ fn write_array_element(buffer: &mut [u8], data_type: &DataType, value: i64, offs
18401850
// These write directly to the output buffer to avoid intermediate allocations.
18411851
// =============================================================================
18421852

1853+
/// Writes a struct value directly to the buffer using pre-downcast typed fields.
1854+
/// Returns the unpadded length written.
1855+
///
1856+
/// This version uses pre-downcast TypedElements for each field, eliminating
1857+
/// per-row type dispatch overhead.
1858+
#[inline]
1859+
fn write_struct_to_buffer_typed(
1860+
buffer: &mut Vec<u8>,
1861+
_struct_array: &StructArray,
1862+
row_idx: usize,
1863+
_fields: &arrow::datatypes::Fields,
1864+
typed_fields: &[TypedElements],
1865+
) -> CometResult<usize> {
1866+
let num_fields = typed_fields.len();
1867+
let nested_bitset_width = ColumnarToRowContext::calculate_bitset_width(num_fields);
1868+
let nested_fixed_size = nested_bitset_width + num_fields * 8;
1869+
1870+
// Remember where this struct starts in the buffer
1871+
let struct_start = buffer.len();
1872+
1873+
// Reserve space for fixed-width portion (zeros for null bits and field slots)
1874+
buffer.resize(struct_start + nested_fixed_size, 0);
1875+
1876+
// Write each field using pre-downcast types
1877+
for (field_idx, typed_field) in typed_fields.iter().enumerate() {
1878+
if typed_field.is_null_at(row_idx) {
1879+
// Set null bit in nested struct
1880+
set_null_bit(buffer, struct_start, field_idx);
1881+
} else {
1882+
let field_offset = struct_start + nested_bitset_width + field_idx * 8;
1883+
1884+
if typed_field.is_fixed_width() {
1885+
// Fixed-width field - use pre-downcast accessor
1886+
let value = typed_field.get_fixed_value(row_idx);
1887+
buffer[field_offset..field_offset + 8].copy_from_slice(&value.to_le_bytes());
1888+
} else {
1889+
// Variable-length field - use pre-downcast writer
1890+
let var_len = typed_field.write_variable_value(buffer, row_idx, struct_start)?;
1891+
if var_len > 0 {
1892+
let padded_len = round_up_to_8(var_len);
1893+
let data_offset = buffer.len() - padded_len - struct_start;
1894+
let offset_and_len = ((data_offset as i64) << 32) | (var_len as i64);
1895+
buffer[field_offset..field_offset + 8]
1896+
.copy_from_slice(&offset_and_len.to_le_bytes());
1897+
}
1898+
}
1899+
}
1900+
}
1901+
1902+
Ok(buffer.len() - struct_start)
1903+
}
1904+
18431905
/// Writes a struct value directly to the buffer.
18441906
/// Returns the unpadded length written.
18451907
///
18461908
/// Processes each field using inline type dispatch to avoid allocation overhead.
1909+
/// This is used for nested structs where we don't have pre-downcast fields.
18471910
#[inline]
18481911
fn write_struct_to_buffer(
18491912
buffer: &mut Vec<u8>,

0 commit comments

Comments
 (0)