@@ -28,7 +28,7 @@ pub use datafusion_common::utils::memory::get_record_batch_memory_size;
2828#[ doc( hidden) ]
2929pub use spill_manager:: SpillManager ;
3030
31- use std:: collections:: VecDeque ;
31+ use std:: collections:: { HashMap , VecDeque } ;
3232use std:: pin:: Pin ;
3333use std:: sync:: { Arc , LazyLock } ;
3434use std:: task:: { Context , Poll } ;
@@ -37,7 +37,7 @@ use arrow::array::{
3737 Array , ArrayRef , BinaryViewArray , BufferSpec , GenericByteViewArray , StringViewArray ,
3838 layout, make_array,
3939} ;
40- use arrow:: buffer:: Buffer ;
40+ use arrow:: buffer:: { Buffer , ScalarBuffer } ;
4141use arrow:: datatypes:: DataType ;
4242use arrow:: datatypes:: { ByteViewType , Schema , SchemaRef } ;
4343use arrow:: ipc:: {
@@ -46,7 +46,7 @@ use arrow::ipc::{
4646 writer:: { IpcWriteOptions , StreamEncoder } ,
4747} ;
4848use arrow:: record_batch:: RecordBatch ;
49- use arrow_data:: ArrayDataBuilder ;
49+ use arrow_data:: { ArrayDataBuilder , ByteView , MAX_INLINE_VIEW_LEN } ;
5050#[ cfg( test) ]
5151use arrow_ipc:: writer:: StreamWriter ;
5252use arrow_ipc:: { CompressionType , root_as_message} ;
@@ -746,6 +746,7 @@ fn get_max_alignment_for_schema(schema: &Schema) -> usize {
746746
747747/// Size of a single view structure in StringView/BinaryView arrays (in bytes).
748748/// Each view is 16 bytes: 4 bytes length + 4 bytes prefix + 8 bytes buffer ID/offset.
749+ #[ cfg( test) ]
749750const VIEW_SIZE_BYTES : usize = 16 ;
750751
751752/// Performs garbage collection on StringView and BinaryView arrays before spilling to reduce memory usage.
@@ -807,27 +808,105 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
807808 . as_any ( )
808809 . downcast_ref :: < StringViewArray > ( )
809810 . expect ( "Utf8View array should downcast to StringViewArray" ) ;
810- if should_gc_view_array ( string_view) {
811- Ok ( ( Arc :: new ( string_view. gc ( ) ) as ArrayRef , true ) )
812- } else {
813- Ok ( ( Arc :: clone ( array) , false ) )
814- }
811+ Ok ( match compact_view_array ( string_view) {
812+ Some ( compacted) => ( Arc :: new ( compacted) as ArrayRef , true ) ,
813+ None => ( Arc :: clone ( array) , false ) ,
814+ } )
815815 }
816816 DataType :: BinaryView => {
817817 let binary_view = array
818818 . as_any ( )
819819 . downcast_ref :: < BinaryViewArray > ( )
820820 . expect ( "BinaryView array should downcast to BinaryViewArray" ) ;
821- if should_gc_view_array ( binary_view) {
822- Ok ( ( Arc :: new ( binary_view. gc ( ) ) as ArrayRef , true ) )
823- } else {
824- Ok ( ( Arc :: clone ( array) , false ) )
825- }
821+ Ok ( match compact_view_array ( binary_view) {
822+ Some ( compacted) => ( Arc :: new ( compacted) as ArrayRef , true ) ,
823+ None => ( Arc :: clone ( array) , false ) ,
824+ } )
826825 }
827826 _ => gc_array_children ( array) ,
828827 }
829828}
830829
830+ /// Compacts a view array before spilling, or returns `None` to spill it as is.
831+ ///
832+ /// The IPC writer writes every data buffer of an array in full, once per
833+ /// entry, and `gc()` copies the bytes of every view separately. Neither
834+ /// handles views that share bytes, as the views of a dictionary-encoded
835+ /// Parquet column do: after `concat` or `interleave` the dictionary is often
836+ /// listed once per input batch, and `gc()` copies it once per row
837+ /// (<https://github.com/apache/datafusion/issues/23564>).
838+ ///
839+ /// Repeated buffer entries are merged first. `gc()` then runs only if it
840+ /// writes less than the merged buffers hold, and runs on the original array
841+ /// so that the views are rewritten at most once.
842+ fn compact_view_array < T : ByteViewType > (
843+ array : & GenericByteViewArray < T > ,
844+ ) -> Option < GenericByteViewArray < T > > {
845+ let buffers = array. data_buffers ( ) ;
846+ if buffers. is_empty ( ) {
847+ return None ;
848+ }
849+
850+ let ( remap, unique) = dedup_buffer_entries ( buffers) ;
851+ let unique_size: usize = unique. iter ( ) . map ( |b| b. len ( ) ) . sum ( ) ;
852+ if should_gc_view_array ( array, unique_size) {
853+ return Some ( array. gc ( ) ) ;
854+ }
855+ if unique. len ( ) == buffers. len ( ) {
856+ return None ;
857+ }
858+
859+ let views: Vec < u128 > = array
860+ . views ( )
861+ . iter ( )
862+ . enumerate ( )
863+ . map ( |( i, & raw) | {
864+ if array. is_null ( i) {
865+ 0
866+ } else if ( raw as u32 ) <= MAX_INLINE_VIEW_LEN {
867+ raw
868+ } else {
869+ let view = ByteView :: from ( raw) ;
870+ ByteView {
871+ buffer_index : remap[ view. buffer_index as usize ] ,
872+ ..view
873+ }
874+ . as_u128 ( )
875+ }
876+ } )
877+ . collect ( ) ;
878+
879+ // SAFETY: every non-null, non-inline view points at the same bytes as
880+ // before, in an entry of `unique` that holds the same buffer, and inline
881+ // views are unchanged.
882+ Some ( unsafe {
883+ GenericByteViewArray :: new_unchecked (
884+ ScalarBuffer :: from ( views) ,
885+ unique. into ( ) ,
886+ array. nulls ( ) . cloned ( ) ,
887+ )
888+ } )
889+ }
890+
891+ /// Returns, for each entry of `buffers`, its index in a list without repeated
892+ /// entries of the same buffer, and that list.
893+ fn dedup_buffer_entries ( buffers : & [ Buffer ] ) -> ( Vec < u32 > , Vec < Buffer > ) {
894+ let mut first_index: HashMap < ( * const u8 , usize ) , u32 > = HashMap :: new ( ) ;
895+ let mut unique: Vec < Buffer > = Vec :: with_capacity ( buffers. len ( ) ) ;
896+ let remap = buffers
897+ . iter ( )
898+ . map ( |buffer| {
899+ * first_index
900+ . entry ( ( buffer. as_ptr ( ) , buffer. len ( ) ) )
901+ . or_insert_with ( || {
902+ unique. push ( buffer. clone ( ) ) ;
903+ ( unique. len ( ) - 1 ) as u32
904+ } )
905+ } )
906+ . collect ( ) ;
907+ ( remap, unique)
908+ }
909+
831910fn gc_array_children ( array : & ArrayRef ) -> Result < ( ArrayRef , bool ) > {
832911 let data = array. to_data ( ) ;
833912 if data. child_data ( ) . is_empty ( ) {
@@ -858,28 +937,32 @@ fn gc_array_children(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
858937 Ok ( ( make_array ( rebuilt) , true ) )
859938}
860939
861- /// Determines whether a view array should be garbage collected before spilling.
940+ /// Determines whether a view array should be garbage collected before spilling,
941+ /// given `data_size`, the bytes its data buffers hold after
942+ /// [`dedup_buffer_entries`].
862943///
863- /// Arrow's `gc()` always allocates new compact buffers (it is never a no-op), so we
864- /// check here to skip the allocation cost when data buffers are small. We subtract
865- /// the views buffer (16 bytes × n_rows) from `get_buffer_memory_size()` so the
866- /// threshold tracks non-inline string data rather than row count.
867- fn should_gc_view_array < T : ByteViewType > ( array : & GenericByteViewArray < T > ) -> bool {
944+ /// Arrow's `gc()` always allocates new compact buffers (it is never a no-op), so
945+ /// it is skipped when the data buffers are small. It is also skipped when it
946+ /// would write more than the data buffers hold: `gc()` copies the bytes of
947+ /// every view separately, so views that share bytes each get their own copy.
948+ /// Both sizes come from the views and buffer lengths, without reading values.
949+ fn should_gc_view_array < T : ByteViewType > (
950+ array : & GenericByteViewArray < T > ,
951+ data_size : usize ,
952+ ) -> bool {
868953 const MIN_BUFFER_SIZE_FOR_GC : usize = 10 * 1024 ; // 10KB threshold
869954
870- if array . data_buffers ( ) . is_empty ( ) {
871- return false ;
872- }
955+ // `total_buffer_bytes_used` is the size of the data buffer `gc()` writes
956+ data_size > MIN_BUFFER_SIZE_FOR_GC && array . total_buffer_bytes_used ( ) < data_size
957+ }
873958
874- let data_buffer_size = array
875- . get_buffer_memory_size ( )
876- . saturating_sub ( array. len ( ) * VIEW_SIZE_BYTES ) ;
877- data_buffer_size > MIN_BUFFER_SIZE_FOR_GC
959+ #[ cfg( test) ]
960+ fn data_buffers_size ( array : & StringViewArray ) -> usize {
961+ array. data_buffers ( ) . iter ( ) . map ( |b| b. len ( ) ) . sum ( )
878962}
879963
880964#[ cfg( test) ]
881965fn calculate_string_view_waste_ratio ( array : & StringViewArray ) -> f64 {
882- use arrow_data:: MAX_INLINE_VIEW_LEN ;
883966 calculate_view_waste_ratio ( array. len ( ) , array. data_buffers ( ) , |i| {
884967 if !array. is_null ( i) {
885968 let value = array. value ( i) ;
@@ -2178,13 +2261,12 @@ mod tests {
21782261 let batch = RecordBatch :: try_new ( Arc :: clone ( & schema) , vec ! [ array_ref] ) ?;
21792262
21802263 // GC should return the original batch for small arrays
2181- let should_gc = should_gc_view_array (
2182- batch
2183- . column ( 0 )
2184- . as_any ( )
2185- . downcast_ref :: < StringViewArray > ( )
2186- . unwrap ( ) ,
2187- ) ;
2264+ let array = batch
2265+ . column ( 0 )
2266+ . as_any ( )
2267+ . downcast_ref :: < StringViewArray > ( )
2268+ . unwrap ( ) ;
2269+ let should_gc = should_gc_view_array ( array, data_buffers_size ( array) ) ;
21882270 let gc_batch = gc_view_arrays ( & batch) ?;
21892271
21902272 assert ! ( !should_gc) ;
@@ -2256,7 +2338,8 @@ mod tests {
22562338 . as_any ( )
22572339 . downcast_ref :: < StringViewArray > ( )
22582340 . unwrap ( ) ;
2259- let should_gc = should_gc_view_array ( sliced_array) ;
2341+ let should_gc =
2342+ should_gc_view_array ( sliced_array, data_buffers_size ( sliced_array) ) ;
22602343 let waste_ratio = calculate_string_view_waste_ratio ( sliced_array) ;
22612344
22622345 assert ! (
@@ -2407,6 +2490,158 @@ mod tests {
24072490 Ok ( ( ) )
24082491 }
24092492
2493+ /// `len` distinct 63 byte values in a single data buffer
2494+ fn dictionary_array ( len : u32 ) -> Result < StringViewArray > {
2495+ use arrow:: array:: AsArray ;
2496+
2497+ let values = StringArray :: from_iter_values (
2498+ ( 0 ..len) . map ( |i| format ! ( "container-id-{i:0>50}" ) ) ,
2499+ ) ;
2500+ // Casting to Utf8View keeps the values in the one existing buffer
2501+ let dictionary = cast ( & values, & DataType :: Utf8View ) ?;
2502+ let dictionary = dictionary. as_string_view ( ) . clone ( ) ;
2503+ assert_eq ! ( dictionary. data_buffers( ) . len( ) , 1 ) ;
2504+ Ok ( dictionary)
2505+ }
2506+
2507+ /// Arrays that each hold views into one shared dictionary buffer, as the
2508+ /// Parquet reader produces for a dictionary-encoded column, combined the
2509+ /// way a sort combines its input batches. Each output row cycles through
2510+ /// the dictionary (<https://github.com/apache/datafusion/issues/23564>).
2511+ fn shared_dictionary_views (
2512+ dictionary_len : u32 ,
2513+ inputs : u32 ,
2514+ rows_per_input : u32 ,
2515+ ) -> Result < ( StringViewArray , StringViewArray ) > {
2516+ use arrow:: array:: { AsArray , UInt32Array } ;
2517+ use arrow:: compute:: { interleave, take} ;
2518+
2519+ let dictionary = dictionary_array ( dictionary_len) ?;
2520+ let arrays = ( 0 ..inputs)
2521+ . map ( |input| {
2522+ let indices = UInt32Array :: from_iter_values (
2523+ ( 0 ..rows_per_input)
2524+ . map ( |row| ( input * rows_per_input + row) % dictionary_len) ,
2525+ ) ;
2526+ take ( & dictionary, & indices, None )
2527+ } )
2528+ . collect :: < std:: result:: Result < Vec < _ > , _ > > ( ) ?;
2529+ let arrays: Vec < & dyn Array > = arrays. iter ( ) . map ( |a| a. as_ref ( ) ) . collect ( ) ;
2530+ let indices: Vec < ( usize , usize ) > = ( 0 ..rows_per_input as usize )
2531+ . flat_map ( |row| ( 0 ..inputs as usize ) . map ( move |input| ( input, row) ) )
2532+ . collect ( ) ;
2533+ let merged = interleave ( & arrays, & indices) ?;
2534+ Ok ( ( dictionary, merged. as_string_view ( ) . clone ( ) ) )
2535+ }
2536+
2537+ #[ test]
2538+ fn test_gc_keeps_shared_dictionary_buffer ( ) -> Result < ( ) > {
2539+ let ( dictionary, merged) = shared_dictionary_views ( 1000 , 8 , 1024 ) ?;
2540+ // `interleave` lists the dictionary once per input
2541+ assert_eq ! ( merged. data_buffers( ) . len( ) , 8 ) ;
2542+ // `gc()` would copy 8192 values of 63 bytes, 8x the dictionary
2543+ assert ! ( merged. total_buffer_bytes_used( ) > data_buffers_size( & dictionary) ) ;
2544+
2545+ let compacted = compact_view_array ( & merged) . expect ( "buffer entries repeat" ) ;
2546+ assert_eq ! ( compacted. data_buffers( ) . len( ) , 1 ) ;
2547+ assert_eq ! (
2548+ compacted. data_buffers( ) [ 0 ] . as_ptr( ) ,
2549+ dictionary. data_buffers( ) [ 0 ] . as_ptr( )
2550+ ) ;
2551+ assert_eq ! ( compacted, merged) ;
2552+
2553+ Ok ( ( ) )
2554+ }
2555+
2556+ #[ test]
2557+ fn test_gc_small_slice_of_shared_buffer ( ) -> Result < ( ) > {
2558+ // 8 inputs of 4 rows each reference a 63 KB dictionary: `gc()` writes
2559+ // 32 values, far less than the dictionary
2560+ let ( dictionary, merged) = shared_dictionary_views ( 1000 , 8 , 4 ) ?;
2561+ assert_eq ! ( merged. data_buffers( ) . len( ) , 8 ) ;
2562+
2563+ let compacted = compact_view_array ( & merged) . expect ( "gc() is smaller" ) ;
2564+ assert_eq ! ( compacted. data_buffers( ) . len( ) , 1 ) ;
2565+ assert_eq ! ( data_buffers_size( & compacted) , 32 * 63 ) ;
2566+ assert_ne ! (
2567+ compacted. data_buffers( ) [ 0 ] . as_ptr( ) ,
2568+ dictionary. data_buffers( ) [ 0 ] . as_ptr( )
2569+ ) ;
2570+ assert_eq ! ( compacted, merged) ;
2571+
2572+ Ok ( ( ) )
2573+ }
2574+
2575+ #[ test]
2576+ fn test_gc_shared_buffer_with_nulls ( ) -> Result < ( ) > {
2577+ use arrow:: array:: { AsArray , BooleanArray } ;
2578+ use arrow:: compute:: nullif;
2579+
2580+ let ( _, merged) = shared_dictionary_views ( 1000 , 8 , 1024 ) ?;
2581+ let mask = BooleanArray :: from_iter ( ( 0 ..merged. len ( ) ) . map ( |i| Some ( i % 7 == 0 ) ) ) ;
2582+ let with_nulls = nullif ( & merged, & mask) ?;
2583+ let with_nulls = with_nulls. as_string_view ( ) ;
2584+
2585+ let compacted = compact_view_array ( with_nulls) . expect ( "buffer entries repeat" ) ;
2586+ assert_eq ! ( compacted. data_buffers( ) . len( ) , 1 ) ;
2587+ assert_eq ! ( compacted. null_count( ) , with_nulls. null_count( ) ) ;
2588+ assert_eq ! ( & compacted, with_nulls) ;
2589+
2590+ Ok ( ( ) )
2591+ }
2592+
2593+ #[ test]
2594+ fn test_gc_skips_array_that_shares_one_buffer ( ) -> Result < ( ) > {
2595+ use arrow:: array:: { AsArray , UInt32Array } ;
2596+ use arrow:: compute:: take;
2597+
2598+ // 10,000 views into one 63 KB dictionary: `gc()` would write 630 KB
2599+ let dictionary = dictionary_array ( 1000 ) ?;
2600+ let indices = UInt32Array :: from_iter_values ( ( 0 ..10_000 ) . map ( |i| i % 1000 ) ) ;
2601+ let shared = take ( & dictionary, & indices, None ) ?;
2602+ let schema = Arc :: new ( Schema :: new ( vec ! [ Field :: new(
2603+ "label" ,
2604+ DataType :: Utf8View ,
2605+ false ,
2606+ ) ] ) ) ;
2607+ let batch = RecordBatch :: try_new ( schema, vec ! [ Arc :: clone( & shared) ] ) ?;
2608+
2609+ assert ! ( compact_view_array( shared. as_string_view( ) ) . is_none( ) ) ;
2610+ let gc_batch = gc_view_arrays ( & batch) ?;
2611+ assert ! ( Arc :: ptr_eq( batch. column( 0 ) , gc_batch. column( 0 ) ) ) ;
2612+
2613+ Ok ( ( ) )
2614+ }
2615+
2616+ #[ test]
2617+ fn test_spill_shared_dictionary_writes_it_once ( ) -> Result < ( ) > {
2618+ let ( dictionary, merged) = shared_dictionary_views ( 1000 , 8 , 1024 ) ?;
2619+ let schema = Arc :: new ( Schema :: new ( vec ! [ Field :: new(
2620+ "label" ,
2621+ DataType :: Utf8View ,
2622+ false ,
2623+ ) ] ) ) ;
2624+ let batch = RecordBatch :: try_new ( Arc :: clone ( & schema) , vec ! [ Arc :: new( merged) ] ) ?;
2625+
2626+ let env = Arc :: new ( RuntimeEnv :: default ( ) ) ;
2627+ let metrics = SpillMetrics :: new ( & ExecutionPlanMetricsSet :: new ( ) , 0 ) ;
2628+ let spill_manager = SpillManager :: new ( env, metrics, schema) ;
2629+ let mut in_progress_file = spill_manager. create_in_progress_file ( "Test GC" ) ?;
2630+ in_progress_file. append_batch ( & batch) ?;
2631+ let spill_file = in_progress_file. finish ( ) ?. unwrap ( ) ;
2632+ let file_size = std:: fs:: metadata ( spill_file. path ( ) . unwrap ( ) ) ?. len ( ) as usize ;
2633+
2634+ // The views and one copy of the dictionary, not 8192 copied values
2635+ let views_size = batch. num_rows ( ) * VIEW_SIZE_BYTES ;
2636+ let dictionary_size = data_buffers_size ( & dictionary) ;
2637+ assert ! (
2638+ file_size < views_size + 2 * dictionary_size,
2639+ "spill file is {file_size} bytes, views {views_size}, dictionary {dictionary_size}"
2640+ ) ;
2641+
2642+ Ok ( ( ) )
2643+ }
2644+
24102645 #[ test]
24112646 fn test_gc_recurses_into_nested_view_arrays ( ) -> Result < ( ) > {
24122647 use arrow:: array:: { DictionaryArray , Int32Array } ;
0 commit comments