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:: 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;
2436use vortex_array:: validity:: Validity ;
2537use vortex_buffer:: Buffer ;
38+ use vortex_buffer:: BufferMut ;
2639use vortex_session:: VortexSession ;
2740
2841fn main ( ) {
@@ -35,16 +48,25 @@ static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
3548/// Number of lists in the source array.
3649const 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
0 commit comments