Skip to content

Commit a2bb4c9

Browse files
authored
feat(parquet): selective null padding for list child readers (#9848)
# Which issue does this PR close? <!-- We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. --> - Contributes to #9731 - Depend on #9847 - Depend on #9846 # Rationale for this change <!-- Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed. Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. --> Parquet list decoding currently materializes padding for null or empty parent lists and then copies the child array to filter that padding back out. This is expensive for nested list columns, especially sparse lists and fixed-width children, where memory can scale with decoded levels instead of actual emitted child values. # What changes are included in this PR? <!-- There is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR. --> This PR makes list child readers emit compact child arrays directly by pushing selective null padding into the leaf `RecordReader`. It also builds definition-level validity bitmaps word-at-a-time, sizes child buffers after levels are decoded, and adds list runtime and peak-memory benchmarks across element types and null densities. # Are these changes tested? <!-- We typically require tests for all PRs in order to: 1. Prevent the code from being accidentally broken by subsequent changes 2. Serve as another way to document the expected behavior of the code If tests are not included in your PR, please explain why (for example, are they covered by existing tests)? --> Extensive test coverage for the new logic through existing list reader tests, which now exercise the production `PrimitiveArrayReader` path with in-memory Parquet pages (see #9846), and `BooleanBufferBuilder::append_word` has targeted unit coverage. Benchmarks results: ``` Name Before After Delta ListArray/StringList/no NULLs 7.3395 ms 5.8001 ms (-21.0%) ListArray/StringList/half NULLs 4.1255 ms 3.4274 ms (-16.9%) ListArray/Int32List/90pct NULLs 1.0366 ms 975.63 us (-5.9%) ListArray/Fixed32List/no NULLs 4.9298 ms 3.3137 ms (-32.8%) ListArray/Fixed32List/half NULLs 2.8998 ms 2.7258 ms (-6.0%) ListArray/Fixed32List/90pct NULLs 1.0556 ms 995.82 us (-5.7%) ListArray/Fixed32List/99pct NULLs 508.65 us 467.16 us (-8.2%) Name Before After Delta ListArray_peak_memory/Int32List/no NULLs 836.51 KiB 574.79 KiB (-31.3%) ListArray_peak_memory/Int32List/half NULLs 482.01 KiB 336.29 KiB (-30.2%) ListArray_peak_memory/Int32List/90pct NULLs 271.95 KiB 175.32 KiB (-35.5%) ListArray_peak_memory/Int32List/99pct NULLs 217.96 KiB 120.04 KiB (-44.9%) ListArray_peak_memory/DoubleList/no NULLs 1.2399 MiB 715.31 KiB (-43.7%) ListArray_peak_memory/DoubleList/half NULLs 753.66 KiB 400.39 KiB (-46.9%) ListArray_peak_memory/DoubleList/90pct NULLs 380.62 KiB 190.89 KiB (-49.8%) ListArray_peak_memory/DoubleList/99pct NULLs 315.21 KiB 121.61 KiB (-61.4%) ListArray_peak_memory/Fixed32List/no NULLs 3.8031 MiB 1.5760 MiB (-58.6%) ListArray_peak_memory/Fixed32List/half NULLs 2.1710 MiB 849.94 KiB (-61.8%) ListArray_peak_memory/Fixed32List/90pct NULLs 1.0017 MiB 277.35 KiB (-73.0%) ListArray_peak_memory/Fixed32List/99pct NULLs 898.69 KiB 130.93 KiB (-85.4%) ListArray_peak_memory/StringList/no NULLs 3.7925 MiB 2.4715 MiB (-34.8%) ListArray_peak_memory/StringList/half NULLs 1.2541 MiB 772.94 KiB (-39.8%) ListArray_peak_memory/StringList/90pct NULLs 296.63 KiB 188.96 KiB (-36.3%) ListArray_peak_memory/StringList/99pct NULLs 226.75 KiB 120.37 KiB (-46.9%) Name Before After Delta ListArray_allocated_bytes/Int32List/no NULLs 10.458 MiB 6.8018 MiB (-35.0%) ListArray_allocated_bytes/Int32List/half NULLs 5.9797 MiB 4.0127 MiB (-32.9%) ListArray_allocated_bytes/Int32List/90pct NULLs 2.9985 MiB 1.8210 MiB (-39.3%) ListArray_allocated_bytes/Int32List/99pct NULLs 2.5579 MiB 1.3733 MiB (-46.3%) ListArray_allocated_bytes/DoubleList/no NULLs 16.083 MiB 8.6546 MiB (-46.2%) ListArray_allocated_bytes/DoubleList/half NULLs 8.9134 MiB 4.8497 MiB (-45.6%) ListArray_allocated_bytes/DoubleList/90pct NULLs 4.3656 MiB 2.0179 MiB (-53.8%) ListArray_allocated_bytes/DoubleList/99pct NULLs 3.7482 MiB 1.3903 MiB (-62.9%) ListArray_allocated_bytes/Fixed32List/no NULLs 49.441 MiB 19.505 MiB (-60.5%) ListArray_allocated_bytes/Fixed32List/half NULLs 26.846 MiB 10.459 MiB (-61.0%) ListArray_allocated_bytes/Fixed32List/90pct NULLs 12.483 MiB 3.1127 MiB (-75.1%) ListArray_allocated_bytes/Fixed32List/99pct NULLs 10.895 MiB 1.4980 MiB (-86.3%) ListArray_allocated_bytes/StringList/no NULLs 47.519 MiB 21.743 MiB (-54.2%) ListArray_allocated_bytes/StringList/half NULLs 19.097 MiB 10.478 MiB (-45.1%) ListArray_allocated_bytes/StringList/90pct NULLs 3.4203 MiB 2.1165 MiB (-38.1%) ListArray_allocated_bytes/StringList/99pct NULLs 2.6424 MiB 1.3777 MiB (-47.9%) ``` # Are there any user-facing changes? <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. If there are any breaking changes to public APIs, please call them out. --> None. --------- Signed-off-by: Hippolyte Barraud <hippolyte.barraud@datadoghq.com>
1 parent d3d7ae8 commit a2bb4c9

28 files changed

Lines changed: 1440 additions & 405 deletions

arrow-buffer/src/builder/boolean.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,46 @@ impl BooleanBufferBuilder {
186186
}
187187
}
188188

189+
/// Appends the low `count` bits from `word` into the buffer.
190+
///
191+
/// `word` is treated as a packed LSB-first bitmap. Only the lowest
192+
/// `count` bits are appended; higher bits are ignored.
193+
///
194+
/// This is significantly faster than calling [`Self::append`] in a
195+
/// loop when the caller already has bits packed into a `u64`.
196+
#[inline]
197+
pub fn append_word(&mut self, word: u64, count: usize) {
198+
debug_assert!(count <= 64);
199+
let mask = (u64::MAX >> ((64 - count) & 63)) * ((count != 0) as u64);
200+
let word = word & mask;
201+
202+
let new_len = self.len + count;
203+
let new_len_bytes = bit_util::ceil(new_len, 8);
204+
if new_len_bytes > self.buffer.len() {
205+
self.buffer.resize(new_len_bytes, 0);
206+
}
207+
208+
let bit_offset = self.len & 7;
209+
let byte_start = self.len / 8;
210+
let buf = self.buffer.as_slice_mut();
211+
212+
// Shift the word to align with the bit offset within the
213+
// current byte, then OR it into the buffer. The shift merges
214+
// correctly with any existing bits in the partial trailing byte.
215+
let shifted = word << bit_offset;
216+
let shifted_bytes = shifted.to_le_bytes();
217+
let bytes_to_write = bit_util::ceil(count + bit_offset, 8).min(8);
218+
for i in 0..bytes_to_write {
219+
buf[byte_start + i] |= shifted_bytes[i];
220+
}
221+
// When bit_offset > 0 the shift can overflow into a 9th byte.
222+
if bit_offset > 0 && count + bit_offset > 64 {
223+
buf[byte_start + 8] |= (word >> (64 - bit_offset)) as u8;
224+
}
225+
226+
self.len = new_len;
227+
}
228+
189229
/// Appends n `additional` bits of value `v` into the buffer
190230
#[inline]
191231
pub fn append_n(&mut self, additional: usize, v: bool) {
@@ -619,6 +659,102 @@ mod tests {
619659
}
620660
}
621661

662+
/// Helper: build via append_word and verify against bit-by-bit append.
663+
fn check_append_word(initial_bits: usize, word: u64, count: usize) {
664+
let mut got = BooleanBufferBuilder::new(0);
665+
let mut expected = BooleanBufferBuilder::new(0);
666+
got.append_n(initial_bits, true);
667+
expected.append_n(initial_bits, true);
668+
got.append_word(word, count);
669+
for i in 0..count {
670+
expected.append(word & (1 << i) != 0);
671+
}
672+
assert_eq!(got.len(), expected.len());
673+
assert_eq!(got.finish(), expected.finish());
674+
}
675+
676+
#[test]
677+
fn test_append_word_zero_count() {
678+
check_append_word(0, u64::MAX, 0);
679+
check_append_word(3, u64::MAX, 0);
680+
}
681+
682+
#[test]
683+
fn test_append_word_aligned() {
684+
for count in [1, 5, 8, 17, 64] {
685+
check_append_word(0, 0xDEAD_BEEF_CAFE_BABE, count);
686+
}
687+
}
688+
689+
#[test]
690+
fn test_append_word_unaligned() {
691+
for offset in 1..=7 {
692+
check_append_word(offset, 0xDEAD_BEEF_CAFE_BABE, 13);
693+
}
694+
}
695+
696+
#[test]
697+
fn test_append_word_overflow_9th_byte() {
698+
check_append_word(3, u64::MAX, 64);
699+
check_append_word(7, 0xA5A5_A5A5_A5A5_A5A5, 64);
700+
}
701+
702+
#[test]
703+
fn test_append_word_small_counts() {
704+
check_append_word(0, 0b1, 1);
705+
check_append_word(0, 0b1010101, 7);
706+
check_append_word(3, 0b1, 1);
707+
check_append_word(3, 0b1010101, 7);
708+
}
709+
710+
#[test]
711+
fn test_append_word_ignores_high_bits_before_later_appends() {
712+
let mut builder = BooleanBufferBuilder::new(0);
713+
builder.append_word(0b10, 1);
714+
builder.append(false);
715+
716+
let finished = builder.finish();
717+
assert_eq!(finished.len(), 2);
718+
assert!(!finished.value(0));
719+
assert!(!finished.value(1));
720+
}
721+
722+
#[test]
723+
fn test_append_word_full_word() {
724+
check_append_word(0, u64::MAX, 64);
725+
check_append_word(0, 0, 64);
726+
}
727+
728+
#[test]
729+
fn test_append_word_sequential() {
730+
let mut got = BooleanBufferBuilder::new(0);
731+
let mut expected = BooleanBufferBuilder::new(0);
732+
for (word, count) in [(0b1010u64, 4), (0b111u64, 3), (0u64, 5), (u64::MAX, 64)] {
733+
got.append_word(word, count);
734+
for i in 0..count {
735+
expected.append(word & (1 << i) != 0);
736+
}
737+
}
738+
assert_eq!(got.finish(), expected.finish());
739+
}
740+
741+
#[test]
742+
fn test_append_word_mixed_with_append() {
743+
let mut got = BooleanBufferBuilder::new(0);
744+
let mut expected = BooleanBufferBuilder::new(0);
745+
got.append(true);
746+
expected.append(true);
747+
got.append_word(0b1100, 4);
748+
for i in 0..4 {
749+
expected.append(0b1100u64 & (1 << i) != 0);
750+
}
751+
got.append(false);
752+
expected.append(false);
753+
got.append_word(0xFF, 8);
754+
expected.append_n(8, true);
755+
assert_eq!(got.finish(), expected.finish());
756+
}
757+
622758
#[test]
623759
fn test_extend_misaligned_end() {
624760
for len in 1..130 {

parquet/benches/arrow_reader.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -785,12 +785,15 @@ fn create_int32_list_reader(
785785
column_desc,
786786
None,
787787
DEFAULT_BATCH_SIZE,
788+
Some(2),
788789
)
789790
.unwrap(),
790791
) as Box<dyn ArrayReader>;
791792
let field = Field::new_list_field(DataType::Int32, true);
792793
let data_type = DataType::List(Arc::new(field));
793-
Box::new(ListArrayReader::<i32>::new(items, data_type, 2, 1, true))
794+
Box::new(ListArrayReader::<i32>::new(
795+
items, data_type, 2, 1, true, None,
796+
))
794797
}
795798

796799
const FIXED_BYTE_LEN: usize = 32;
@@ -858,11 +861,14 @@ fn create_fixed32_list_reader(
858861
column_desc,
859862
None,
860863
DEFAULT_BATCH_SIZE,
864+
Some(2),
861865
)
862866
.unwrap();
863867
let field = Field::new_list_field(DataType::FixedSizeBinary(FIXED_BYTE_LEN as i32), true);
864868
let data_type = DataType::List(Arc::new(field));
865-
Box::new(ListArrayReader::<i32>::new(items, data_type, 2, 1, true))
869+
Box::new(ListArrayReader::<i32>::new(
870+
items, data_type, 2, 1, true, None,
871+
))
866872
}
867873

868874
fn bench_array_reader(mut array_reader: Box<dyn ArrayReader>) -> usize {
@@ -912,6 +918,7 @@ fn create_primitive_array_reader(
912918
column_desc,
913919
None,
914920
DEFAULT_BATCH_SIZE,
921+
None,
915922
)
916923
.unwrap();
917924
Box::new(reader)
@@ -922,6 +929,7 @@ fn create_primitive_array_reader(
922929
column_desc,
923930
None,
924931
DEFAULT_BATCH_SIZE,
932+
None,
925933
)
926934
.unwrap();
927935
Box::new(reader)
@@ -932,6 +940,7 @@ fn create_primitive_array_reader(
932940
column_desc,
933941
None,
934942
DEFAULT_BATCH_SIZE,
943+
None,
935944
)
936945
.unwrap();
937946
Box::new(reader)
@@ -951,6 +960,7 @@ fn create_f16_by_bytes_reader(
951960
column_desc,
952961
None,
953962
DEFAULT_BATCH_SIZE,
963+
None,
954964
)
955965
.unwrap(),
956966
_ => unimplemented!(),
@@ -968,13 +978,15 @@ fn create_decimal_by_bytes_reader(
968978
column_desc,
969979
None,
970980
DEFAULT_BATCH_SIZE,
981+
None,
971982
)
972983
.unwrap(),
973984
Type::FIXED_LEN_BYTE_ARRAY => make_fixed_len_byte_array_reader(
974985
Box::new(page_iterator),
975986
column_desc,
976987
None,
977988
DEFAULT_BATCH_SIZE,
989+
None,
978990
)
979991
.unwrap(),
980992
_ => unimplemented!(),
@@ -990,6 +1002,7 @@ fn create_fixed_len_byte_array_reader(
9901002
column_desc,
9911003
None,
9921004
DEFAULT_BATCH_SIZE,
1005+
None,
9931006
)
9941007
.unwrap()
9951008
}
@@ -1003,6 +1016,7 @@ fn create_byte_array_reader(
10031016
column_desc,
10041017
None,
10051018
DEFAULT_BATCH_SIZE,
1019+
None,
10061020
)
10071021
.unwrap()
10081022
}
@@ -1016,6 +1030,7 @@ fn create_byte_view_array_reader(
10161030
column_desc,
10171031
None,
10181032
DEFAULT_BATCH_SIZE,
1033+
None,
10191034
)
10201035
.unwrap()
10211036
}
@@ -1029,6 +1044,7 @@ fn create_string_view_byte_array_reader(
10291044
column_desc,
10301045
None,
10311046
DEFAULT_BATCH_SIZE,
1047+
None,
10321048
)
10331049
.unwrap()
10341050
}
@@ -1045,6 +1061,7 @@ fn create_string_byte_array_dictionary_reader(
10451061
column_desc,
10461062
Some(arrow_type),
10471063
DEFAULT_BATCH_SIZE,
1064+
None,
10481065
)
10491066
.unwrap()
10501067
}
@@ -1053,10 +1070,19 @@ fn create_string_list_reader(
10531070
page_iterator: impl PageIterator + 'static,
10541071
column_desc: ColumnDescPtr,
10551072
) -> Box<dyn ArrayReader> {
1056-
let items = create_byte_array_reader(page_iterator, column_desc);
1073+
let items = make_byte_array_reader(
1074+
Box::new(page_iterator),
1075+
column_desc,
1076+
None,
1077+
DEFAULT_BATCH_SIZE,
1078+
Some(2),
1079+
)
1080+
.unwrap();
10571081
let field = Field::new_list_field(DataType::Utf8, true);
10581082
let data_type = DataType::List(Arc::new(field));
1059-
Box::new(ListArrayReader::<i32>::new(items, data_type, 2, 1, true))
1083+
Box::new(ListArrayReader::<i32>::new(
1084+
items, data_type, 2, 1, true, None,
1085+
))
10601086
}
10611087

10621088
fn bench_byte_decimal<T>(

parquet/benches/arrow_reader_peak_memory.rs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -443,46 +443,68 @@ fn int32_list_wrapper(
443443
column_desc: ColumnDescPtr,
444444
) -> Box<dyn ArrayReader> {
445445
let child: Box<dyn ArrayReader> = Box::new(
446-
PrimitiveArrayReader::<Int32Type>::new(pages, column_desc, None, DEFAULT_BATCH_SIZE)
447-
.unwrap(),
446+
PrimitiveArrayReader::<Int32Type>::new(
447+
pages,
448+
column_desc,
449+
None,
450+
DEFAULT_BATCH_SIZE,
451+
Some(2),
452+
)
453+
.unwrap(),
448454
);
449455
let field = Field::new_list_field(DataType::Int32, true);
450456
let data_type = DataType::List(Arc::new(field));
451-
Box::new(ListArrayReader::<i32>::new(child, data_type, 2, 1, true))
457+
Box::new(ListArrayReader::<i32>::new(
458+
child, data_type, 2, 1, true, None,
459+
))
452460
}
453461

454462
fn double_list_wrapper(
455463
pages: Box<dyn PageIterator>,
456464
column_desc: ColumnDescPtr,
457465
) -> Box<dyn ArrayReader> {
458466
let child: Box<dyn ArrayReader> = Box::new(
459-
PrimitiveArrayReader::<DoubleType>::new(pages, column_desc, None, DEFAULT_BATCH_SIZE)
460-
.unwrap(),
467+
PrimitiveArrayReader::<DoubleType>::new(
468+
pages,
469+
column_desc,
470+
None,
471+
DEFAULT_BATCH_SIZE,
472+
Some(2),
473+
)
474+
.unwrap(),
461475
);
462476
let field = Field::new_list_field(DataType::Float64, true);
463477
let data_type = DataType::List(Arc::new(field));
464-
Box::new(ListArrayReader::<i32>::new(child, data_type, 2, 1, true))
478+
Box::new(ListArrayReader::<i32>::new(
479+
child, data_type, 2, 1, true, None,
480+
))
465481
}
466482

467483
fn fixed32_list_wrapper(
468484
pages: Box<dyn PageIterator>,
469485
column_desc: ColumnDescPtr,
470486
) -> Box<dyn ArrayReader> {
471487
let child =
472-
make_fixed_len_byte_array_reader(pages, column_desc, None, DEFAULT_BATCH_SIZE).unwrap();
488+
make_fixed_len_byte_array_reader(pages, column_desc, None, DEFAULT_BATCH_SIZE, Some(2))
489+
.unwrap();
473490
let field = Field::new_list_field(DataType::FixedSizeBinary(FIXED_BYTE_LEN as i32), true);
474491
let data_type = DataType::List(Arc::new(field));
475-
Box::new(ListArrayReader::<i32>::new(child, data_type, 2, 1, true))
492+
Box::new(ListArrayReader::<i32>::new(
493+
child, data_type, 2, 1, true, None,
494+
))
476495
}
477496

478497
fn string_list_wrapper(
479498
pages: Box<dyn PageIterator>,
480499
column_desc: ColumnDescPtr,
481500
) -> Box<dyn ArrayReader> {
482-
let child = make_byte_array_reader(pages, column_desc, None, DEFAULT_BATCH_SIZE).unwrap();
501+
let child =
502+
make_byte_array_reader(pages, column_desc, None, DEFAULT_BATCH_SIZE, Some(2)).unwrap();
483503
let field = Field::new_list_field(DataType::Utf8, true);
484504
let data_type = DataType::List(Arc::new(field));
485-
Box::new(ListArrayReader::<i32>::new(child, data_type, 2, 1, true))
505+
Box::new(ListArrayReader::<i32>::new(
506+
child, data_type, 2, 1, true, None,
507+
))
486508
}
487509

488510
fn add_benches<M: Measurement>(c: &mut Criterion<M>, measurement_name: &str) {

0 commit comments

Comments
 (0)