Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 157 additions & 75 deletions encodings/sparse/src/canonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use vortex_array::arrays::StructArray;
use vortex_array::arrays::VarBinView;
use vortex_array::arrays::VarBinViewArray;
use vortex_array::arrays::fixed_size_list::FixedSizeListArrayExt;
use vortex_array::arrays::listview::ListViewArrayExt;
use vortex_array::arrays::listview::ListViewArraySlotsExt;
use vortex_array::arrays::listview::ListViewRebuildMode;
use vortex_array::arrays::primitive::PrimitiveArrayExt;
Expand All @@ -31,6 +32,7 @@ use vortex_array::builders::ArrayBuilder;
use vortex_array::builders::DecimalBuilder;
use vortex_array::builders::FixedSizeListBuilder;
use vortex_array::builders::ListViewBuilder;
use vortex_array::builders::builder_with_capacity;
use vortex_array::dtype::DType;
use vortex_array::dtype::DecimalDType;
use vortex_array::dtype::DecimalType;
Expand All @@ -47,6 +49,7 @@ use vortex_array::match_each_unsigned_integer_ptype;
use vortex_array::match_smallest_list_offset_type;
use vortex_array::patches::Patches;
use vortex_array::scalar::DecimalScalar;
use vortex_array::scalar::ListScalar;
use vortex_array::scalar::Scalar;
use vortex_array::scalar::StructScalar;
use vortex_array::validity::Validity;
Expand Down Expand Up @@ -232,51 +235,89 @@ fn execute_sparse_lists_inner<I: IntegerPType, O: OffsetBuilderPType>(
total_canonical_values,
len,
);
let patch_values = patch_values.into_array();
// The fill's elements become an array once, up front. Every gap then appends that same array,
// so the fill's elements are stored once for the whole result however many gaps reference them.
let fill_elements = list_scalar_elements_array(fill_value.as_list());

// One mask for the whole patch array rather than a validity lookup per patch.
let patch_validity = patch_values
.listview_validity()
.execute_mask(patch_values.len(), ctx)
.vortex_expect("sparse list validity mask failed to execute");

let mut next_index = 0;
let mut patch_idx = 0;

while patch_idx < patch_indices.len() {
let sparse_idx = sparse_index_at(patch_indices, patch_idx);
append_fill(&mut builder, fill_value, sparse_idx - next_index, ctx);

// Patches landing on consecutive rows go in as one slice of the patch array rather than a
// list at a time, so the builder sees a run per gap instead of a run per patch.
let run_end = consecutive_run_end(patch_indices, patch_idx);
patch_values
.slice(patch_idx..run_end)
.vortex_expect("patch run is in bounds")
.append_to_builder(&mut builder, ctx)
.vortex_expect("Failed to append sparse values");

next_index = sparse_index_at(patch_indices, run_end - 1) + 1;
patch_idx = run_end;

for ((patch_idx, sparse_idx), patch_valid) in
patch_indices.iter().enumerate().zip(patch_validity.iter())
{
let sparse_idx = sparse_idx
.to_usize()
.vortex_expect("patch index must fit in usize");

append_list_fill(
&mut builder,
fill_elements.as_ref(),
sparse_idx - next_index,
ctx,
);

// Take each patch's elements rather than slicing the patch array itself: slicing a
// `ListView` slices its offsets, its sizes and its elements, and every one of those slices
// pays an optimizer pass, where this pays one for the elements alone.
if patch_valid {
let patch_list = patch_values
.list_elements_at(patch_idx)
.vortex_expect("list_elements_at");
builder
.append_array_as_list(&patch_list, ctx)
.vortex_expect("Failed to append sparse value");
} else {
builder.append_null();
}

next_index = sparse_idx + 1;
}

append_fill(&mut builder, fill_value, len - next_index, ctx);
append_list_fill(&mut builder, fill_elements.as_ref(), len - next_index, ctx);

builder.finish()
}

/// The sparse row that the patch at `patch_idx` occupies.
fn sparse_index_at<I: IntegerPType>(patch_indices: &[I], patch_idx: usize) -> usize {
patch_indices[patch_idx]
.to_usize()
.vortex_expect("patch index must fit in usize")
/// Materializes a list scalar's elements into an array, or `None` if the scalar is null.
fn list_scalar_elements_array(list: ListScalar) -> Option<ArrayRef> {
list.elements().map(|elements| {
let mut builder = builder_with_capacity(list.element_dtype(), elements.len());
for element in elements {
builder
.append_scalar(&element)
.vortex_expect("list element scalar was invalid");
}
builder.finish()
})
}

/// The end of the run of patches starting at `start` that occupy consecutive sparse rows.
/// Appends the run of `count` fill lists that covers the gap before the next patch.
///
/// Patch indices are strictly increasing, so a run is any stretch over which they step by one.
fn consecutive_run_end<I: IntegerPType>(patch_indices: &[I], start: usize) -> usize {
let mut end = start + 1;
while end < patch_indices.len()
&& sparse_index_at(patch_indices, end) == sparse_index_at(patch_indices, end - 1) + 1
{
end += 1;
/// The whole run goes in as one append that points `count` views at a single copy of
/// `fill_elements`, so a gap costs nothing per row it covers.
fn append_list_fill<O: OffsetBuilderPType, S: OffsetBuilderPType>(
builder: &mut ListViewBuilder<O, S>,
fill_elements: Option<&ArrayRef>,
count: usize,
ctx: &mut ExecutionCtx,
) {
if count == 0 {
return;
}

match fill_elements {
Some(fill_elements) => builder
.append_array_as_repeated_list(fill_elements, count, ctx)
.vortex_expect("Failed to append sparse fill value"),
// A null fill has no elements to share, and the builder can record the nulls without
// going through an array at all.
None => builder.append_nulls(count),
}
end
}

/// Canonicalize a sparse [`FixedSizeListArray`] by expanding it into a dense representation.
Expand Down Expand Up @@ -328,63 +369,103 @@ fn execute_sparse_fixed_size_list_inner<I: IntegerPType>(
nullability,
array_len,
);
let values = values.into_array();
// The fill's elements become an array once, up front, so that a gap does not rebuild them.
// They are tiled per row rather than shared - a fixed-size list holds its elements back to
// back - unless they are all the same scalar, in which case the tile stays constant-encoded
// and the tiling costs nothing.
let fill_elements = fixed_size_list_fill_tile(fill_value.as_list(), list_size);

// One mask for the whole patch array rather than a validity lookup per patch.
let patch_validity = values
.validity()
.vortex_expect("sparse fixed-size-list validity should be derivable")
.execute_mask(values.len(), ctx)
.vortex_expect("sparse fixed-size-list validity mask failed to execute");

let mut next_index = 0;
let mut patch_idx = 0;

while patch_idx < patch_indices.len() {
for ((patch_idx, sparse_idx), patch_valid) in
patch_indices.iter().enumerate().zip(patch_validity.iter())
{
// Fill gap before this patch with fill values.
let sparse_idx = sparse_index_at(patch_indices, patch_idx);
append_fill(&mut builder, fill_value, sparse_idx - next_index, ctx);

// Patches landing on consecutive rows go in as one slice of the patch array, whose
// elements and validity are both appended in bulk. A null patch carries its own
// placeholder elements along, which is what the builder would have appended for it anyway.
let run_end = consecutive_run_end(patch_indices, patch_idx);
values
.slice(patch_idx..run_end)
.vortex_expect("patch run is in bounds")
.append_to_builder(&mut builder, ctx)
.vortex_expect("Failed to append sparse fixed-size-list values");

next_index = sparse_index_at(patch_indices, run_end - 1) + 1;
patch_idx = run_end;
let sparse_idx = sparse_idx
.to_usize()
.vortex_expect("patch index must fit in usize");
append_fixed_size_list_fill(
&mut builder,
fill_elements.as_ref(),
sparse_idx - next_index,
ctx,
);

// Take each patch's elements rather than slicing the patch array itself: slicing a
// `FixedSizeList` slices its elements and its validity, and every one of those slices pays
// an optimizer pass, where this pays one for the elements alone.
if patch_valid {
let patch_list = values
.fixed_size_list_elements_at(patch_idx)
.vortex_expect("fixed_size_list_elements_at");
builder
.append_array_as_list(&patch_list, ctx)
.vortex_expect("Failed to append sparse fixed-size-list value");
} else {
builder.append_null();
}

next_index = sparse_idx + 1;
}

// Fill remaining positions after last patch.
append_fill(&mut builder, fill_value, array_len - next_index, ctx);
append_fixed_size_list_fill(
&mut builder,
fill_elements.as_ref(),
array_len - next_index,
ctx,
);

builder.finish_into_fixed_size_list()
}

/// Appends the run of `count` fill values that covers the gap before the next patch.
/// Materializes the elements a fixed-size-list fill value covers each of its rows with, or `None`
/// if the fill is null.
///
/// The run goes in as a single [`ConstantArray`] rather than one appended value per row. For a
/// list dtype that is the difference between storing the fill value once per gap and once per row:
/// canonicalizing a constant list array points every view at one copy of the value, and the
/// builder keeps that layout.
fn append_fill(
builder: &mut dyn ArrayBuilder,
fill_value: &Scalar,
/// Elements that are all the same scalar stay a constant array, so tiling them over a gap costs
/// nothing however many rows it covers.
fn fixed_size_list_fill_tile(fill: ListScalar, list_size: u32) -> Option<ArrayRef> {
let elements = fill.elements()?;

Some(match elements.iter().all_equal_value() {
Ok(uniform) => ConstantArray::new(uniform.clone(), list_size as usize).into_array(),
Err(_) => {
let mut builder = builder_with_capacity(fill.element_dtype(), elements.len());
for element in &elements {
builder
.append_scalar(element)
.vortex_expect("fixed-size-list element scalar was invalid");
}
builder.finish()
}
})
}

/// Appends the run of `count` fill lists that covers the gap before the next patch.
fn append_fixed_size_list_fill(
builder: &mut FixedSizeListBuilder,
fill_elements: Option<&ArrayRef>,
count: usize,
ctx: &mut ExecutionCtx,
) {
if count == 0 {
return;
}

if fill_value.is_null() {
// A null fill has no elements to share, and the builder can record the nulls without
// going through an array at all.
builder.append_nulls(count);
return;
match fill_elements {
Some(fill_elements) => builder
.append_array_as_repeated_list(fill_elements, count, ctx)
.vortex_expect("Failed to append sparse fixed-size-list fill value"),
// A null fill has no elements of its own, only the placeholders the builder writes.
None => builder.append_nulls(count),
}

ConstantArray::new(fill_value.clone(), count)
.into_array()
.append_to_builder(builder, ctx)
.vortex_expect("Failed to append sparse fill value");
}

fn execute_sparse_bools(
Expand Down Expand Up @@ -1342,10 +1423,11 @@ mod test {
}

/// Nested builders chunk a child on the boundaries it is appended on, so the number of appends
/// canonicalization makes is now visible in the elements child. A run of consecutive patches
/// and the gap after it should cost one chunk each, not one chunk per row.
/// canonicalization makes is visible in the elements child. Patches go in one at a time, so
/// they cost a chunk each; a gap covers all its rows with a single append, so it costs one
/// chunk however many rows it fills.
#[test]
fn test_sparse_list_chunks_elements_per_run_not_per_patch() -> VortexResult<()> {
fn test_sparse_list_chunks_elements_per_patch_and_once_per_gap() -> VortexResult<()> {
let mut ctx = SESSION.create_execution_ctx();

const PATCHES: usize = 100;
Expand All @@ -1369,8 +1451,8 @@ mod test {
let actual = sparse.execute::<ListViewArray>(&mut ctx)?;
assert_eq!(
actual.elements().as_::<Chunked>().nchunks(),
2,
"expected one chunk for the patch run and one for the gap",
PATCHES + 1,
"expected one chunk per patch and a single chunk for the whole gap",
);

let expected_lists = (0..patches_i32)
Expand Down
Loading
Loading