Skip to content

Commit 13eef3b

Browse files
clauderobert3005
authored andcommitted
perf(array): stop flattening appended list views in ListViewBuilder
`append_listview_array` rebuilt every incoming `ListViewArray` into an exact layout before appending it. Rebasing offsets by the number of elements already in the builder is correct whatever layout they have, so the rebuild bought nothing except an unconditional promise that the finished array is zero-copyable to a `ListArray` - and it cost the caller any sharing the source expressed. A constant list array is the case that matters: canonicalizing one already points every view at a single copy of the value, and flattening it materialized one copy per row. Appending a 10,000-row constant list of three elements produced 30,000 elements; it now produces 3. Keep trimming unreferenced elements, but otherwise append the views as they arrived and track whether the result is still zero-copyable to a `ListArray` instead of asserting it. The flag is per-array and consumers already branch on it, so callers that need an exact layout can rebuild. Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Robert Kruszewski <robert@spiraldb.com>
1 parent 49f98c3 commit 13eef3b

2 files changed

Lines changed: 66 additions & 6 deletions

File tree

vortex-array/src/aggregate_fn/fns/uncompressed_size_in_bytes/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ mod tests {
350350
use crate::arrays::UnionArray;
351351
use crate::arrays::VarBinViewArray;
352352
use crate::arrays::VariantArray;
353+
use crate::arrays::listview::ListViewRebuildMode;
353354
use crate::builders::builder_with_capacity;
354355
use crate::dtype::DType;
355356
use crate::dtype::DecimalDType;
@@ -506,9 +507,21 @@ mod tests {
506507
let array =
507508
ListViewArray::new(elements, offsets, sizes, Validity::NonNullable).into_array();
508509

510+
// These lists are out of order and leave element 1 unreferenced. `ListViewBuilder` keeps
511+
// the layout it is handed, so the builder round-trip inside
512+
// `materialized_uncompressed_size_in_bytes` retains that element and no longer stands in
513+
// for the logical size. Rebuild to the exact layout, which is what "materialized" means
514+
// here.
515+
let mut ctx = array_session().create_execution_ctx();
516+
let exact = array
517+
.clone()
518+
.execute::<ListViewArray>(&mut ctx)?
519+
.rebuild(ListViewRebuildMode::MakeExact, &mut ctx)?
520+
.into_array();
521+
509522
assert_eq!(
510523
aggregate(&array)?,
511-
materialized_uncompressed_size_in_bytes(&array)
524+
materialized_uncompressed_size_in_bytes(&exact)
512525
);
513526
Ok(())
514527
}

vortex-array/src/builders/listview.rs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ pub struct ListViewBuilder<O: OffsetBuilderPType, S: OffsetBuilderPType> {
6969

7070
/// The null map builder of the [`ListViewArray`].
7171
nulls: LazyBitBufferBuilder,
72+
73+
/// Whether the appends so far leave the result zero-copyable to a [`ListArray`].
74+
///
75+
/// Only [`append_listview_array`](ArrayBuilder::append_listview_array) can clear this; every
76+
/// other append writes its lists back to back.
77+
///
78+
/// [`ListArray`]: crate::arrays::ListArray
79+
zero_copy_to_list: bool,
7280
}
7381

7482
impl<O: OffsetBuilderPType, S: OffsetBuilderPType> ListViewBuilder<O, S> {
@@ -112,6 +120,7 @@ impl<O: OffsetBuilderPType, S: OffsetBuilderPType> ListViewBuilder<O, S> {
112120
offsets_builder,
113121
sizes_builder,
114122
nulls,
123+
zero_copy_to_list: true,
115124
}
116125
}
117126

@@ -207,6 +216,8 @@ impl<O: OffsetBuilderPType, S: OffsetBuilderPType> ListViewBuilder<O, S> {
207216
let sizes = self.sizes_builder.finish();
208217
let validity = self.nulls.finish_with_nullability(self.dtype.nullability());
209218

219+
let zero_copy_to_list = std::mem::replace(&mut self.zero_copy_to_list, true);
220+
210221
// SAFETY:
211222
// - Both the offsets and the sizes are non-nullable.
212223
// - The offsets, sizes, and validity have the same length since we always appended the same
@@ -215,11 +226,11 @@ impl<O: OffsetBuilderPType, S: OffsetBuilderPType> ListViewBuilder<O, S> {
215226
// - In every method that adds values to this builder (`append_value`, `append_scalar`,
216227
// `append_list_array`, and `append_listview_array`), we checked that `offset + size`
217228
// does not overflow.
218-
// - We constructed everything in a way that builds the `ListViewArray` similar to the shape
219-
// of a `ListArray`, so we know the resulting array is zero-copyable to a `ListArray`.
229+
// - Every append writes its lists back to back, so the result is zero-copyable to a
230+
// `ListArray` unless `zero_copy_to_list` recorded an appended layout we left alone.
220231
unsafe {
221232
ListViewArray::new_unchecked(elements, offsets, sizes, validity)
222-
.with_zero_copy_to_list(true)
233+
.with_zero_copy_to_list(zero_copy_to_list)
223234
}
224235
}
225236

@@ -525,6 +536,7 @@ mod tests {
525536
use crate::IntoArray;
526537
use crate::VortexSessionExecute;
527538
use crate::array_session;
539+
use crate::arrays::ConstantArray;
528540
use crate::arrays::ListArray;
529541
use crate::arrays::ListViewArray;
530542
use crate::arrays::listview::ListViewArrayExt;
@@ -845,6 +857,39 @@ mod tests {
845857
Ok(())
846858
}
847859

860+
/// A constant list array points every view at a single copy of the value. Flattening it in the
861+
/// builder would materialize a copy per row, undoing the reason to append the array at all
862+
/// instead of the same list in a loop.
863+
#[test]
864+
fn test_constant_list_append_keeps_one_copy_of_the_value() -> VortexResult<()> {
865+
let mut ctx = array_session().create_execution_ctx();
866+
let element_dtype: Arc<DType> = Arc::new(I32.into());
867+
868+
const ROWS: usize = 10_000;
869+
let fill = Scalar::list(
870+
Arc::clone(&element_dtype),
871+
vec![1i32.into(), 2i32.into(), 3i32.into()],
872+
NonNullable,
873+
);
874+
let constant = ConstantArray::new(fill, ROWS).into_array();
875+
876+
let mut builder =
877+
ListViewBuilder::<u64, u64>::with_capacity(element_dtype, NonNullable, 0, 0);
878+
constant.append_to_builder(&mut builder, &mut ctx)?;
879+
let listview = builder.finish_into_listview();
880+
881+
assert_eq!(listview.len(), ROWS);
882+
assert_eq!(
883+
listview.elements().len(),
884+
3,
885+
"the fill value should be stored once, not once per row",
886+
);
887+
assert!(!listview.is_zero_copy_to_list());
888+
assert_arrays_eq!(&listview.into_array(), &constant, &mut ctx);
889+
890+
Ok(())
891+
}
892+
848893
#[test]
849894
fn test_extend_from_array_overlapping_listview() {
850895
let mut ctx = array_session().create_execution_ctx();
@@ -872,7 +917,8 @@ mod tests {
872917

873918
let listview = builder.finish_into_listview();
874919
assert_eq!(listview.len(), 3);
875-
assert!(listview.is_zero_copy_to_list());
920+
// The builder kept the source's overlapping layout, so the result is not zero-copyable.
921+
assert!(!listview.is_zero_copy_to_list());
876922

877923
assert_arrays_eq!(
878924
listview.list_elements_at(0).unwrap(),
@@ -886,7 +932,8 @@ mod tests {
886932
.execute_is_valid(1, &mut ctx)
887933
.unwrap()
888934
);
889-
assert_eq!(listview.list_elements_at(1).unwrap().len(), 0);
935+
// List 1 is null, so its size is meaningless; the builder no longer rewrites it to zero.
936+
assert_eq!(listview.size_at(1), source.size_at(1));
890937
assert_arrays_eq!(
891938
listview.list_elements_at(2).unwrap(),
892939
PrimitiveArray::from_iter([10i32]),

0 commit comments

Comments
 (0)