Skip to content

Commit 2be8f97

Browse files
committed
Port FSL take benchmarks to PiecewiseSequential
Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent 98a9180 commit 2be8f97

3 files changed

Lines changed: 209 additions & 9 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: 207 additions & 9 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::PiecewiseSequentialArray;
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() {
@@ -35,16 +48,25 @@ static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
3548
/// Number of lists in the source array.
3649
const NUM_LISTS: usize = 500;
3750

38-
/// Number of indices to take.
39-
const NUM_INDICES: &[usize] = &[100, 1_000];
51+
/// Number of indices to take. This keeps even the widest, longest cases below one millisecond in
52+
/// CodSpeed's instruction-count simulation.
53+
const NUM_INDICES: &[usize] = &[10];
4054

4155
/// Fixed size list lengths (elements per list).
42-
const LIST_SIZES: &[usize] = &[16, 64, 256, 1024, 4096];
56+
const LIST_SIZES: &[usize] = &[16, 64, 128, 256, 512, 1024, 2048, 4096];
57+
58+
/// F16 list lengths for isolating the per-index, piecewise, and manual range-copy strategies.
59+
const F16_STRATEGY_LIST_SIZES: &[usize] = &[1, 2, 4, 8, 16, 64, 128, 256, 512, 1024, 2048, 4096];
4360

4461
/// Creates a FixedSizeListArray with the given list size and number of lists.
45-
fn create_fsl(list_size: usize, num_lists: usize) -> FixedSizeListArray {
62+
fn create_fsl<T>(list_size: usize, num_lists: usize) -> FixedSizeListArray
63+
where
64+
T: NativePType + FromPrimitive,
65+
{
4666
let total_elements = list_size * num_lists;
47-
let elements: Buffer<i64> = (0..total_elements as i64).collect();
67+
let elements: Buffer<T> = (0..total_elements)
68+
.map(|idx| T::from_u16((idx % 251) as u16).unwrap())
69+
.collect();
4870
FixedSizeListArray::new(
4971
elements.into_array(),
5072
list_size as u32,
@@ -62,12 +84,35 @@ fn create_random_indices(num_indices: usize, max_index: usize) -> Buffer<u64> {
6284
}
6385

6486
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
65-
fn take_fsl_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
66-
let fsl = create_fsl(LIST_SIZE, NUM_LISTS);
87+
fn take_fsl_f16_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
88+
take_fsl_random::<f16, LIST_SIZE>(bencher, num_indices);
89+
}
90+
91+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
92+
fn take_fsl_u8_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
93+
take_fsl_random::<u8, LIST_SIZE>(bencher, num_indices);
94+
}
95+
96+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
97+
fn take_fsl_u32_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
98+
take_fsl_random::<u32, LIST_SIZE>(bencher, num_indices);
99+
}
100+
101+
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
102+
fn take_fsl_u64_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
103+
take_fsl_random::<u64, LIST_SIZE>(bencher, num_indices);
104+
}
105+
106+
fn take_fsl_random<T, const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize)
107+
where
108+
T: NativePType + FromPrimitive,
109+
{
110+
let fsl = create_fsl::<T>(LIST_SIZE, NUM_LISTS);
67111
let indices = create_random_indices(num_indices, NUM_LISTS);
68112
let indices_array = indices.into_array();
69113

70114
bencher
115+
.counter(BytesCount::of_many::<T>(num_indices * LIST_SIZE))
71116
.with_inputs(|| (&fsl, &indices_array, SESSION.create_execution_ctx()))
72117
.bench_refs(|(array, indices, execution_ctx)| {
73118
array
@@ -79,10 +124,162 @@ fn take_fsl_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize)
79124
});
80125
}
81126

127+
#[divan::bench(args = NUM_INDICES, consts = F16_STRATEGY_LIST_SIZES)]
128+
fn take_fsl_f16_force_per_index<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
129+
let fsl = create_fsl::<f16>(LIST_SIZE, NUM_LISTS);
130+
let indices = create_random_indices(num_indices, NUM_LISTS);
131+
132+
bencher
133+
.counter(BytesCount::of_many::<f16>(num_indices * LIST_SIZE))
134+
.with_inputs(|| (&fsl, &indices, SESSION.create_execution_ctx()))
135+
.bench_refs(|(array, indices, execution_ctx)| {
136+
match_smallest_offset_type!(array.elements().len(), |E| {
137+
take_fsl_f16_per_index_strategy::<LIST_SIZE, E>(array, indices)
138+
})
139+
.into_array()
140+
.execute::<RecursiveCanonical>(execution_ctx)
141+
.unwrap()
142+
});
143+
}
144+
145+
#[divan::bench(args = NUM_INDICES, consts = F16_STRATEGY_LIST_SIZES)]
146+
fn take_fsl_f16_force_piecewise_sequential<const LIST_SIZE: usize>(
147+
bencher: Bencher,
148+
num_indices: usize,
149+
) {
150+
let fsl = create_fsl::<f16>(LIST_SIZE, NUM_LISTS);
151+
let indices = create_random_indices(num_indices, NUM_LISTS);
152+
153+
bencher
154+
.counter(BytesCount::of_many::<f16>(num_indices * LIST_SIZE))
155+
.with_inputs(|| (&fsl, &indices, SESSION.create_execution_ctx()))
156+
.bench_refs(|(array, indices, execution_ctx)| {
157+
take_fsl_f16_piecewise_sequential_strategy::<LIST_SIZE>(array, indices)
158+
.into_array()
159+
.execute::<RecursiveCanonical>(execution_ctx)
160+
.unwrap()
161+
});
162+
}
163+
164+
#[divan::bench(args = NUM_INDICES, consts = F16_STRATEGY_LIST_SIZES)]
165+
fn take_fsl_f16_force_manual_range_copy<const LIST_SIZE: usize>(
166+
bencher: Bencher,
167+
num_indices: usize,
168+
) {
169+
let fsl = create_fsl::<f16>(LIST_SIZE, NUM_LISTS);
170+
let indices = create_random_indices(num_indices, NUM_LISTS);
171+
172+
bencher
173+
.counter(BytesCount::of_many::<f16>(num_indices * LIST_SIZE))
174+
.with_inputs(|| (&fsl, &indices, SESSION.create_execution_ctx()))
175+
.bench_refs(|(array, indices, execution_ctx)| {
176+
take_fsl_f16_manual_range_copy_strategy::<LIST_SIZE>(array, indices, execution_ctx)
177+
.into_array()
178+
.execute::<RecursiveCanonical>(execution_ctx)
179+
.unwrap()
180+
});
181+
}
182+
183+
fn take_fsl_f16_per_index_strategy<const LIST_SIZE: usize, E: IntegerPType>(
184+
array: &FixedSizeListArray,
185+
indices: &Buffer<u64>,
186+
) -> FixedSizeListArray {
187+
let mut element_indices = BufferMut::<E>::with_capacity(indices.len() * LIST_SIZE);
188+
for &idx in indices.as_ref() {
189+
let start = idx as usize * LIST_SIZE;
190+
let end = start + LIST_SIZE;
191+
for element_idx in start..end {
192+
// SAFETY: capacity is exactly `indices.len() * LIST_SIZE`, and this loop appends
193+
// exactly `LIST_SIZE` element indices for each input index.
194+
unsafe { element_indices.push_unchecked(E::from_usize(element_idx).unwrap()) };
195+
}
196+
}
197+
198+
let element_indices =
199+
PrimitiveArray::new(element_indices.freeze(), Validity::NonNullable).into_array();
200+
let elements = array.elements().take(element_indices).unwrap();
201+
202+
// SAFETY: `elements` was built by taking exactly `LIST_SIZE` elements per input index, so its
203+
// length is `indices.len() * LIST_SIZE`; the output is non-nullable by construction.
204+
unsafe {
205+
FixedSizeListArray::new_unchecked(
206+
elements,
207+
LIST_SIZE as u32,
208+
Validity::NonNullable,
209+
indices.len(),
210+
)
211+
}
212+
}
213+
214+
fn take_fsl_f16_piecewise_sequential_strategy<const LIST_SIZE: usize>(
215+
array: &FixedSizeListArray,
216+
indices: &Buffer<u64>,
217+
) -> FixedSizeListArray {
218+
let starts = indices
219+
.as_ref()
220+
.iter()
221+
.map(|&idx| idx * LIST_SIZE as u64)
222+
.collect::<Vec<_>>();
223+
let run_count = starts.len();
224+
let starts = PrimitiveArray::from_iter(starts).into_array();
225+
let lengths = ConstantArray::new(LIST_SIZE as u64, run_count).into_array();
226+
227+
// SAFETY: benchmark indices are generated in-bounds, lengths is a non-nullable unsigned
228+
// constant, and output length is exactly `indices.len() * LIST_SIZE`.
229+
let element_indices = unsafe {
230+
PiecewiseSequentialArray::new_unchecked(starts, lengths, indices.len() * LIST_SIZE)
231+
}
232+
.into_array();
233+
let elements = array.elements().take(element_indices).unwrap();
234+
235+
// SAFETY: each generated run has width `LIST_SIZE`, and there is one run per input index,
236+
// so `elements.len() == indices.len() * LIST_SIZE`.
237+
unsafe {
238+
FixedSizeListArray::new_unchecked(
239+
elements,
240+
LIST_SIZE as u32,
241+
Validity::NonNullable,
242+
indices.len(),
243+
)
244+
}
245+
}
246+
247+
fn take_fsl_f16_manual_range_copy_strategy<const LIST_SIZE: usize>(
248+
array: &FixedSizeListArray,
249+
indices: &Buffer<u64>,
250+
execution_ctx: &mut ExecutionCtx,
251+
) -> FixedSizeListArray {
252+
let elements = array
253+
.elements()
254+
.clone()
255+
.execute::<PrimitiveArray>(execution_ctx)
256+
.unwrap();
257+
let source = elements.as_slice::<f16>();
258+
let mut values = BufferMut::<f16>::with_capacity(indices.len() * LIST_SIZE);
259+
260+
for &idx in indices.as_ref() {
261+
let start = idx as usize * LIST_SIZE;
262+
values.extend_from_slice(&source[start..start + LIST_SIZE]);
263+
}
264+
265+
// SAFETY: the buffer was filled with exactly `LIST_SIZE` copied values per input index, so it
266+
// has the element length required by the constructed FSL.
267+
unsafe {
268+
FixedSizeListArray::new_unchecked(
269+
PrimitiveArray::new(values.freeze(), Validity::NonNullable).into_array(),
270+
LIST_SIZE as u32,
271+
Validity::NonNullable,
272+
indices.len(),
273+
)
274+
}
275+
}
276+
82277
#[divan::bench(args = NUM_INDICES, consts = LIST_SIZES)]
83-
fn take_fsl_nullable_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
278+
fn take_fsl_f16_nullable_random<const LIST_SIZE: usize>(bencher: Bencher, num_indices: usize) {
84279
let total_elements = LIST_SIZE * NUM_LISTS;
85-
let elements: Buffer<i64> = (0..total_elements as i64).collect();
280+
let elements: Buffer<f16> = (0..total_elements)
281+
.map(|idx| f16::from_u16((idx % 251) as u16).unwrap())
282+
.collect();
86283

87284
// Create validity with ~10% nulls
88285
let mut rng = StdRng::seed_from_u64(123);
@@ -94,6 +291,7 @@ fn take_fsl_nullable_random<const LIST_SIZE: usize>(bencher: Bencher, num_indice
94291
let indices_array = indices.into_array();
95292

96293
bencher
294+
.counter(BytesCount::of_many::<f16>(num_indices * LIST_SIZE))
97295
.with_inputs(|| (&fsl, &indices_array, SESSION.create_execution_ctx()))
98296
.bench_refs(|(array, indices, execution_ctx)| {
99297
array

vortex-bench/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ regex = { workspace = true }
5454
reqwest = { workspace = true, features = ["stream"] }
5555
serde = { workspace = true, features = ["derive"] }
5656
serde_json = { workspace = true }
57+
sha2 = { workspace = true }
5758
spatialbench = { workspace = true }
5859
spatialbench-arrow = { workspace = true }
5960
spatialbench-parquet = { workspace = true }

0 commit comments

Comments
 (0)