Skip to content

Commit 9918f2f

Browse files
committed
perf(sparse): build the fixed-size-list fill tile once, not once per gap
`append_fill` handed the fill scalar to `Constant::append_to_builder` per gap, which materialized the scalar's elements into an array every time. Hoist that out of the loop the way the list path already does: the fill's elements become an array once, up front, and every gap tiles that same array through `FixedSizeListBuilder::append_array_as_repeated_list`. Elements that are all the same scalar stay a constant array, so tiling them over a gap costs nothing at all however many rows it covers. Anything else is copied in per row, because a fixed-size list holds its elements back to back and cannot point its rows at one shared copy. `canonicalize_sparse_fixed_size_list` medians, against develop: (512, 7, 4) 21.1 us -> 25.6 us (was 55.0 us) (1024, 17, 8) 29.1 us -> 32.9 us (was 63.9 us) (8192, 1024, 4) 150.1 us -> 140.1 us (was 142.3 us) What is left of the gap against develop is the per-row copy: each row pays its own append for `list_size` elements, where tiling a run could double the region already written instead. Signed-off-by: Robert Kruszewski <robert@spiraldb.com>
1 parent 6c44824 commit 9918f2f

2 files changed

Lines changed: 115 additions & 52 deletions

File tree

‎encodings/sparse/src/canonical.rs‎

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,12 @@ fn execute_sparse_fixed_size_list_inner<I: IntegerPType>(
368368
nullability,
369369
array_len,
370370
);
371+
// The fill's elements become an array once, up front, so that a gap does not rebuild them.
372+
// They are tiled per row rather than shared - a fixed-size list holds its elements back to
373+
// back - unless they are all the same scalar, in which case the tile stays constant-encoded
374+
// and the tiling costs nothing.
375+
let fill_elements = fixed_size_list_fill_tile(fill_value.as_list(), list_size);
376+
371377
// One mask for the whole patch array rather than a validity lookup per patch.
372378
let patch_validity = values
373379
.validity()
@@ -384,7 +390,12 @@ fn execute_sparse_fixed_size_list_inner<I: IntegerPType>(
384390
let sparse_idx = sparse_idx
385391
.to_usize()
386392
.vortex_expect("patch index must fit in usize");
387-
append_fill(&mut builder, fill_value, sparse_idx - next_index, ctx);
393+
append_fixed_size_list_fill(
394+
&mut builder,
395+
fill_elements.as_ref(),
396+
sparse_idx - next_index,
397+
ctx,
398+
);
388399

389400
// Take each patch's elements rather than slicing the patch array itself: slicing a
390401
// `FixedSizeList` slices its elements and its validity, and every one of those slices pays
@@ -404,38 +415,56 @@ fn execute_sparse_fixed_size_list_inner<I: IntegerPType>(
404415
}
405416

406417
// Fill remaining positions after last patch.
407-
append_fill(&mut builder, fill_value, array_len - next_index, ctx);
418+
append_fixed_size_list_fill(
419+
&mut builder,
420+
fill_elements.as_ref(),
421+
array_len - next_index,
422+
ctx,
423+
);
408424

409425
builder.finish_into_fixed_size_list()
410426
}
411427

412-
/// Appends the run of `count` fill values that covers the gap before the next patch.
428+
/// Materializes the elements a fixed-size-list fill value covers each of its rows with, or `None`
429+
/// if the fill is null.
413430
///
414-
/// The run goes in as a single [`ConstantArray`] rather than one appended value per row. For a
415-
/// list dtype that is the difference between storing the fill value once per gap and once per row:
416-
/// canonicalizing a constant list array points every view at one copy of the value, and the
417-
/// builder keeps that layout.
418-
fn append_fill(
419-
builder: &mut dyn ArrayBuilder,
420-
fill_value: &Scalar,
431+
/// Elements that are all the same scalar stay a constant array, so tiling them over a gap costs
432+
/// nothing however many rows it covers.
433+
fn fixed_size_list_fill_tile(fill: ListScalar, list_size: u32) -> Option<ArrayRef> {
434+
let elements = fill.elements()?;
435+
436+
Some(match elements.iter().all_equal_value() {
437+
Ok(uniform) => ConstantArray::new(uniform.clone(), list_size as usize).into_array(),
438+
Err(_) => {
439+
let mut builder = builder_with_capacity(fill.element_dtype(), elements.len());
440+
for element in &elements {
441+
builder
442+
.append_scalar(element)
443+
.vortex_expect("fixed-size-list element scalar was invalid");
444+
}
445+
builder.finish()
446+
}
447+
})
448+
}
449+
450+
/// Appends the run of `count` fill lists that covers the gap before the next patch.
451+
fn append_fixed_size_list_fill(
452+
builder: &mut FixedSizeListBuilder,
453+
fill_elements: Option<&ArrayRef>,
421454
count: usize,
422455
ctx: &mut ExecutionCtx,
423456
) {
424457
if count == 0 {
425458
return;
426459
}
427460

428-
if fill_value.is_null() {
429-
// A null fill has no elements to share, and the builder can record the nulls without
430-
// going through an array at all.
431-
builder.append_nulls(count);
432-
return;
461+
match fill_elements {
462+
Some(fill_elements) => builder
463+
.append_array_as_repeated_list(fill_elements, count, ctx)
464+
.vortex_expect("Failed to append sparse fixed-size-list fill value"),
465+
// A null fill has no elements of its own, only the placeholders the builder writes.
466+
None => builder.append_nulls(count),
433467
}
434-
435-
ConstantArray::new(fill_value.clone(), count)
436-
.into_array()
437-
.append_to_builder(builder, ctx)
438-
.vortex_expect("Failed to append sparse fill value");
439468
}
440469

441470
fn execute_sparse_bools(

‎vortex-array/src/builders/fixed_size_list.rs‎

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,63 @@ impl FixedSizeListBuilder {
107107
Ok(())
108108
}
109109

110-
/// Appends the same fixed-size list `value` `n` times.
110+
/// Appends `array` as `n` identical non-null lists.
111111
///
112-
/// A fixed-size list array holds its elements back to back, so a run of `n` identical lists is
113-
/// the value's elements tiled `n` times - there is no layout that lets the rows share one copy
114-
/// the way a list view's can. Two cases avoid paying for the tiling anyway:
112+
/// A fixed-size list array holds its elements back to back, so `n` identical lists are the
113+
/// array's elements tiled `n` times - there is no layout that lets the rows share one copy the
114+
/// way a list view's can, so the tile is copied in per row. A constant tile is the exception:
115+
/// tiling it gives another constant array, so it goes in as a single chunk and no elements are
116+
/// materialized at all.
115117
///
116-
/// - a null value needs no elements at all, only placeholders, which
117-
/// [`append_nulls`](ArrayBuilder::append_nulls) writes in bulk;
118-
/// - a value whose elements are all the same scalar tiles to a constant array, which goes in as
119-
/// a single chunk.
118+
/// A caller with a run of appends to make should hand over the same `array` each time rather
119+
/// than rebuild it, which is the whole reason this takes an array instead of a scalar.
120+
pub fn append_array_as_repeated_list(
121+
&mut self,
122+
array: &ArrayRef,
123+
n: usize,
124+
ctx: &mut ExecutionCtx,
125+
) -> VortexResult<()> {
126+
vortex_ensure!(
127+
array.dtype() == self.element_dtype(),
128+
"Array dtype {:?} does not match list element dtype {:?}",
129+
array.dtype(),
130+
self.element_dtype()
131+
);
132+
vortex_ensure!(
133+
array.len() == self.list_size() as usize,
134+
"Array length {} does not match fixed list size {}",
135+
array.len(),
136+
self.list_size()
137+
);
138+
139+
if n == 0 {
140+
return Ok(());
141+
}
142+
143+
let tiled_len = n * array.len();
144+
match array.as_constant() {
145+
Some(element) => {
146+
let tiled = ConstantArray::new(element, tiled_len).into_array();
147+
self.elements_builder.append_array(&tiled, ctx)?;
148+
}
149+
None => {
150+
self.elements_builder.reserve_exact(tiled_len);
151+
for _ in 0..n {
152+
self.elements_builder.append_array_values(array, ctx)?;
153+
}
154+
}
155+
}
156+
self.nulls.append_n_non_nulls(n);
157+
158+
Ok(())
159+
}
160+
161+
/// Appends the same fixed-size list `value` `n` times.
120162
///
121-
/// Otherwise the tile is copied in per row. That is one copy of each element, where
122-
/// canonicalizing the run first would build the whole tiled array and then copy it again.
163+
/// This materializes the value's elements and hands them to
164+
/// [`append_array_as_repeated_list`](Self::append_array_as_repeated_list) - as a constant array
165+
/// when they are all the same scalar, so that the tiling costs nothing. A caller with a run of
166+
/// appends to make should materialize the elements once itself and call that directly.
123167
pub(crate) fn append_constant(
124168
&mut self,
125169
value: ListScalar,
@@ -139,36 +183,26 @@ impl FixedSizeListBuilder {
139183
return Ok(());
140184
};
141185

186+
let list_size = self.list_size() as usize;
142187
vortex_ensure!(
143-
elements.len() == self.list_size() as usize,
188+
elements.len() == list_size,
144189
"Scalar list length {} does not match fixed list size {}",
145190
elements.len(),
146-
self.list_size()
191+
list_size
147192
);
148193

149-
let tiled_len = n * elements.len();
150-
if let Ok(uniform) = elements.iter().all_equal_value() {
151-
let tile = ConstantArray::new(uniform.clone(), tiled_len).into_array();
152-
self.elements_builder.append_array(&tile, ctx)?;
153-
self.nulls.append_n_non_nulls(n);
154-
return Ok(());
155-
}
156-
157-
let tile = {
158-
let mut tile_builder = builder_with_capacity(self.element_dtype(), elements.len());
159-
for element in &elements {
160-
tile_builder.append_scalar(element)?;
194+
let tile = match elements.iter().all_equal_value() {
195+
Ok(uniform) => ConstantArray::new(uniform.clone(), list_size).into_array(),
196+
Err(_) => {
197+
let mut tile_builder = builder_with_capacity(self.element_dtype(), list_size);
198+
for element in &elements {
199+
tile_builder.append_scalar(element)?;
200+
}
201+
tile_builder.finish()
161202
}
162-
tile_builder.finish()
163203
};
164204

165-
self.elements_builder.reserve_exact(tiled_len);
166-
for _ in 0..n {
167-
self.elements_builder.append_array_values(&tile, ctx)?;
168-
}
169-
self.nulls.append_n_non_nulls(n);
170-
171-
Ok(())
205+
self.append_array_as_repeated_list(&tile, n, ctx)
172206
}
173207

174208
/// Appends the values of a canonical [`FixedSizeListArray`] to the builder, recursing into the

0 commit comments

Comments
 (0)