Skip to content

Commit d6079c7

Browse files
committed
GC & Deduplicate String View on Spill
1 parent f755cb4 commit d6079c7

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
@@ -34,7 +34,7 @@ use std::task::{Context, Poll};
3434

3535
use arrow::array::{
3636
Array, ArrayRef, BinaryViewArray, BufferSpec, GenericByteViewArray, StringViewArray,
37-
layout, make_array,
37+
builder::GenericByteViewBuilder, layout, make_array,
3838
};
3939
use arrow::buffer::Buffer;
4040
use arrow::datatypes::DataType;
@@ -420,6 +420,20 @@ pub(crate) fn gc_view_arrays(batch: &RecordBatch) -> Result<RecordBatch> {
420420
}
421421
}
422422

423+
/// Garbage collect and deduplicate a string view before writing it out to disk
424+
///
425+
/// This is to avoid inflating disk usage and also to ensure that deduplication reduces memory pressure when reading back.
426+
fn gc_dedup_view<T: ByteViewType>(
427+
array: &GenericByteViewArray<T>,
428+
) -> GenericByteViewArray<T> {
429+
let mut builder = GenericByteViewBuilder::<T>::with_capacity(array.len())
430+
.with_deduplicate_strings();
431+
for v in array.iter() {
432+
builder.append_option(v);
433+
}
434+
builder.finish()
435+
}
436+
423437
fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
424438
match array.data_type() {
425439
DataType::Utf8View => {
@@ -428,7 +442,7 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
428442
.downcast_ref::<StringViewArray>()
429443
.expect("Utf8View array should downcast to StringViewArray");
430444
if should_gc_view_array(string_view) {
431-
Ok((Arc::new(string_view.gc()) as ArrayRef, true))
445+
Ok((Arc::new(gc_dedup_view(string_view)) as ArrayRef, true))
432446
} else {
433447
Ok((Arc::clone(array), false))
434448
}
@@ -439,7 +453,7 @@ fn gc_array(array: &ArrayRef) -> Result<(ArrayRef, bool)> {
439453
.downcast_ref::<BinaryViewArray>()
440454
.expect("BinaryView array should downcast to BinaryViewArray");
441455
if should_gc_view_array(binary_view) {
442-
Ok((Arc::new(binary_view.gc()) as ArrayRef, true))
456+
Ok((Arc::new(gc_dedup_view(binary_view)) as ArrayRef, true))
443457
} else {
444458
Ok((Arc::clone(array), false))
445459
}
@@ -1345,7 +1359,7 @@ mod tests {
13451359
let size_without_gc: usize = array_without_gc
13461360
.data_buffers()
13471361
.iter()
1348-
.map(|buffer| buffer.capacity())
1362+
.map(|buffer| buffer.len())
13491363
.sum();
13501364

13511365
let gc_batch = gc_view_arrays(&sliced_batch)?;
@@ -1357,7 +1371,7 @@ mod tests {
13571371
let size_with_gc: usize = array_with_gc
13581372
.data_buffers()
13591373
.iter()
1360-
.map(|buffer| buffer.capacity())
1374+
.map(|buffer| buffer.len())
13611375
.sum();
13621376

13631377
let reduction_percent =
@@ -1376,9 +1390,18 @@ mod tests {
13761390
use arrow::array::{DictionaryArray, Int32Array};
13771391
use arrow::buffer::Buffer;
13781392

1379-
let strings: Vec<String> = (0..200)
1393+
// A small pool of distinct, non-inlined (> 12 byte) strings.
1394+
let distinct: Vec<String> = (0..8)
13801395
.map(|i| format!("http://example.com/nested/path/that/is/not/inlined/{i}"))
13811396
.collect();
1397+
1398+
// Bytes stored once each string is deduplicated (all references collapse
1399+
// to a single copy of every distinct value).
1400+
let distinct_bytes: usize = distinct.iter().map(|s| s.len()).sum();
1401+
1402+
let strings: Vec<String> = (0..200)
1403+
.map(|i| distinct[i % distinct.len()].clone())
1404+
.collect();
13821405
let string_values = Arc::new(StringViewArray::from(strings)) as ArrayRef;
13831406

13841407
let list_data = ArrayDataBuilder::new(DataType::List(Arc::new(
@@ -1418,9 +1441,11 @@ mod tests {
14181441
.as_any()
14191442
.downcast_ref::<StringViewArray>()
14201443
.unwrap();
1421-
assert!(
1422-
calculate_string_view_waste_ratio(gc_list_values) < 0.2,
1423-
"GC should compact nested List child views"
1444+
let list_stored_bytes: usize =
1445+
gc_list_values.data_buffers().iter().map(|b| b.len()).sum();
1446+
assert_eq!(
1447+
list_stored_bytes, distinct_bytes,
1448+
"GC should deduplicate nested List child views (regression: dedup not applied)"
14241449
);
14251450

14261451
let gc_dictionary_values = gc_batch.column(1).to_data().child_data()[0].clone();
@@ -1429,9 +1454,14 @@ mod tests {
14291454
.as_any()
14301455
.downcast_ref::<StringViewArray>()
14311456
.unwrap();
1432-
assert!(
1433-
calculate_string_view_waste_ratio(gc_dictionary_values) < 0.2,
1434-
"GC should compact nested Dictionary values"
1457+
let dictionary_stored_bytes: usize = gc_dictionary_values
1458+
.data_buffers()
1459+
.iter()
1460+
.map(|b| b.len())
1461+
.sum();
1462+
assert_eq!(
1463+
dictionary_stored_bytes, distinct_bytes,
1464+
"GC should deduplicate nested Dictionary values (regression: dedup not applied)"
14351465
);
14361466

14371467
Ok(())

0 commit comments

Comments
 (0)