perf: Optimize fused nested level conversion - #811
Conversation
Add a fused Parquet level conversion path for direct nullable struct parents over list/map children so the reader can populate repeated lengths and struct nulls from one def/rep traversal. Add oracle-based level conversion coverage, end-to-end continuous, skipped, and filtered Parquet reads, and fused struct/list cases to the existing level conversion microbenchmark. Reuse shared level conversion helpers and simplify repeated reader fallback setup. Co-authored-by: TRAE CLI <noreply@bytedance.com>
| RepeatedReader& repeatedReader) { | ||
| const auto repDefRange = pageReader.repDefRange(); | ||
| const int32_t numRepDefs = repDefRange.second - repDefRange.first; | ||
| auto lengths = repeatedReader.prepareRepDefLengths(numRepDefs); |
There was a problem hiding this comment.
Please avoid destructively preparing the output buffers before checking whether the level shape is supported. For a valid but unsupported shape, such as a required list directly under the struct, prepareRepDefLengths() moves the reusable lengths_.lengths() buffer into the local variable, then the fused call returns false and the local buffer is destroyed. The fallback at line 165 can no longer reuse it and must allocate a new buffer on every batch. Please perform the cheap LevelInfo shape check before moving/allocating the outputs, or restore the lengths buffer before returning false.
| if (repDefChild == nullptr) { | ||
| return nullptr; | ||
| } | ||
| const bool fuseStructAndRepeated = isArrayOrMap(*repDefChild); |
There was a problem hiding this comment.
Could we restrict this optimization to nullable structs, e.g. structReader->levelInfo().def_level > 0? For a required struct, the previous setNullsFromRepDefs() path is a no-op, so there are not two conversions to fuse. The new path still allocates and writes an all-valid struct bitmap and installs it as preset nulls, adding work to a path that previously only converted the list.
What problem does this PR solve?
Parquet nested reads currently convert the same DataPage def/rep levels separately when a nullable struct directly wraps a list or map child: once for repeated list/map lengths and validity, and once for the parent struct null bitmap. This repeats the same level traversal on a hot nested-read path.
This PR adds a fused conversion path for the direct nullable struct -> list/map case. The reader now attempts to populate the repeated lengths/list validity and parent struct null bitmap in one pass, then falls back to the existing separate conversions when the level relationship is not supported.
Issue Number: N/A
Type of Change
Description
This PR adds:
DefRepLevelsToListLengthsAndStructBitmap, which fuses direct list/map length conversion with direct parent struct bitmap conversion.PageReader::getListLengthsAndStructNullswrapper used by nested Parquet readers.ParquetReaderTestfor continuous reads, non-contiguousskip + nextreads, and top-level filtering.The fused reader path avoids setting repeated lengths twice by delaying the old repeated setup until the fused path either succeeds or explicitly falls back.
Performance Impact
No Impact: This change does not affect the critical path (e.g., build system, doc, error handling).
Positive Impact: I have run benchmarks.
Click to view Benchmark Results
Negative Impact: Explained below (e.g., trade-off for correctness).
Release Note
Release Note:
Checklist (For Author)
Validation run locally:
Breaking Changes
No
Yes (Description: ...)
Click to view Breaking Changes