1515// specific language governing permissions and limitations
1616// under the License.
1717
18- use arrow:: array:: { ArrayRef , BooleanArray , Int64Array } ;
18+ use arrow:: array:: {
19+ Array , ArrayRef , BooleanArray , Int64Array , ListArray , MapArray , StringArray ,
20+ StructArray ,
21+ } ;
22+ use arrow:: buffer:: { NullBuffer , OffsetBuffer } ;
1923use arrow:: compute:: SortOptions ;
20- use arrow:: datatypes:: { DataType , Field , Int64Type , Schema } ;
21- use arrow:: util:: bench_util:: { create_boolean_array, create_primitive_array} ;
24+ use arrow:: datatypes:: { DataType , Field , Fields , Float64Type , Int64Type , Schema } ;
25+ use arrow:: util:: bench_util:: {
26+ create_boolean_array, create_primitive_array, create_string_array_with_len,
27+ } ;
2228use datafusion_common:: instant:: Instant ;
2329use std:: hint:: black_box;
2430use std:: sync:: Arc ;
@@ -29,14 +35,21 @@ use datafusion_expr::{
2935use datafusion_functions_aggregate:: first_last:: {
3036 FirstValue , LastValue , TrivialFirstValueAccumulator , TrivialLastValueAccumulator ,
3137} ;
38+ use datafusion_functions_aggregate_common:: aggregate:: groups_accumulator:: GroupsAccumulatorAdapter ;
3239use datafusion_physical_expr:: PhysicalSortExpr ;
3340use datafusion_physical_expr:: expressions:: col;
3441
3542use criterion:: { BatchSize , Criterion , criterion_group, criterion_main} ;
3643
37- fn prepare_groups_accumulator ( is_first : bool ) -> Box < dyn GroupsAccumulator > {
44+ /// Build a `GroupsAccumulator` for an arbitrary value type, so the nested-type
45+ /// (`Struct` / `List`) fast paths added for `first_value` / `last_value` can be
46+ /// exercised with the same harness as the primitive ones.
47+ fn prepare_typed_groups_accumulator (
48+ is_first : bool ,
49+ value_type : DataType ,
50+ ) -> Box < dyn GroupsAccumulator > {
3851 let schema = Arc :: new ( Schema :: new ( vec ! [
39- Field :: new( "value" , DataType :: Int64 , true ) ,
52+ Field :: new( "value" , value_type . clone ( ) , true ) ,
4053 Field :: new( "ord" , DataType :: Int64 , true ) ,
4154 ] ) ) ;
4255
@@ -46,11 +59,12 @@ fn prepare_groups_accumulator(is_first: bool) -> Box<dyn GroupsAccumulator> {
4659 options : SortOptions :: default ( ) ,
4760 } ;
4861
49- let value_field: Arc < Field > = Field :: new ( "value" , DataType :: Int64 , true ) . into ( ) ;
50- let accumulator_args = AccumulatorArgs {
62+ let value_field: Arc < Field > = Field :: new ( "value" , value_type. clone ( ) , true ) . into ( ) ;
63+ let value_expr = col ( "value" , & schema) . unwrap ( ) ;
64+ let make_args = || AccumulatorArgs {
5165 return_field : Arc :: clone ( & value_field) ,
5266 schema : & schema,
53- expr_fields : & [ value_field] ,
67+ expr_fields : std :: slice :: from_ref ( & value_field) ,
5468 ignore_nulls : false ,
5569 order_bys : std:: slice:: from_ref ( & sort_expr) ,
5670 is_reversed : false ,
@@ -60,20 +74,81 @@ fn prepare_groups_accumulator(is_first: bool) -> Box<dyn GroupsAccumulator> {
6074 "LAST_VALUE(value ORDER BY ord)"
6175 } ,
6276 is_distinct : false ,
63- exprs : & [ col ( "value" , & schema ) . unwrap ( ) ] ,
77+ exprs : std :: slice :: from_ref ( & value_expr ) ,
6478 } ;
6579
80+ // Mirror the planner: use the native GroupsAccumulator when this value type
81+ // is supported and otherwise fall back to a GroupsAccumulatorAdapter around
82+ // one per-group Accumulator. Deciding with `groups_accumulator_supported`
83+ // (rather than catching `create_groups_accumulator` errors) keeps genuine
84+ // construction failures loud. The same case then runs the fallback on a
85+ // build without native nested support and the native path on one with it,
86+ // so a before/after benchmark run surfaces the win directly.
87+ let supported = if is_first {
88+ FirstValue :: new ( ) . groups_accumulator_supported ( make_args ( ) )
89+ } else {
90+ LastValue :: new ( ) . groups_accumulator_supported ( make_args ( ) )
91+ } ;
92+ if !supported {
93+ return build_fallback_adapter ( is_first, value_type) ;
94+ }
6695 if is_first {
6796 FirstValue :: new ( )
68- . create_groups_accumulator ( accumulator_args )
97+ . create_groups_accumulator ( make_args ( ) )
6998 . unwrap ( )
7099 } else {
71100 LastValue :: new ( )
72- . create_groups_accumulator ( accumulator_args )
101+ . create_groups_accumulator ( make_args ( ) )
73102 . unwrap ( )
74103 }
75104}
76105
106+ /// Build the *fallback* grouped accumulator for a value type: a
107+ /// `GroupsAccumulatorAdapter` wrapping one per-group `Accumulator`. This is
108+ /// exactly what nested value types (`List` / `Struct` / `Map`) used before
109+ /// they gained a native `GroupsAccumulator`, and it is what the planner still
110+ /// selects when `groups_accumulator_supported` returns `false`. Benching this
111+ /// side by side with `prepare_typed_groups_accumulator` (the native path)
112+ /// shows the win from the native `GroupsAccumulator`.
113+ fn build_fallback_adapter (
114+ is_first : bool ,
115+ value_type : DataType ,
116+ ) -> Box < dyn GroupsAccumulator > {
117+ Box :: new ( GroupsAccumulatorAdapter :: new ( move || {
118+ let schema = Arc :: new ( Schema :: new ( vec ! [
119+ Field :: new( "value" , value_type. clone( ) , true ) ,
120+ Field :: new( "ord" , DataType :: Int64 , true ) ,
121+ ] ) ) ;
122+ let sort_expr = PhysicalSortExpr {
123+ expr : col ( "ord" , & schema) ?,
124+ options : SortOptions :: default ( ) ,
125+ } ;
126+ let value_field: Arc < Field > =
127+ Field :: new ( "value" , value_type. clone ( ) , true ) . into ( ) ;
128+ let value_expr = col ( "value" , & schema) ?;
129+ let accumulator_args = AccumulatorArgs {
130+ return_field : Arc :: clone ( & value_field) ,
131+ schema : & schema,
132+ expr_fields : std:: slice:: from_ref ( & value_field) ,
133+ ignore_nulls : false ,
134+ order_bys : std:: slice:: from_ref ( & sort_expr) ,
135+ is_reversed : false ,
136+ name : if is_first {
137+ "FIRST_VALUE(value ORDER BY ord)"
138+ } else {
139+ "LAST_VALUE(value ORDER BY ord)"
140+ } ,
141+ is_distinct : false ,
142+ exprs : std:: slice:: from_ref ( & value_expr) ,
143+ } ;
144+ if is_first {
145+ FirstValue :: new ( ) . accumulator ( accumulator_args)
146+ } else {
147+ LastValue :: new ( ) . accumulator ( accumulator_args)
148+ }
149+ } ) )
150+ }
151+
77152fn create_trivial_accumulator (
78153 is_first : bool ,
79154 ignore_nulls : bool ,
@@ -104,11 +179,13 @@ fn evaluate_bench(
104179) {
105180 let n = values. len ( ) ;
106181 let group_indices: Vec < usize > = ( 0 ..n) . map ( |i| i % num_groups) . collect ( ) ;
182+ let value_type = values. data_type ( ) . clone ( ) ;
107183
108184 c. bench_function ( name, |b| {
109185 b. iter_batched (
110186 || {
111- let mut accumulator = prepare_groups_accumulator ( is_first) ;
187+ let mut accumulator =
188+ prepare_typed_groups_accumulator ( is_first, value_type. clone ( ) ) ;
112189 accumulator
113190 . update_batch (
114191 & [ Arc :: clone ( & values) , Arc :: clone ( & ord) ] ,
@@ -139,6 +216,7 @@ fn update_bench(
139216) {
140217 let n = values. len ( ) ;
141218 let group_indices: Vec < usize > = ( 0 ..n) . map ( |i| i % num_groups) . collect ( ) ;
219+ let value_type = values. data_type ( ) . clone ( ) ;
142220
143221 // Initialize with worst-case ordering so update_batch forces rows comparison for all groups.
144222 let worst_ord: ArrayRef = Arc :: new ( Int64Array :: from ( vec ! [
@@ -153,7 +231,8 @@ fn update_bench(
153231 c. bench_function ( name, |b| {
154232 b. iter_batched (
155233 || {
156- let mut accumulator = prepare_groups_accumulator ( is_first) ;
234+ let mut accumulator =
235+ prepare_typed_groups_accumulator ( is_first, value_type. clone ( ) ) ;
157236 accumulator
158237 . update_batch (
159238 & [ Arc :: clone ( & values) , Arc :: clone ( & worst_ord) ] ,
@@ -197,6 +276,7 @@ fn merge_bench(
197276 let n = values. len ( ) ;
198277 let group_indices: Vec < usize > = ( 0 ..n) . map ( |i| i % num_groups) . collect ( ) ;
199278 let is_set: ArrayRef = Arc :: new ( BooleanArray :: from ( vec ! [ true ; n] ) ) ;
279+ let value_type = values. data_type ( ) . clone ( ) ;
200280
201281 // Initialize with worst-case ordering so update_batch forces rows comparison for all groups.
202282 let worst_ord: ArrayRef = Arc :: new ( Int64Array :: from ( vec ! [
@@ -212,7 +292,8 @@ fn merge_bench(
212292 b. iter_batched (
213293 || {
214294 // Prebuild accumulator
215- let mut accumulator = prepare_groups_accumulator ( is_first) ;
295+ let mut accumulator =
296+ prepare_typed_groups_accumulator ( is_first, value_type. clone ( ) ) ;
216297 accumulator
217298 . update_batch (
218299 & [ Arc :: clone ( & values) , Arc :: clone ( & worst_ord) ] ,
@@ -270,6 +351,167 @@ fn trivial_update_bench(
270351 } ) ;
271352}
272353
354+ /// A top-level validity buffer with roughly `null_density` nulls, so the
355+ /// generated nested arrays have null *values* (not just null inner
356+ /// fields/elements) — matching the `nulls={pct}%` semantics of the primitive
357+ /// benchmarks, where the value itself is null. Returns `None` at 0% so the
358+ /// arrays stay fully valid. Derived from arrow's own null generator for a
359+ /// deterministic, density-accurate pattern.
360+ fn top_level_nulls ( n : usize , null_density : f32 ) -> Option < NullBuffer > {
361+ create_primitive_array :: < Int64Type > ( n, null_density)
362+ . nulls ( )
363+ . cloned ( )
364+ }
365+
366+ /// A 3-field struct value column `Struct<Int64, Utf8, Float64>`. `null_density`
367+ /// controls both the struct-level null values and the inner field nulls.
368+ fn create_struct_array ( n : usize , null_density : f32 ) -> ArrayRef {
369+ let a = Arc :: new ( create_primitive_array :: < Int64Type > ( n, null_density) ) as ArrayRef ;
370+ let b =
371+ Arc :: new ( create_string_array_with_len :: < i32 > ( n, null_density, 16 ) ) as ArrayRef ;
372+ let d = Arc :: new ( create_primitive_array :: < Float64Type > ( n, null_density) ) as ArrayRef ;
373+ let fields = Fields :: from ( vec ! [
374+ Field :: new( "c0" , DataType :: Int64 , true ) ,
375+ Field :: new( "c1" , DataType :: Utf8 , true ) ,
376+ Field :: new( "c2" , DataType :: Float64 , true ) ,
377+ ] ) ;
378+ Arc :: new ( StructArray :: new (
379+ fields,
380+ vec ! [ a, b, d] ,
381+ top_level_nulls ( n, null_density) ,
382+ ) )
383+ }
384+
385+ /// A `List<Int64>` value column with fixed-size lists of `list_len` elements.
386+ fn create_list_array ( n : usize , list_len : usize , null_density : f32 ) -> ArrayRef {
387+ let child = Arc :: new ( create_primitive_array :: < Int64Type > (
388+ n * list_len,
389+ null_density,
390+ ) ) as ArrayRef ;
391+ let offsets = OffsetBuffer :: from_lengths ( std:: iter:: repeat_n ( list_len, n) ) ;
392+ let field = Arc :: new ( Field :: new_list_field ( DataType :: Int64 , true ) ) ;
393+ Arc :: new ( ListArray :: new (
394+ field,
395+ offsets,
396+ child,
397+ top_level_nulls ( n, null_density) ,
398+ ) )
399+ }
400+
401+ /// A `Map<Utf8, Int64>` value column with `entries_per_row` entries per row.
402+ /// Values carry `null_density` nulls (keys are never null), matching the null
403+ /// treatment of the struct / list generators.
404+ fn create_map_array ( n : usize , entries_per_row : usize , null_density : f32 ) -> ArrayRef {
405+ let total = n * entries_per_row;
406+ let values =
407+ Arc :: new ( create_primitive_array :: < Int64Type > ( total, null_density) ) as ArrayRef ;
408+ let keys = Arc :: new ( StringArray :: from_iter_values (
409+ ( 0 ..total) . map ( |idx| format ! ( "k{}" , idx % entries_per_row) ) ,
410+ ) ) as ArrayRef ;
411+ let entry_fields = Fields :: from ( vec ! [
412+ Field :: new( "keys" , DataType :: Utf8 , false ) ,
413+ Field :: new( "values" , DataType :: Int64 , true ) ,
414+ ] ) ;
415+ let entries = StructArray :: new ( entry_fields. clone ( ) , vec ! [ keys, values] , None ) ;
416+ let offsets = OffsetBuffer :: from_lengths ( std:: iter:: repeat_n ( entries_per_row, n) ) ;
417+ let map_field =
418+ Arc :: new ( Field :: new ( "entries" , DataType :: Struct ( entry_fields) , false ) ) ;
419+ Arc :: new ( MapArray :: new (
420+ map_field,
421+ offsets,
422+ entries,
423+ top_level_nulls ( n, null_density) ,
424+ false ,
425+ ) )
426+ }
427+
428+ /// A composite `List<Struct<a: Int64, b: Utf8>>` column — a list whose
429+ /// elements are structs (the "array of records" shape). Exercises the
430+ /// nested-within-nested case, which the generic value-state path must also
431+ /// handle.
432+ fn create_list_of_struct_array ( n : usize , list_len : usize , null_density : f32 ) -> ArrayRef {
433+ let total = n * list_len;
434+ let a =
435+ Arc :: new ( create_primitive_array :: < Int64Type > ( total, null_density) ) as ArrayRef ;
436+ let b =
437+ Arc :: new ( create_string_array_with_len :: < i32 > ( total, null_density, 8 ) ) as ArrayRef ;
438+ let struct_fields = Fields :: from ( vec ! [
439+ Field :: new( "a" , DataType :: Int64 , true ) ,
440+ Field :: new( "b" , DataType :: Utf8 , true ) ,
441+ ] ) ;
442+ let child =
443+ Arc :: new ( StructArray :: new ( struct_fields. clone ( ) , vec ! [ a, b] , None ) ) as ArrayRef ;
444+ let offsets = OffsetBuffer :: from_lengths ( std:: iter:: repeat_n ( list_len, n) ) ;
445+ let list_field =
446+ Arc :: new ( Field :: new_list_field ( DataType :: Struct ( struct_fields) , true ) ) ;
447+ Arc :: new ( ListArray :: new (
448+ list_field,
449+ offsets,
450+ child,
451+ top_level_nulls ( n, null_density) ,
452+ ) )
453+ }
454+
455+ fn first_last_nested_benchmark ( c : & mut Criterion ) {
456+ const N : usize = 65536 ;
457+ const NUM_GROUPS : usize = 1024 ;
458+
459+ let ord = Arc :: new ( create_primitive_array :: < Int64Type > ( N , 0.0 ) ) as ArrayRef ;
460+
461+ for pct in [ 0 , 90 ] {
462+ let null_density = ( pct as f32 ) / 100.0 ;
463+
464+ // One column per nested value type. Each type gets the same treatment
465+ // as the primitive first_value / last_value benchmarks: update and
466+ // merge (both first and last) plus evaluate, at 0% and 90% nulls. On a
467+ // build without native nested support these run the fallback adapter;
468+ // with this PR they run the native GroupsAccumulator, so the benchmark
469+ // bot's before/after diff shows the win per type.
470+ let columns: [ ( & str , ArrayRef ) ; 4 ] = [
471+ ( "struct(i64,utf8,f64)" , create_struct_array ( N , null_density) ) ,
472+ ( "list<i64>[4]" , create_list_array ( N , 4 , null_density) ) ,
473+ ( "map<utf8,i64>" , create_map_array ( N , 4 , null_density) ) ,
474+ (
475+ "list<struct(i64,utf8)>[4]" ,
476+ create_list_of_struct_array ( N , 4 , null_density) ,
477+ ) ,
478+ ] ;
479+
480+ for ( type_label, values) in columns {
481+ for ( fn_label, is_first) in [ ( "first_value" , true ) , ( "last_value" , false ) ] {
482+ update_bench (
483+ c,
484+ is_first,
485+ & format ! ( "{fn_label} update_bench {type_label} nulls={pct}%" ) ,
486+ values. clone ( ) ,
487+ ord. clone ( ) ,
488+ None ,
489+ NUM_GROUPS ,
490+ ) ;
491+ merge_bench (
492+ c,
493+ is_first,
494+ & format ! ( "{fn_label} merge_bench {type_label} nulls={pct}%" ) ,
495+ values. clone ( ) ,
496+ ord. clone ( ) ,
497+ None ,
498+ NUM_GROUPS ,
499+ ) ;
500+ }
501+ evaluate_bench (
502+ c,
503+ true ,
504+ EmitTo :: All ,
505+ & format ! ( "first_value evaluate_bench {type_label} nulls={pct}%, all" ) ,
506+ values. clone ( ) ,
507+ ord. clone ( ) ,
508+ None ,
509+ NUM_GROUPS ,
510+ ) ;
511+ }
512+ }
513+ }
514+
273515fn first_last_benchmark ( c : & mut Criterion ) {
274516 const N : usize = 65536 ;
275517 const NUM_GROUPS : usize = 1024 ;
@@ -354,5 +596,5 @@ fn first_last_benchmark(c: &mut Criterion) {
354596 }
355597}
356598
357- criterion_group ! ( benches, first_last_benchmark) ;
599+ criterion_group ! ( benches, first_last_benchmark, first_last_nested_benchmark ) ;
358600criterion_main ! ( benches) ;
0 commit comments