Skip to content

Commit 01f147a

Browse files
authored
Conditionally slice elements to selected index bounds before filtering wide list arrays (#9679)
## Summary Avoid expanding ListArray element masks over large unselected prefixes and suffixes. For lists averaging more than 1,024 elements per sublist, filter the child slice spanning the first through last selected list instead of the complete element range. ## Example Suppose a ListArray contains eight sublists with 2,000 elements each, and the outer filter selects rows 3 and 4. The existing path expands the mask across all 16,000 child elements. This change slices elements 6,000..10,000 and expands the mask across only those 4,000 elements. If the selected rows span the complete element range, the optimization keeps the existing full-range path. --------- Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 68d6573 commit 01f147a

1 file changed

Lines changed: 185 additions & 8 deletions

File tree

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

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

Lines changed: 185 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
use std::ops::Range;
5+
46
use num_traits::Zero;
57
use vortex_buffer::BitBufferMut;
68
use vortex_buffer::Buffer;
@@ -32,14 +34,54 @@ use crate::validity::Validity;
3234
/// Note that this is somewhat arbitrarily chosen...
3335
const MASK_EXPANSION_DENSITY_THRESHOLD: f64 = 0.05;
3436

35-
/// Construct an element mask from contiguous list offsets and a selection mask.
37+
/// Crop when at least `1 / threshold` of referenced elements are unselected.
38+
const PERCENTAGE_REFERENCED_UNSELECTED_ELEMENTS_THRESHOLD: usize = 20;
39+
40+
/// Minimum percentage of referenced-but-unselected prefix and suffix elements required before
41+
/// cropping.
42+
const N_REFERENCED_UNSELECTED_ELEMENTS_THRESHOLD: usize = 1024;
43+
44+
/// Return the element range to construct new mask over and to subsequently filter. In the general case this will be the range
45+
/// of elements referenced by a sublist.
46+
///
47+
/// If there are enough elements in head or tail that are referenced but not selected, it is more efficient
48+
/// to bound the element range to the first and last selected indices. We can then expand the
49+
/// mask only over this subset of elements, slice the elements array, and then filter. This avoids the
50+
/// overhead of potentially wasteful mask reconstruction.
51+
///
52+
/// Returns the range and a flag indicating whether the range is a subinterval of the referenced element range.
53+
fn element_range_from_offsets<O: IntegerPType>(
54+
offsets: &[O],
55+
selection: &MaskValuesRef,
56+
) -> (Range<usize>, bool) {
57+
let referenced_elements_range = offsets[0].as_()..offsets[offsets.len() - 1].as_();
58+
59+
let selected_indices = selection.indices();
60+
let first_selected_sublist_index = selected_indices[0];
61+
let last_selected_sublist_index = selected_indices[selected_indices.len() - 1];
62+
let selected_elements_range =
63+
offsets[first_selected_sublist_index].as_()..offsets[last_selected_sublist_index + 1].as_();
64+
let trimmed_element_count = referenced_elements_range.len() - selected_elements_range.len();
65+
66+
if trimmed_element_count >= N_REFERENCED_UNSELECTED_ELEMENTS_THRESHOLD
67+
&& trimmed_element_count.saturating_mul(PERCENTAGE_REFERENCED_UNSELECTED_ELEMENTS_THRESHOLD)
68+
>= referenced_elements_range.len()
69+
{
70+
(selected_elements_range, true)
71+
} else {
72+
(referenced_elements_range, false)
73+
}
74+
}
75+
76+
/// Construct an element mask relative to `element_range` from contiguous list offsets and an
77+
/// outer-row selection mask.
3678
pub fn element_mask_from_offsets<O: IntegerPType>(
3779
offsets: &[O],
3880
selection: &MaskValuesRef,
81+
element_range: &Range<usize>,
3982
) -> Mask {
40-
let first_offset = offsets.first().map_or(0, |first_offset| first_offset.as_());
41-
let last_offset = offsets.last().map_or(0, |last_offset| last_offset.as_());
42-
let len = last_offset - first_offset;
83+
let first_offset = element_range.start;
84+
let len = element_range.end - first_offset;
4385

4486
let mut mask_builder = BitBufferMut::with_capacity(len);
4587

@@ -119,7 +161,7 @@ impl FilterKernel for List {
119161
// TODO(ngates): for ultra-sparse masks, we don't need to optimize the entire offsets.
120162
let offsets = array.offsets().clone();
121163

122-
let (new_offsets, element_mask) =
164+
let (new_offsets, element_range, range_is_subinterval, element_mask) =
123165
match_each_integer_ptype!(offsets.dtype().as_ptype(), |O| {
124166
let offsets_buffer = offsets.execute::<Buffer<O>>(ctx)?;
125167
let offsets = offsets_buffer.as_slice();
@@ -135,12 +177,27 @@ impl FilterKernel for List {
135177

136178
// TODO(ngates): for very dense masks, there may be no point in filtering the elements,
137179
// and instead we should construct a view against the unfiltered elements.
138-
let element_mask = element_mask_from_offsets::<O>(offsets, selection);
180+
let (element_range, range_is_subinterval) =
181+
element_range_from_offsets::<O>(offsets, selection);
182+
let element_mask =
183+
element_mask_from_offsets::<O>(offsets, selection, &element_range);
139184

140-
(new_offsets.freeze().into_array(), element_mask)
185+
(
186+
new_offsets.freeze().into_array(),
187+
element_range,
188+
range_is_subinterval,
189+
element_mask,
190+
)
141191
});
142192

143-
let new_elements = array.sliced_elements()?.filter(element_mask)?;
193+
let new_elements = if range_is_subinterval {
194+
array
195+
.elements()
196+
.slice(element_range)?
197+
.filter(element_mask)?
198+
} else {
199+
array.sliced_elements()?.filter(element_mask)?
200+
};
144201

145202
// SAFETY: new_offsets are monotonically increasing starting from 0 with length
146203
// true_count + 1, and the elements have been filtered to match.
@@ -149,3 +206,123 @@ impl FilterKernel for List {
149206
}))
150207
}
151208
}
209+
210+
#[cfg(test)]
211+
mod tests {
212+
use vortex_error::VortexResult;
213+
use vortex_error::vortex_bail;
214+
use vortex_mask::Mask;
215+
216+
use super::element_mask_from_offsets;
217+
use super::element_range_from_offsets;
218+
219+
#[test]
220+
fn element_mask_excludes_unselected_prefix_and_suffix() -> VortexResult<()> {
221+
let Mask::Values(selection) = Mask::from_indices(5, [2]) else {
222+
vortex_bail!("a partially selective mask uses Mask::Values")
223+
};
224+
225+
let offsets = [10u32, 20010, 40010, 60010, 80010, 100010];
226+
let (range, range_is_subinterval) = element_range_from_offsets(&offsets, &selection);
227+
let element_mask = element_mask_from_offsets(&offsets, &selection, &range);
228+
229+
assert_eq!(range, 40010..60010);
230+
assert!(range_is_subinterval);
231+
assert!(element_mask.all_true());
232+
assert_eq!(element_mask.len(), 20000);
233+
Ok(())
234+
}
235+
236+
#[test]
237+
fn element_mask_retains_gaps_between_selected_lists() -> VortexResult<()> {
238+
let Mask::Values(selection) = Mask::from_indices(5, [1, 3]) else {
239+
vortex_bail!("a partially selective mask uses Mask::Values")
240+
};
241+
242+
let offsets = [10u32, 20010, 40010, 60010, 80010, 100010];
243+
let (range, range_is_subinterval) = element_range_from_offsets(&offsets, &selection);
244+
let element_mask = element_mask_from_offsets(&offsets, &selection, &range);
245+
246+
assert_eq!(range, 20010..80010);
247+
assert!(range_is_subinterval);
248+
assert_eq!(element_mask.len(), 60000);
249+
assert_eq!(element_mask.true_count(), 40000);
250+
Ok(())
251+
}
252+
253+
#[test]
254+
fn element_range_preserves_complete_range_for_short_lists() -> VortexResult<()> {
255+
let Mask::Values(selection) = Mask::from_indices(5, [2]) else {
256+
vortex_bail!("a partially selective mask uses Mask::Values")
257+
};
258+
259+
let offsets = [10u32, 20, 30, 40, 50, 60];
260+
let (range, range_is_subinterval) = element_range_from_offsets(&offsets, &selection);
261+
let element_mask = element_mask_from_offsets(&offsets, &selection, &range);
262+
263+
assert_eq!(range, 10..60);
264+
assert!(!range_is_subinterval);
265+
assert_eq!(element_mask.len(), 50);
266+
assert_eq!(element_mask.true_count(), 10);
267+
Ok(())
268+
}
269+
270+
#[test]
271+
fn element_range_requires_minimum_savings() -> VortexResult<()> {
272+
let Mask::Values(selection) = Mask::from_indices(2, [0]) else {
273+
vortex_bail!("a partially selective mask uses Mask::Values")
274+
};
275+
276+
let offsets = [0u32, 512, 1024];
277+
assert_eq!(
278+
element_range_from_offsets(&offsets, &selection),
279+
(0..1024, false)
280+
);
281+
Ok(())
282+
}
283+
284+
#[test]
285+
fn element_range_requires_sufficient_savings_ratio() -> VortexResult<()> {
286+
let Mask::Values(selection) = Mask::from_slices(100, vec![(0, 96)]) else {
287+
vortex_bail!("a partially selective mask uses Mask::Values")
288+
};
289+
let offsets = (0..=100).map(|index| index * 1000).collect::<Vec<u32>>();
290+
291+
assert_eq!(
292+
element_range_from_offsets(&offsets, &selection),
293+
(0..100_000, false)
294+
);
295+
Ok(())
296+
}
297+
298+
#[test]
299+
fn element_range_crops_at_sufficient_savings_ratio() -> VortexResult<()> {
300+
let Mask::Values(selection) = Mask::from_slices(100, vec![(0, 95)]) else {
301+
vortex_bail!("a partially selective mask uses Mask::Values")
302+
};
303+
let offsets = (0..=100).map(|index| index * 1000).collect::<Vec<u32>>();
304+
305+
assert_eq!(
306+
element_range_from_offsets(&offsets, &selection),
307+
(0..95_000, true)
308+
);
309+
Ok(())
310+
}
311+
312+
#[test]
313+
fn element_mask_preserves_complete_range_for_edge_spanning_selection() -> VortexResult<()> {
314+
let Mask::Values(selection) = Mask::from_indices(5, [0, 4]) else {
315+
vortex_bail!("a partially selective mask uses Mask::Values")
316+
};
317+
318+
let offsets = [10u32, 20010, 40010, 60010, 80010, 100010];
319+
let (range, range_is_subinterval) = element_range_from_offsets(&offsets, &selection);
320+
let element_mask = element_mask_from_offsets(&offsets, &selection, &range);
321+
322+
assert_eq!(range, 10..100010);
323+
assert!(!range_is_subinterval);
324+
assert_eq!(element_mask.len(), 100000);
325+
assert_eq!(element_mask.true_count(), 40000);
326+
Ok(())
327+
}
328+
}

0 commit comments

Comments
 (0)