Skip to content

Commit ed77d49

Browse files
committed
skip the list rebuild when lists are already exact
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 9534b7d commit ed77d49

1 file changed

Lines changed: 62 additions & 15 deletions

File tree

  • vortex-array/src/scalar_fn/fns/list_transform

‎vortex-array/src/scalar_fn/fns/list_transform/mod.rs‎

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ use std::fmt::Formatter;
1010
use std::sync::Arc;
1111

1212
use itertools::Itertools;
13+
use num_traits::AsPrimitive;
14+
use vortex_buffer::BufferMut;
1315
use vortex_error::VortexResult;
1416
use vortex_error::vortex_bail;
1517
use vortex_error::vortex_ensure;
1618
use vortex_error::vortex_ensure_eq;
19+
use vortex_mask::AllOr;
1720
use vortex_session::registry::CachedId;
1821

1922
use crate::ArrayRef;
2023
use crate::ExecutionCtx;
2124
use crate::IntoArray;
25+
use crate::arrays::List;
2226
use crate::arrays::ListArray;
2327
use crate::arrays::ListViewArray;
2428
use crate::arrays::PrimitiveArray;
@@ -133,10 +137,7 @@ impl ScalarFnVTable for ListTransform {
133137
let ListTransformOptions::Bound(lambda) = options else {
134138
vortex_bail!("list_transform()'s lambda is not bound");
135139
};
136-
// Every element belongs to exactly one valid list, in order: nothing comes before the
137-
// first list or after the last, and a null list has no elements.
138-
let list = args.get(0)?.execute::<ListViewArray>(ctx)?;
139-
let list = list_from_list_view(empty_null_lists(list, ctx)?, ctx)?;
140+
let list = exact_lists(args.get(0)?, ctx)?;
140141
let elements = list.elements();
141142
let values = lambda.evaluate(vec![elements.clone()], elements.len())?;
142143
vortex_ensure_eq!(values.len(), elements.len());
@@ -184,28 +185,74 @@ fn element_dtype(list: &DType, params: &[FieldName]) -> VortexResult<[DType; 1]>
184185
Ok([element.as_ref().clone()])
185186
}
186187

188+
/// The lists laid out so that every element belongs to exactly one valid list, in order: nothing
189+
/// comes before the first list or after the last, and a null list has no elements.
190+
fn exact_lists(list: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<ListArray> {
191+
// A list array usually is exact already, and then needs no rebuild.
192+
let list = match list.try_downcast::<List>() {
193+
Ok(list) if is_exact(&list, ctx)? => return Ok(list),
194+
Ok(list) => list.into_array(),
195+
Err(list) => list,
196+
};
197+
let list = list.execute::<ListViewArray>(ctx)?;
198+
list_from_list_view(empty_null_lists(list, ctx)?, ctx)
199+
}
200+
201+
/// Whether nothing comes before the first list or after the last, and no null list holds elements.
202+
fn is_exact(list: &ListArray, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
203+
let offsets = list.offsets().clone().execute::<PrimitiveArray>(ctx)?;
204+
let nulls = null_runs(&list.list_validity(), list.len(), ctx)?;
205+
Ok(match_each_integer_ptype!(offsets.ptype(), |O| {
206+
let offsets = offsets.as_slice::<O>();
207+
let last: usize = offsets[offsets.len() - 1].as_();
208+
// The offsets are sorted, so a run of null lists is empty when it starts and ends at the
209+
// same offset.
210+
offsets[0] == O::default()
211+
&& last == list.elements().len()
212+
&& nulls
213+
.iter()
214+
.all(|&(start, end)| offsets[start] == offsets[end])
215+
}))
216+
}
217+
218+
/// The runs of null rows, as `(start, end)` ranges.
219+
fn null_runs(
220+
validity: &Validity,
221+
len: usize,
222+
ctx: &mut ExecutionCtx,
223+
) -> VortexResult<Vec<(usize, usize)>> {
224+
if matches!(validity, Validity::NonNullable | Validity::AllValid) {
225+
return Ok(Vec::new());
226+
}
227+
Ok(match (!&validity.execute_mask(len, ctx)?).slices() {
228+
AllOr::All => vec![(0, len)],
229+
AllOr::None => Vec::new(),
230+
AllOr::Some(runs) => runs.to_vec(),
231+
})
232+
}
233+
187234
/// The lists with every null list emptied, so that nothing under a null list reaches the lambda.
188235
fn empty_null_lists(list: ListViewArray, ctx: &mut ExecutionCtx) -> VortexResult<ListViewArray> {
189236
let validity = list.listview_validity();
190-
if matches!(validity, Validity::NonNullable | Validity::AllValid) {
237+
let nulls = null_runs(&validity, list.len(), ctx)?;
238+
if nulls.is_empty() {
191239
return Ok(list);
192240
}
193-
let valid = validity.execute_mask(list.len(), ctx)?;
194241
let sizes = list.sizes().clone().execute::<PrimitiveArray>(ctx)?;
195242
let sizes = match_each_integer_ptype!(sizes.ptype(), |T| {
196243
let sizes = sizes.as_slice::<T>();
197-
let null = |index: usize| !valid.value(index);
198244
// Writers usually leave null lists empty already.
199-
if !(0..sizes.len()).any(|index| null(index) && sizes[index] != T::default()) {
245+
if nulls
246+
.iter()
247+
.all(|&(start, end)| sizes[start..end].iter().all(|size| *size == T::default()))
248+
{
200249
return Ok(list);
201250
}
202-
PrimitiveArray::from_iter((0..sizes.len()).map(|index| {
203-
if null(index) {
204-
T::default()
205-
} else {
206-
sizes[index]
207-
}
208-
}))
251+
let mut sizes = BufferMut::copy_from(sizes);
252+
for &(start, end) in &nulls {
253+
sizes[start..end].fill(T::default());
254+
}
255+
PrimitiveArray::new(sizes.freeze(), Validity::NonNullable)
209256
});
210257
// SAFETY: only sizes changed, and only to 0, so every view stays within the elements. The new
211258
// array is not marked zero-copy to a list: an emptied list leaves its elements behind as a gap,

0 commit comments

Comments
 (0)