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
1314use std:: sync:: LazyLock ;
1415
1516use divan:: Bencher ;
17+ use divan:: counter:: BytesCount ;
18+ use num_traits:: FromPrimitive ;
1619use rand:: RngExt ;
1720use rand:: SeedableRng ;
1821use rand:: rngs:: StdRng ;
22+ use vortex_array:: ExecutionCtx ;
1923use vortex_array:: IntoArray ;
2024use vortex_array:: RecursiveCanonical ;
2125use vortex_array:: VortexSessionExecute ;
2226use vortex_array:: array_session;
27+ use vortex_array:: arrays:: ConstantArray ;
2328use 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;
2436use vortex_array:: validity:: Validity ;
2537use vortex_buffer:: Buffer ;
38+ use vortex_buffer:: BufferMut ;
2639use vortex_session:: VortexSession ;
2740
2841fn main ( ) {
@@ -39,18 +52,45 @@ const NUM_LISTS: usize = 500;
3952const 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 ) ]
65105fn 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