Skip to content

Commit 70daf11

Browse files
committed
Use PiecewiseSequence for List take elements
Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent 56b0394 commit 70daf11

1 file changed

Lines changed: 65 additions & 119 deletions

File tree

  • vortex-array/src/arrays/list/compute

vortex-array/src/arrays/list/compute/take.rs

Lines changed: 65 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ use crate::arrays::list::ListArrayExt;
2424
use crate::arrays::piecewise_sequence::constant_unsigned_usize;
2525
use crate::arrays::piecewise_sequence::maybe_contiguous_slices;
2626
use crate::arrays::primitive::PrimitiveArrayExt;
27-
use crate::builders::ArrayBuilder;
28-
use crate::builders::PrimitiveBuilder;
2927
use crate::dtype::IntegerPType;
30-
use crate::dtype::Nullability;
3128
use crate::dtype::UnsignedPType;
3229
use crate::executor::ExecutionCtx;
3330
use crate::match_each_unsigned_integer_ptype;
@@ -65,7 +62,7 @@ impl TakeExecute for List {
6562
match_each_unsigned_integer_ptype!(offsets.ptype(), |O| {
6663
match_each_unsigned_integer_ptype!(indices.ptype(), |I| {
6764
match_smallest_offset_type!(total_approx, |OutputOffsetType| {
68-
_take::<I, O, OutputOffsetType>(
65+
take_with_piecewise_elements::<I, O, OutputOffsetType>(
6966
array,
7067
offsets.as_view(),
7168
indices.as_view(),
@@ -78,7 +75,11 @@ impl TakeExecute for List {
7875
}
7976
}
8077

81-
fn _take<I: IntegerPType, O: IntegerPType, OutputOffsetType: IntegerPType>(
78+
fn take_with_piecewise_elements<
79+
I: IntegerPType,
80+
O: IntegerPType,
81+
OutputOffsetType: IntegerPType,
82+
>(
8283
array: ArrayView<'_, List>,
8384
offsets_array: ArrayView<'_, Primitive>,
8485
indices_array: ArrayView<'_, Primitive>,
@@ -92,56 +93,78 @@ fn _take<I: IntegerPType, O: IntegerPType, OutputOffsetType: IntegerPType>(
9293
.vortex_expect("Failed to compute validity mask")
9394
.execute_mask(indices_array.as_ref().len(), ctx)?;
9495

95-
if !indices_validity.all_true() || !data_validity.all_true() {
96-
return _take_nullable::<I, O, OutputOffsetType>(array, offsets_array, indices_array, ctx);
97-
}
98-
9996
let offsets: &[O] = offsets_array.as_slice();
10097
let indices: &[I] = indices_array.as_slice();
10198

102-
let mut new_offsets = PrimitiveBuilder::<OutputOffsetType>::with_capacity(
103-
Nullability::NonNullable,
104-
indices.len(),
105-
);
106-
let mut elements_to_take =
107-
PrimitiveBuilder::with_capacity(Nullability::NonNullable, 2 * indices.len());
99+
let offsets_capacity = indices
100+
.len()
101+
.checked_add(1)
102+
.ok_or_else(|| vortex_err!("List take offsets length overflow"))?;
103+
let mut new_offsets = BufferMut::<OutputOffsetType>::with_capacity(offsets_capacity);
104+
let mut element_starts = BufferMut::<u64>::with_capacity(indices.len());
105+
let mut element_lengths = BufferMut::<u64>::with_capacity(indices.len());
106+
107+
let mut current_offset = 0usize;
108+
new_offsets.push(OutputOffsetType::zero());
108109

109-
let mut current_offset = OutputOffsetType::zero();
110-
new_offsets.append_zero();
110+
for (&data_idx, index_valid) in indices.iter().zip_eq(indices_validity.iter()) {
111+
if !index_valid {
112+
new_offsets.push(new_offset_value::<OutputOffsetType>(current_offset)?);
113+
element_starts.push(0);
114+
element_lengths.push(0);
115+
continue;
116+
}
111117

112-
for &data_idx in indices {
113118
let data_idx: usize = data_idx.as_();
114119

120+
if !data_validity.value(data_idx) {
121+
new_offsets.push(new_offset_value::<OutputOffsetType>(current_offset)?);
122+
element_starts.push(0);
123+
element_lengths.push(0);
124+
continue;
125+
}
126+
115127
let start = offsets[data_idx];
116128
let stop = offsets[data_idx + 1];
129+
let start: usize = start.as_();
130+
let stop: usize = stop.as_();
131+
let length = stop
132+
.checked_sub(start)
133+
.ok_or_else(|| vortex_err!("List offsets are not monotonic at offset {stop}"))?;
117134

118-
// Annoyingly, we can't turn (start..end) into a range, so we're doing that manually.
119-
//
120-
// We could convert start and end to usize, but that would impose a potentially
121-
// harder constraint - now we don't care if they fit into usize as long as their
122-
// difference does.
123-
let additional: usize = (stop - start).as_();
124-
125-
// TODO(0ax1): optimize this
126-
elements_to_take.reserve_exact(additional);
127-
for i in 0..additional {
128-
elements_to_take.append_value(start + O::from_usize(i).vortex_expect("i < additional"));
129-
}
130-
current_offset +=
131-
OutputOffsetType::from_usize((stop - start).as_()).vortex_expect("offset conversion");
132-
new_offsets.append_value(current_offset);
135+
current_offset = current_offset
136+
.checked_add(length)
137+
.ok_or_else(|| vortex_err!("List take output elements length overflow"))?;
138+
new_offsets.push(new_offset_value::<OutputOffsetType>(current_offset)?);
139+
element_starts.push(start as u64);
140+
element_lengths.push(length as u64);
133141
}
134142

135-
let elements_to_take = elements_to_take.finish();
136-
let new_offsets = new_offsets.finish();
137-
138-
let new_elements = array.elements().take(elements_to_take)?;
143+
let new_offsets = PrimitiveArray::new(new_offsets.freeze(), Validity::NonNullable).into_array();
144+
let multipliers = ConstantArray::new(1u64, element_starts.len()).into_array();
139145

140-
Ok(ListArray::try_new(
141-
new_elements,
142-
new_offsets,
143-
array.validity()?.take(indices_array.array())?,
144-
)?
146+
// SAFETY: valid source rows contribute ranges derived from list offsets; null index/source
147+
// rows contribute zero-length placeholder ranges. `current_offset` is the sum of all generated
148+
// element lengths, and multiplier 1 preserves contiguous ranges.
149+
let element_indices = unsafe {
150+
PiecewiseSequenceArray::new_unchecked(
151+
element_starts.into_array(),
152+
element_lengths.into_array(),
153+
multipliers,
154+
current_offset,
155+
)
156+
};
157+
let new_elements = array.elements().take(element_indices.into_array())?;
158+
159+
// SAFETY: offsets are rebuilt from the gathered element ranges and have one entry per output
160+
// row plus the leading zero; validity is produced by the usual take-validity path.
161+
Ok(unsafe {
162+
ListArray::new_unchecked(
163+
new_elements,
164+
new_offsets,
165+
array.validity()?.take(indices_array.array())?,
166+
)
167+
}
145168
.into_array())
146169
}
147170

@@ -577,83 +600,6 @@ fn new_offset_value<T: IntegerPType>(value: usize) -> T {
577600
T::from_usize(value).vortex_expect("output offset fits selected offset type")
578601
}
579602

580-
// Kept out-of-line: as a single-callsite generic helper it would otherwise be inlined into every
581-
// monomorphization of `_take`, duplicating the entire nullable path across all specializations.
582-
#[inline(never)]
583-
fn _take_nullable<I: IntegerPType, O: IntegerPType, OutputOffsetType: IntegerPType>(
584-
array: ArrayView<'_, List>,
585-
offsets_array: ArrayView<'_, Primitive>,
586-
indices_array: ArrayView<'_, Primitive>,
587-
ctx: &mut ExecutionCtx,
588-
) -> VortexResult<ArrayRef> {
589-
let offsets: &[O] = offsets_array.as_slice();
590-
let indices: &[I] = indices_array.as_slice();
591-
let data_validity = array
592-
.list_validity()
593-
.execute_mask(array.as_ref().len(), ctx)?;
594-
let indices_validity = indices_array
595-
.validity()
596-
.vortex_expect("Failed to compute validity mask")
597-
.execute_mask(indices_array.as_ref().len(), ctx)?;
598-
599-
let mut new_offsets = PrimitiveBuilder::<OutputOffsetType>::with_capacity(
600-
Nullability::NonNullable,
601-
indices.len(),
602-
);
603-
604-
// This will be the indices we push down to the child array to call `take` with.
605-
//
606-
// There are 2 things to note here:
607-
// - We do not know how many elements we need to take from our child since lists are variable
608-
// size: thus we arbitrarily choose a capacity of `2 * # of indices`.
609-
// - The type of the primitive builder needs to fit the largest offset of the (parent)
610-
// `ListArray`, so we make this `PrimitiveBuilder` generic over `O` (instead of `I`).
611-
let mut elements_to_take =
612-
PrimitiveBuilder::<O>::with_capacity(Nullability::NonNullable, 2 * indices.len());
613-
614-
let mut current_offset = OutputOffsetType::zero();
615-
new_offsets.append_zero();
616-
617-
for (data_idx, index_valid) in indices.iter().zip(indices_validity.iter()) {
618-
if !index_valid {
619-
new_offsets.append_value(current_offset);
620-
continue;
621-
}
622-
623-
let data_idx: usize = data_idx.as_();
624-
625-
if !data_validity.value(data_idx) {
626-
new_offsets.append_value(current_offset);
627-
continue;
628-
}
629-
630-
let start = offsets[data_idx];
631-
let stop = offsets[data_idx + 1];
632-
633-
// See the note in `_take` on the reasoning.
634-
let additional: usize = (stop - start).as_();
635-
636-
elements_to_take.reserve_exact(additional);
637-
for i in 0..additional {
638-
elements_to_take.append_value(start + O::from_usize(i).vortex_expect("i < additional"));
639-
}
640-
current_offset +=
641-
OutputOffsetType::from_usize((stop - start).as_()).vortex_expect("offset conversion");
642-
new_offsets.append_value(current_offset);
643-
}
644-
645-
let elements_to_take = elements_to_take.finish();
646-
let new_offsets = new_offsets.finish();
647-
let new_elements = array.elements().take(elements_to_take)?;
648-
649-
Ok(ListArray::try_new(
650-
new_elements,
651-
new_offsets,
652-
array.validity()?.take(indices_array.array())?,
653-
)?
654-
.into_array())
655-
}
656-
657603
#[cfg(test)]
658604
mod test {
659605
use std::sync::Arc;

0 commit comments

Comments
 (0)