Skip to content

Commit 81c8f13

Browse files
authored
Use PiecewiseSequence indices for FSL take (#8800)
## Summary Ports the TakeSlices-style optimization to `Take(elements, PiecewiseSequenceArray)` instead of introducing a value-wrapper API. - FSL take now builds one start per selected list, a constant list length, and a constant multiplier of `1`, then calls `elements.take(piecewise_indices)`. - Primitive, bool, decimal, VarBinView, and VarBin take paths detect `PiecewiseSequenceArray` indices and gather contiguous ranges only when `multipliers` is a constant unsigned `1`; non-unit multipliers fall back through normal materialized-index take. - Shared PiecewiseSequence execution handles general `start + j * multiplier` sequences, while the current optimized consumers stay explicitly contiguous. ## Validation - `cargo fmt -p vortex-array` - `cargo test -p vortex-array piecewise_sequence` - `cargo check -p vortex-array` - `cargo check -p vortex-array --benches` - `git diff --check` --------- Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent 05c7c2f commit 81c8f13

11 files changed

Lines changed: 1211 additions & 277 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vortex-array/benches/take_fsl.rs

Lines changed: 259 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,36 @@
66
//! Parameterized over:
77
//! - Number of indices to take
88
//! - Fixed size list length (elements per list)
9+
//! - Element byte width
910
1011
#![expect(clippy::cast_possible_truncation)]
1112
#![expect(clippy::unwrap_used)]
1213

1314
use std::sync::LazyLock;
1415

1516
use divan::Bencher;
17+
use divan::counter::BytesCount;
18+
use num_traits::FromPrimitive;
1619
use rand::RngExt;
1720
use rand::SeedableRng;
1821
use rand::rngs::StdRng;
22+
use vortex_array::ExecutionCtx;
1923
use vortex_array::IntoArray;
2024
use vortex_array::RecursiveCanonical;
2125
use vortex_array::VortexSessionExecute;
2226
use vortex_array::array_session;
27+
use vortex_array::arrays::ConstantArray;
2328
use vortex_array::arrays::FixedSizeListArray;
29+
use vortex_array::arrays::PiecewiseSequenceArray;
30+
use vortex_array::arrays::PrimitiveArray;
31+
use vortex_array::arrays::fixed_size_list::FixedSizeListArrayExt;
32+
use vortex_array::dtype::IntegerPType;
33+
use vortex_array::dtype::NativePType;
34+
use vortex_array::dtype::half::f16;
35+
use vortex_array::match_smallest_offset_type;
2436
use vortex_array::validity::Validity;
2537
use vortex_buffer::Buffer;
38+
use vortex_buffer::BufferMut;
2639
use vortex_session::VortexSession;
2740

2841
fn main() {
@@ -39,18 +52,45 @@ const NUM_LISTS: usize = 500;
3952
const NUM_INDICES: &[usize] = &[100, 1_000];
4053

4154
/// Fixed size list lengths (elements per list).
42-
const LIST_SIZES: &[usize] = &[16, 64, 256, 1024, 4096];
55+
const LIST_SIZES: &[usize] = &[16, 64, 128, 256, 512, 1024, 2048, 4096];
56+
57+
/// F16 list lengths for isolating the per-index, piecewise, and manual range-copy strategies.
58+
const F16_STRATEGY_LIST_SIZES: &[usize] = &[1, 2, 4, 8, 16, 64, 128, 256, 512, 1024, 2048, 4096];
59+
60+
/// F16 strategy benchmarks keep a smaller take width so the forced slow strategies stay cheap.
61+
const F16_STRATEGY_NUM_INDICES: &[usize] = &[10];
4362

4463
/// Creates a FixedSizeListArray with the given list size and number of lists.
45-
fn create_fsl(list_size: usize, num_lists: usize) -> FixedSizeListArray {
64+
fn create_fsl<T>(list_size: usize, num_lists: usize) -> FixedSizeListArray
65+
where
66+
T: NativePType + FromPrimitive,
67+
{
68+
create_fsl_with_validity::<T>(list_size, num_lists, Validity::NonNullable)
69+
}
70+
71+
fn create_fsl_with_validity<T>(
72+
list_size: usize,
73+
num_lists: usize,
74+
validity: Validity,
75+
) -> FixedSizeListArray
76+
where
77+
T: NativePType + FromPrimitive,
78+
{
79+
let total_elements = list_size * num_lists;
80+
let elements: Buffer<T> = (0..total_elements)
81+
.map(|idx| T::from_u16((idx % 251) as u16).unwrap())
82+
.collect();
83+
FixedSizeListArray::new(elements.into_array(), list_size as u32, validity, num_lists)
84+
}
85+
86+
fn create_i64_fsl_with_validity(
87+
list_size: usize,
88+
num_lists: usize,
89+
validity: Validity,
90+
) -> FixedSizeListArray {
4691
let total_elements = list_size * num_lists;
4792
let elements: Buffer<i64> = (0..total_elements as i64).collect();
48-
FixedSizeListArray::new(
49-
elements.into_array(),
50-
list_size as u32,
51-
Validity::NonNullable,
52-
num_lists,
53-
)
93+
FixedSizeListArray::new(elements.into_array(), list_size as u32, validity, num_lists)
5494
}
5595

5696
/// Creates random indices for taking from the array.
@@ -63,11 +103,50 @@ fn create_random_indices(num_indices: usize, max_index: usize) -> Buffer<u64> {
63103

64104
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
65105
fn take_fsl_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
66-
let fsl = create_fsl(LIST_SIZE, NUM_LISTS);
106+
let fsl = create_i64_fsl_with_validity(LIST_SIZE, NUM_LISTS, Validity::NonNullable);
107+
bench_take_fsl_random::<i64, LIST_SIZE>(bencher, num_indices, fsl);
108+
}
109+
110+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
111+
fn take_fsl_f16_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
112+
take_fsl_random_typed::<f16, LIST_SIZE>(bencher, num_indices);
113+
}
114+
115+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
116+
fn take_fsl_u8_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
117+
take_fsl_random_typed::<u8, LIST_SIZE>(bencher, num_indices);
118+
}
119+
120+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
121+
fn take_fsl_u32_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
122+
take_fsl_random_typed::<u32, LIST_SIZE>(bencher, num_indices);
123+
}
124+
125+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
126+
fn take_fsl_u64_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
127+
take_fsl_random_typed::<u64, LIST_SIZE>(bencher, num_indices);
128+
}
129+
130+
fn take_fsl_random_typed<T, const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize)
131+
where
132+
T: NativePType + FromPrimitive,
133+
{
134+
let fsl = create_fsl::<T>(LIST_SIZE, NUM_LISTS);
135+
bench_take_fsl_random::<T, LIST_SIZE>(bencher, num_indices, fsl);
136+
}
137+
138+
fn bench_take_fsl_random<T, const LIST_SIZE: usize>(
139+
bencher: Bencher,
140+
num_indices: usize,
141+
fsl: FixedSizeListArray,
142+
) where
143+
T: NativePType,
144+
{
67145
let indices = create_random_indices(num_indices, NUM_LISTS);
68146
let indices_array = indices.into_array();
69147

70148
bencher
149+
.counter(BytesCount::of_many::<T>(num_indices * LIST_SIZE))
71150
.with_inputs(|| (&fsl, &indices_array, SESSION.create_execution_ctx()))
72151
.bench_refs(|(array, indices, execution_ctx)| {
73152
array
@@ -79,28 +158,185 @@ fn take_fsl_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize)
79158
});
80159
}
81160

82-
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
83-
fn take_fsl_nullable_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
84-
let total_elements = LIST_SIZE * NUM_LISTS;
85-
let elements: Buffer<i64> = (0..total_elements as i64).collect();
86-
87-
// Create validity with ~10% nulls
88-
let mut rng = StdRng::seed_from_u64(123);
89-
let validity = Validity::from_iter((0..NUM_LISTS).map(|_| rng.random_ratio(9, 10)));
161+
#[divan::bench(args = F16_STRATEGY_NUM_INDICES, consts = F16_STRATEGY_LIST_SIZES)]
162+
fn take_fsl_f16_force_per_index<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
163+
let fsl = create_fsl::<f16>(LIST_SIZE, NUM_LISTS);
164+
let indices = create_random_indices(num_indices, NUM_LISTS);
90165

91-
let fsl = FixedSizeListArray::new(elements.into_array(), LIST_SIZE as u32, validity, NUM_LISTS);
166+
bencher
167+
.counter(BytesCount::of_many::<f16>(num_indices * LIST_SIZE))
168+
.with_inputs(|| (&fsl, &indices, SESSION.create_execution_ctx()))
169+
.bench_refs(|(array, indices, execution_ctx)| {
170+
match_smallest_offset_type!(array.elements().len(), |E| {
171+
take_fsl_f16_per_index_strategy::<LIST_SIZE, E>(array, indices)
172+
})
173+
.into_array()
174+
.execute::<RecursiveCanonical>(execution_ctx)
175+
.unwrap()
176+
});
177+
}
92178

179+
#[divan::bench(args = F16_STRATEGY_NUM_INDICES, consts = F16_STRATEGY_LIST_SIZES)]
180+
fn take_fsl_f16_force_piecewise_sequence<const LIST_SIZE: usize>(
181+
bencher: Bencher,
182+
num_indices: usize,
183+
) {
184+
let fsl = create_fsl::<f16>(LIST_SIZE, NUM_LISTS);
93185
let indices = create_random_indices(num_indices, NUM_LISTS);
94-
let indices_array = indices.into_array();
95186

96187
bencher
97-
.with_inputs(|| (&fsl, &indices_array, SESSION.create_execution_ctx()))
188+
.counter(BytesCount::of_many::<f16>(num_indices * LIST_SIZE))
189+
.with_inputs(|| (&fsl, &indices, SESSION.create_execution_ctx()))
98190
.bench_refs(|(array, indices, execution_ctx)| {
99-
array
100-
.clone()
101-
.take(indices.clone())
191+
take_fsl_f16_piecewise_sequence_strategy::<LIST_SIZE>(array, indices)
192+
.into_array()
193+
.execute::<RecursiveCanonical>(execution_ctx)
102194
.unwrap()
195+
});
196+
}
197+
198+
#[divan::bench(args = F16_STRATEGY_NUM_INDICES, consts = F16_STRATEGY_LIST_SIZES)]
199+
fn take_fsl_f16_force_manual_range_copy<const LIST_SIZE: usize>(
200+
bencher: Bencher,
201+
num_indices: usize,
202+
) {
203+
let fsl = create_fsl::<f16>(LIST_SIZE, NUM_LISTS);
204+
let indices = create_random_indices(num_indices, NUM_LISTS);
205+
206+
bencher
207+
.counter(BytesCount::of_many::<f16>(num_indices * LIST_SIZE))
208+
.with_inputs(|| (&fsl, &indices, SESSION.create_execution_ctx()))
209+
.bench_refs(|(array, indices, execution_ctx)| {
210+
take_fsl_f16_manual_range_copy_strategy::<LIST_SIZE>(array, indices, execution_ctx)
211+
.into_array()
103212
.execute::<RecursiveCanonical>(execution_ctx)
104213
.unwrap()
105214
});
106215
}
216+
217+
fn take_fsl_f16_per_index_strategy<const LIST_SIZE: usize, E: IntegerPType>(
218+
array: &FixedSizeListArray,
219+
indices: &Buffer<u64>,
220+
) -> FixedSizeListArray {
221+
let mut element_indices = BufferMut::<E>::with_capacity(indices.len() * LIST_SIZE);
222+
for &idx in indices.as_ref() {
223+
let start = idx as usize * LIST_SIZE;
224+
let end = start + LIST_SIZE;
225+
for element_idx in start..end {
226+
// SAFETY: capacity is exactly `indices.len() * LIST_SIZE`, and this loop appends
227+
// exactly `LIST_SIZE` element indices for each input index.
228+
unsafe { element_indices.push_unchecked(E::from_usize(element_idx).unwrap()) };
229+
}
230+
}
231+
232+
let element_indices =
233+
PrimitiveArray::new(element_indices.freeze(), Validity::NonNullable).into_array();
234+
let elements = array.elements().take(element_indices).unwrap();
235+
236+
// SAFETY: `elements` was built by taking exactly `LIST_SIZE` elements per input index, so its
237+
// length is `indices.len() * LIST_SIZE`; the output is non-nullable by construction.
238+
unsafe {
239+
FixedSizeListArray::new_unchecked(
240+
elements,
241+
LIST_SIZE as u32,
242+
Validity::NonNullable,
243+
indices.len(),
244+
)
245+
}
246+
}
247+
248+
fn take_fsl_f16_piecewise_sequence_strategy<const LIST_SIZE: usize>(
249+
array: &FixedSizeListArray,
250+
indices: &Buffer<u64>,
251+
) -> FixedSizeListArray {
252+
let starts = indices
253+
.as_ref()
254+
.iter()
255+
.map(|&idx| idx * LIST_SIZE as u64)
256+
.collect::<Vec<_>>();
257+
let run_count = starts.len();
258+
let starts = PrimitiveArray::from_iter(starts).into_array();
259+
let lengths = ConstantArray::new(LIST_SIZE as u64, run_count).into_array();
260+
let multipliers = ConstantArray::new(1u64, run_count).into_array();
261+
262+
// SAFETY: benchmark indices are generated in-bounds; lengths and multiplier 1 are
263+
// non-nullable unsigned constants; output length is exactly `indices.len() * LIST_SIZE`.
264+
let element_indices = unsafe {
265+
PiecewiseSequenceArray::new_unchecked(
266+
starts,
267+
lengths,
268+
multipliers,
269+
indices.len() * LIST_SIZE,
270+
)
271+
}
272+
.into_array();
273+
let elements = array.elements().take(element_indices).unwrap();
274+
275+
// SAFETY: each generated run has width `LIST_SIZE`, and there is one run per input index,
276+
// so `elements.len() == indices.len() * LIST_SIZE`.
277+
unsafe {
278+
FixedSizeListArray::new_unchecked(
279+
elements,
280+
LIST_SIZE as u32,
281+
Validity::NonNullable,
282+
indices.len(),
283+
)
284+
}
285+
}
286+
287+
fn take_fsl_f16_manual_range_copy_strategy<const LIST_SIZE: usize>(
288+
array: &FixedSizeListArray,
289+
indices: &Buffer<u64>,
290+
execution_ctx: &mut ExecutionCtx,
291+
) -> FixedSizeListArray {
292+
let elements = array
293+
.elements()
294+
.clone()
295+
.execute::<PrimitiveArray>(execution_ctx)
296+
.unwrap();
297+
let source = elements.as_slice::<f16>();
298+
let mut values = BufferMut::<f16>::with_capacity(indices.len() * LIST_SIZE);
299+
300+
for &idx in indices.as_ref() {
301+
let start = idx as usize * LIST_SIZE;
302+
values.extend_from_slice(&source[start..start + LIST_SIZE]);
303+
}
304+
305+
// SAFETY: the buffer was filled with exactly `LIST_SIZE` copied values per input index, so it
306+
// has the element length required by the constructed FSL.
307+
unsafe {
308+
FixedSizeListArray::new_unchecked(
309+
PrimitiveArray::new(values.freeze(), Validity::NonNullable).into_array(),
310+
LIST_SIZE as u32,
311+
Validity::NonNullable,
312+
indices.len(),
313+
)
314+
}
315+
}
316+
317+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
318+
fn take_fsl_nullable_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
319+
// Create validity with ~10% nulls
320+
let mut rng = StdRng::seed_from_u64(123);
321+
let validity = Validity::from_iter((0..NUM_LISTS).map(|_| rng.random_ratio(9, 10)));
322+
323+
let fsl = create_i64_fsl_with_validity(LIST_SIZE, NUM_LISTS, validity);
324+
bench_take_fsl_random::<i64, LIST_SIZE>(bencher, num_indices, fsl);
325+
}
326+
327+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
328+
fn take_fsl_f16_nullable_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
329+
take_fsl_nullable_random_typed::<f16, LIST_SIZE>(bencher, num_indices);
330+
}
331+
332+
fn take_fsl_nullable_random_typed<T, const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize)
333+
where
334+
T: NativePType + FromPrimitive,
335+
{
336+
// Create validity with ~10% nulls
337+
let mut rng = StdRng::seed_from_u64(123);
338+
let validity = Validity::from_iter((0..NUM_LISTS).map(|_| rng.random_ratio(9, 10)));
339+
340+
let fsl = create_fsl_with_validity::<T>(LIST_SIZE, NUM_LISTS, validity);
341+
bench_take_fsl_random::<T, LIST_SIZE>(bencher, num_indices, fsl);
342+
}

0 commit comments

Comments
 (0)