Skip to content

Commit cbdecca

Browse files
cetra3adriangb
authored andcommitted
GC & Deduplicate String View on Spill
1 parent 5b5bed9 commit cbdecca

1 file changed

Lines changed: 42 additions & 12 deletions

File tree

  • datafusion/physical-plan/src/spill

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

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use std::task::{Context, Poll};
3535

3636
use arrow::array::{
3737
Array, ArrayRef, BinaryViewArray, BufferSpec, GenericByteViewArray, StringViewArray,
38-
layout, make_array,
38+
builder::GenericByteViewBuilder, layout, make_array,
3939
};
4040
use arrow::buffer::Buffer;
4141
use arrow::datatypes::DataType;
@@ -800,6 +800,20 @@ pub(crate) fn gc_view_arrays(batch: &RecordBatch) -> Result<RecordBatch> {
800800
}
801801
}
802802

803+
/// Garbage collect and deduplicate a string view before writing it out to disk
804+
///
805+
/// This is to avoid inflating disk usage and also to ensure that deduplication reduces memory pressure when reading back.
806+
fn gc_dedup_view<T: ByteViewType>(
807+
array: &GenericByteViewArray<T>,
808+
) -> GenericByteViewArray<T> {
809+
let mut builder = GenericByteViewBuilder::<T>::with_capacity(array.len())
810+
.with_deduplicate_strings();
811+
for v in array.iter() {
812+
builder.append_option(v);
813+
}
814+
builder.finish()
815+
}
816+
803817
fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
804818
match array.data_type() {
805819
DataType::Utf8View => {
@@ -808,7 +822,7 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
808822
.downcast_ref::<StringViewArray>()
809823
.expect("Utf8View array should downcast to StringViewArray");
810824
if should_gc_view_array(string_view) {
811-
Ok((Arc::new(string_view.gc()) as ArrayRef, true))
825+
Ok((Arc::new(gc_dedup_view(string_view)) as ArrayRef, true))
812826
} else {
813827
Ok((Arc::clone(array), false))
814828
}
@@ -819,7 +833,7 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
819833
.downcast_ref::<BinaryViewArray>()
820834
.expect("BinaryView array should downcast to BinaryViewArray");
821835
if should_gc_view_array(binary_view) {
822-
Ok((Arc::new(binary_view.gc()) as ArrayRef, true))
836+
Ok((Arc::new(gc_dedup_view(binary_view)) as ArrayRef, true))
823837
} else {
824838
Ok((Arc::clone(array), false))
825839
}
@@ -2381,7 +2395,7 @@ mod tests {
23812395
let size_without_gc: usize = array_without_gc
23822396
.data_buffers()
23832397
.iter()
2384-
.map(|buffer| buffer.capacity())
2398+
.map(|buffer| buffer.len())
23852399
.sum();
23862400

23872401
let gc_batch = gc_view_arrays(&sliced_batch)?;
@@ -2393,7 +2407,7 @@ mod tests {
23932407
let size_with_gc: usize = array_with_gc
23942408
.data_buffers()
23952409
.iter()
2396-
.map(|buffer| buffer.capacity())
2410+
.map(|buffer| buffer.len())
23972411
.sum();
23982412

23992413
let reduction_percent =
@@ -2412,9 +2426,18 @@ mod tests {
24122426
use arrow::array::{DictionaryArray, Int32Array};
24132427
use arrow::buffer::Buffer;
24142428

2415-
let strings: Vec<String> = (0..200)
2429+
// A small pool of distinct, non-inlined (> 12 byte) strings.
2430+
let distinct: Vec<String> = (0..8)
24162431
.map(|i| format!("http://example.com/nested/path/that/is/not/inlined/{i}"))
24172432
.collect();
2433+
2434+
// Bytes stored once each string is deduplicated (all references collapse
2435+
// to a single copy of every distinct value).
2436+
let distinct_bytes: usize = distinct.iter().map(|s| s.len()).sum();
2437+
2438+
let strings: Vec<String> = (0..200)
2439+
.map(|i| distinct[i % distinct.len()].clone())
2440+
.collect();
24182441
let string_values = Arc::new(StringViewArray::from(strings)) as ArrayRef;
24192442

24202443
let list_data = ArrayDataBuilder::new(DataType::List(Arc::new(
@@ -2454,9 +2477,11 @@ mod tests {
24542477
.as_any()
24552478
.downcast_ref::<StringViewArray>()
24562479
.unwrap();
2457-
assert!(
2458-
calculate_string_view_waste_ratio(gc_list_values) < 0.2,
2459-
"GC should compact nested List child views"
2480+
let list_stored_bytes: usize =
2481+
gc_list_values.data_buffers().iter().map(|b| b.len()).sum();
2482+
assert_eq!(
2483+
list_stored_bytes, distinct_bytes,
2484+
"GC should deduplicate nested List child views (regression: dedup not applied)"
24602485
);
24612486

24622487
let gc_dictionary_values = gc_batch.column(1).to_data().child_data()[0].clone();
@@ -2465,9 +2490,14 @@ mod tests {
24652490
.as_any()
24662491
.downcast_ref::<StringViewArray>()
24672492
.unwrap();
2468-
assert!(
2469-
calculate_string_view_waste_ratio(gc_dictionary_values) < 0.2,
2470-
"GC should compact nested Dictionary values"
2493+
let dictionary_stored_bytes: usize = gc_dictionary_values
2494+
.data_buffers()
2495+
.iter()
2496+
.map(|b| b.len())
2497+
.sum();
2498+
assert_eq!(
2499+
dictionary_stored_bytes, distinct_bytes,
2500+
"GC should deduplicate nested Dictionary values (regression: dedup not applied)"
24712501
);
24722502

24732503
Ok(())

0 commit comments

Comments
 (0)