@@ -661,37 +661,54 @@ impl ExternalSorter {
661661
662662 // NOTE: we use stable merge sort for longer keys due to less comparison
663663 let ( keys, batch) = self . prune_sort_keys_from_batch . prune ( batch) ?;
664+ // On the fast path the batch is already sorted (and limited) above, so
665+ // the key rows from `prune` are in final order — we can append them
666+ // sequentially and skip allocating an identity `Vec<u32>`. The non-fast
667+ // path needs an actual sorted-index vector for both key appending and
668+ // the `take_batch` below.
664669 let sorted_indices = if is_fast_path {
665- // the batch was already sorted (and limited) above, so the key rows
666- // produced by `prune` are in final order — use identity indices.
667- ( 0 ..keys. num_rows ( ) as u32 ) . collect :: < Vec < _ > > ( )
670+ None
668671 } else if keys. size ( ) / keys. num_rows ( ) <= 8 {
669- ( 0 ..keys. num_rows ( ) as u32 )
670- . sorted_unstable_by_key ( |& row_idx| unsafe {
671- // safety: bypass boundary and lifetime checking
672- std:: mem:: transmute :: < _ , & ' static [ u8 ] > (
673- keys. row_unchecked ( row_idx as usize ) . as_ref ( ) ,
674- )
675- } )
676- . take ( self . limit )
677- . collect :: < Vec < _ > > ( )
672+ Some (
673+ ( 0 ..keys. num_rows ( ) as u32 )
674+ . sorted_unstable_by_key ( |& row_idx| unsafe {
675+ // safety: bypass boundary and lifetime checking
676+ std:: mem:: transmute :: < _ , & ' static [ u8 ] > (
677+ keys. row_unchecked ( row_idx as usize ) . as_ref ( ) ,
678+ )
679+ } )
680+ . take ( self . limit )
681+ . collect :: < Vec < _ > > ( ) ,
682+ )
678683 } else {
679- ( 0 ..keys. num_rows ( ) as u32 )
680- . sorted_by_key ( |& row_idx| unsafe {
681- // safety: bypass boundary and lifetime checking
682- std:: mem:: transmute :: < _ , & ' static [ u8 ] > (
683- keys. row_unchecked ( row_idx as usize ) . as_ref ( ) ,
684- )
685- } )
686- . take ( self . limit )
687- . collect :: < Vec < _ > > ( )
684+ Some (
685+ ( 0 ..keys. num_rows ( ) as u32 )
686+ . sorted_by_key ( |& row_idx| unsafe {
687+ // safety: bypass boundary and lifetime checking
688+ std:: mem:: transmute :: < _ , & ' static [ u8 ] > (
689+ keys. row_unchecked ( row_idx as usize ) . as_ref ( ) ,
690+ )
691+ } )
692+ . take ( self . limit )
693+ . collect :: < Vec < _ > > ( ) ,
694+ )
688695 } ;
689696
690697 // build keys
691698 let mut key_collector = InMemRowsKeyCollector :: default ( ) ;
692699 key_collector. reserve ( keys. num_rows ( ) , keys. size ( ) ) ;
693- for & row_idx in & sorted_indices {
694- key_collector. add_key ( keys. row ( row_idx as usize ) . as_ref ( ) ) ;
700+ match sorted_indices. as_deref ( ) {
701+ Some ( indices) => {
702+ for & row_idx in indices {
703+ key_collector. add_key ( keys. row ( row_idx as usize ) . as_ref ( ) ) ;
704+ }
705+ }
706+ // fast path: rows are already in final order, append sequentially.
707+ None => {
708+ for row_idx in 0 ..keys. num_rows ( ) as u32 {
709+ key_collector. add_key ( keys. row ( row_idx as usize ) . as_ref ( ) ) ;
710+ }
711+ }
695712 }
696713 key_collector. freeze ( ) ;
697714
@@ -702,7 +719,7 @@ impl ExternalSorter {
702719 // avoid an extra `take_batch` copy.
703720 batch
704721 } else {
705- take_batch ( batch, sorted_indices) ?
722+ take_batch ( batch, sorted_indices. expect ( "non-fast-path indices" ) ) ?
706723 }
707724 } else {
708725 create_zero_column_batch ( batch. num_rows ( ) )
@@ -1262,13 +1279,13 @@ impl PruneSortKeysFromBatch {
12621279 descending : key. sort_expr . options . descending ,
12631280 nulls_first : key. sort_expr . options . nulls_first ,
12641281 } ;
1265- let indices = sort_to_indices ( & array , Some ( options ) , None ) ? ;
1266- let indices = indices
1267- . values ( )
1268- . iter ( )
1269- . copied ( )
1270- . take ( limit )
1271- . collect :: < Vec < _ > > ( ) ;
1282+ // Pass `limit` straight to ` sort_to_indices` so Arrow uses partial
1283+ // sorting (it only emits the top-`limit` indices); when `limit`
1284+ // covers the whole batch Arrow internally falls back to a full sort,
1285+ // so this is never worse than `None`. The returned `UInt32Array` is
1286+ // handed directly to `take_batch` (which accepts a `PrimitiveArray` )
1287+ // instead of being copied through an intermediate `Vec<u32>`.
1288+ let indices = sort_to_indices ( & array , Some ( options ) , Some ( limit ) ) ? ;
12721289 take_batch ( batch, indices)
12731290 }
12741291
@@ -1718,6 +1735,18 @@ mod fuzztest {
17181735 }
17191736
17201737 async fn bench_sort_repeat ( repeat : usize , mem : usize , use_auron : bool ) -> Result < ( usize , f64 ) > {
1738+ bench_sort_repeat_with_limit ( repeat, mem, use_auron, None ) . await
1739+ }
1740+
1741+ /// Variant of `bench_sort_repeat` that can pass a Top-N `limit` to the
1742+ /// Auron `SortExec`, so the `sort_to_indices(limit)` partial-sort path
1743+ /// can be exercised (vs. a full sort when `limit` is `None`).
1744+ async fn bench_sort_repeat_with_limit (
1745+ repeat : usize ,
1746+ mem : usize ,
1747+ use_auron : bool ,
1748+ limit : Option < usize > ,
1749+ ) -> Result < ( usize , f64 ) > {
17211750 MemManager :: init ( mem) ;
17221751 let session_ctx =
17231752 SessionContext :: new_with_config ( SessionConfig :: new ( ) . with_batch_size ( 10000 ) ) ;
@@ -1736,12 +1765,15 @@ mod fuzztest {
17361765 None ,
17371766 ) ?) ;
17381767 let sort: Arc < dyn ExecutionPlan > = if use_auron {
1739- Arc :: new ( SortExec :: new ( input, sort_exprs. clone ( ) , None , 0 ) )
1768+ Arc :: new ( SortExec :: new ( input, sort_exprs. clone ( ) , limit , 0 ) )
17401769 } else {
1741- Arc :: new ( datafusion:: physical_plan:: sorts:: sort:: SortExec :: new (
1742- LexOrdering :: new ( sort_exprs. iter ( ) . cloned ( ) ) . expect ( "invalid sort exprs" ) ,
1743- input,
1744- ) )
1770+ Arc :: new (
1771+ datafusion:: physical_plan:: sorts:: sort:: SortExec :: new (
1772+ LexOrdering :: new ( sort_exprs. iter ( ) . cloned ( ) ) . expect ( "invalid sort exprs" ) ,
1773+ input,
1774+ )
1775+ . with_fetch ( limit) ,
1776+ )
17451777 } ;
17461778 let start = Instant :: now ( ) ;
17471779 let output = datafusion:: physical_plan:: collect ( sort. clone ( ) , task_ctx. clone ( ) ) . await ?;
@@ -1767,18 +1799,39 @@ mod fuzztest {
17671799 Ok ( ( ) )
17681800 }
17691801
1802+ #[ tokio:: test]
1803+ #[ ignore = "manual benchmark" ]
1804+ async fn bench_native_sort_topn_limit_in_mem ( ) -> Result < ( ) > {
1805+ // Top-N scenario: limit=10000 << num_rows=1M, so the primitive fast
1806+ // path's `sort_to_indices(Some(limit))` partial sort should be cheaper
1807+ // than a full sort. Compares Auron vs DataFusion, both with limit.
1808+ let limit = 10_000usize ;
1809+ for repeat in [ 1usize , 100 ] {
1810+ let ( _, elapsed_auron) =
1811+ bench_sort_repeat_with_limit ( repeat, 1_000_000_000 , true , Some ( limit) ) . await ?;
1812+ let ( _, elapsed_df) =
1813+ bench_sort_repeat_with_limit ( repeat, 1_000_000_000 , false , Some ( limit) ) . await ?;
1814+ eprintln ! (
1815+ "[sort top-N limit={limit}] repeat={repeat:>3}, auron={elapsed_auron:.3}s, datafusion={elapsed_df:.3}s, speedup(df/auron)={:.2}x" ,
1816+ elapsed_auron / elapsed_df
1817+ ) ;
1818+ }
1819+ Ok ( ( ) )
1820+ }
1821+
17701822 #[ tokio:: test]
17711823 #[ ignore = "manual benchmark" ]
17721824 async fn bench_native_sort_varying_repeat_external ( ) -> Result < ( ) > {
1825+ // NOTE: this measures Auron's external (spilling) sort in isolation.
1826+ // DataFusion's native SortExec has no spilling path, so under the same
1827+ // 2 MB `MemManager` limit Auron spills while DataFusion keeps sorting
1828+ // fully in memory — the two are not comparable. We therefore report
1829+ // Auron's spill cost on its own; use the in-mem group for a fair
1830+ // Auron-vs-DataFusion comparison (both stay in memory at 1 GB).
17731831 for repeat in [ 1usize , 4 , 20 , 100 ] {
17741832 let ( _, elapsed_auron) = bench_sort_repeat ( repeat, 2_000_000 , true ) . await ?;
1775- let ( _, elapsed_df) = bench_sort_repeat ( repeat, 2_000_000 , false ) . await ?;
17761833 eprintln ! (
1777- "[sort external] repeat={:>3}, auron={:.3}s, datafusion={:.3}s, speedup(df/auron)={:.2}x" ,
1778- repeat,
1779- elapsed_auron,
1780- elapsed_df,
1781- elapsed_auron / elapsed_df
1834+ "[sort external/spill, auron-only] repeat={repeat:>3}, auron={elapsed_auron:.3}s"
17821835 ) ;
17831836 }
17841837 Ok ( ( ) )
0 commit comments