Skip to content

Commit 3372b2b

Browse files
committed
Merge latest main
Signed-off-by: 1fanwang <1fannnw@gmail.com>
2 parents 9e378c6 + b350ea6 commit 3372b2b

1 file changed

Lines changed: 270 additions & 35 deletions

File tree

  • datafusion/physical-plan/src/spill

‎datafusion/physical-plan/src/spill/mod.rs‎

Lines changed: 270 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub use datafusion_common::utils::memory::get_record_batch_memory_size;
2828
#[doc(hidden)]
2929
pub use spill_manager::SpillManager;
3030

31-
use std::collections::VecDeque;
31+
use std::collections::{HashMap, VecDeque};
3232
use std::pin::Pin;
3333
use std::sync::{Arc, LazyLock};
3434
use 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};
4141
use arrow::datatypes::DataType;
4242
use arrow::datatypes::{ByteViewType, Schema, SchemaRef};
4343
use arrow::ipc::{
@@ -46,7 +46,7 @@ use arrow::ipc::{
4646
writer::{IpcWriteOptions, StreamEncoder},
4747
};
4848
use arrow::record_batch::RecordBatch;
49-
use arrow_data::ArrayDataBuilder;
49+
use arrow_data::{ArrayDataBuilder, ByteView, MAX_INLINE_VIEW_LEN};
5050
#[cfg(test)]
5151
use arrow_ipc::writer::StreamWriter;
5252
use 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)]
749750
const 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+
831910
fn 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)]
881965
fn 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

Comments
 (0)