perf(parquet): Reuse DeltaByteArrayDecoder via reset() + devirtualize#4
Closed
jaylisde wants to merge 1 commit into
Closed
perf(parquet): Reuse DeltaByteArrayDecoder via reset() + devirtualize#4jaylisde wants to merge 1 commit into
jaylisde wants to merge 1 commit into
Conversation
jaylisde
force-pushed
the
pr/delta-bp-perf
branch
from
May 30, 2026 07:14
92e33b7 to
2d17343
Compare
jaylisde
force-pushed
the
pr/delta-bytearray-perf
branch
from
May 30, 2026 07:15
9fa0405 to
38359d6
Compare
This was referenced Jun 1, 2026
jaylisde
force-pushed
the
pr/delta-bytearray-perf
branch
from
June 3, 2026 00:01
38359d6 to
7967878
Compare
jaylisde
added a commit
that referenced
this pull request
Jun 3, 2026
…sitor paths Builds on #3 (PR A) and #4 (PR B) to close the remaining DELTA-vs-PLAIN/DICT scan gap on TPC-H Q12. 1. Inline SIMD bit-unpack kernel in DeltaBpDecoder::decodeLongs. When the read aligns at a miniblock start and consumes a whole miniblock, dispatch on bit_width 0..32 to a compile-time specialized kernel: - bw 0: arithmetic-sequence fast path (no bit-extract). - bw 1..16: 4-value/iter, single unaligned 64-bit load (4*16 = 64 bits fit in one u64 window). - bw 17..32: 2-value/iter, __uint128_t funnel-shift (lowers to two u64 loads + SHRD on x86_64). The trailing u64 read is safe because intra-page overshoots fall into the next miniblock and the last miniblock has PageReader::kPageReadPadding (8) trailing bytes guaranteed. bit_widths 33..64 fall through to the per-row scalar inner loop, which is unchanged. 2. New readWithVisitorSparseBuffered path for the !Visitor::dense + !hasNulls + deterministic filter + NoHook + integral DataType case (the hot path on Q12 because filter chaining produces sparse row sets after l_shipmode is applied). Decodes kBatch=1024 physical values into a stack buffer via decodeLongs (which now uses the SIMD kernel above), then walks the visitor's sparse rows array using rows[k] - batchPhysStart as buffer index. The existing per-row visitor.process is preserved — only the decode side is batched. n is capped to the visitor's residual physical span so the decoder never advances past what the visitor will consume. 3. readWithVisitorDenseBatched kBatch raised from 256 to 1024 to amortize the chunk-loop overhead now that decodeLongs is much faster per call. 4. Add velox/dwio/parquet/tests/reader/DeltaBpDecoderTest.cpp: - Hand-rolled DeltaEncoder for byte-stream control in tests. - 11 boundary tests: bit_widths 0/8/10/16/24/32, multi-block in a single readValues, mid-miniblock split across two readValues calls, negative minDelta, bit_width > 32 fallback, narrowing to int32_t. - 32 parameterized roundtrip tests covering bit_widths 1..32, each forcing the encoder to pick exactly that width by saturating one residual to (1<<bw)-1. Bench (TPC-H Q12 SF10, DELTA-encoded lineitem, num_drivers=4, 5-run median): wall #3 baseline 897 ms +#4 (PR B) 748 ms +this PR 626 ms That is -16.3% on top of PR B, -30.2% from the PR A baseline. PLAIN/DICT parquet paths are unchanged. Test plan - velox_dwio_parquet_delta_bp_decoder_test: 43 tests pass (11 boundary + 32 parameterized bw 1..32). - velox_dwio_parquet_table_scan_test: 54 tests pass (includes the 7 delta tests added in PR A). - velox_parquet_e2e_filter_test: 34 tests pass. Tracking: #2.
jaylisde
force-pushed
the
pr/delta-bp-perf
branch
from
June 3, 2026 06:30
5e5071f to
a53751d
Compare
jaylisde
force-pushed
the
pr/delta-bytearray-perf
branch
from
June 3, 2026 06:30
7967878 to
e987d8d
Compare
jaylisde
added a commit
that referenced
this pull request
Jun 3, 2026
…sitor paths Builds on #3 (PR A) and #4 (PR B) to close the remaining DELTA-vs-PLAIN/DICT scan gap on TPC-H Q12. 1. Inline SIMD bit-unpack kernel in DeltaBpDecoder::decodeLongs. When the read aligns at a miniblock start and consumes a whole miniblock, dispatch on bit_width 0..32 to a compile-time specialized kernel: - bw 0: arithmetic-sequence fast path (no bit-extract). - bw 1..16: 4-value/iter, single unaligned 64-bit load (4*16 = 64 bits fit in one u64 window). - bw 17..32: 2-value/iter, __uint128_t funnel-shift (lowers to two u64 loads + SHRD on x86_64). The trailing u64 read is safe because intra-page overshoots fall into the next miniblock and the last miniblock has PageReader::kPageReadPadding (8) trailing bytes guaranteed. bit_widths 33..64 fall through to the per-row scalar inner loop, which is unchanged. 2. New readWithVisitorSparseBuffered path for the !Visitor::dense + !hasNulls + deterministic filter + NoHook + integral DataType case (the hot path on Q12 because filter chaining produces sparse row sets after l_shipmode is applied). Decodes kBatch=1024 physical values into a stack buffer via decodeLongs (which now uses the SIMD kernel above), then walks the visitor's sparse rows array using rows[k] - batchPhysStart as buffer index. The existing per-row visitor.process is preserved — only the decode side is batched. n is capped to the visitor's residual physical span so the decoder never advances past what the visitor will consume. 3. readWithVisitorDenseBatched kBatch raised from 256 to 1024 to amortize the chunk-loop overhead now that decodeLongs is much faster per call. 4. Add velox/dwio/parquet/tests/reader/DeltaBpDecoderTest.cpp: - Hand-rolled DeltaEncoder for byte-stream control in tests. - 11 boundary tests: bit_widths 0/8/10/16/24/32, multi-block in a single readValues, mid-miniblock split across two readValues calls, negative minDelta, bit_width > 32 fallback, narrowing to int32_t. - 32 parameterized roundtrip tests covering bit_widths 1..32, each forcing the encoder to pick exactly that width by saturating one residual to (1<<bw)-1. Bench (TPC-H Q12 SF10, DELTA-encoded lineitem, num_drivers=4, 5-run median): wall #3 baseline 897 ms +#4 (PR B) 748 ms +this PR 626 ms That is -16.3% on top of PR B, -30.2% from the PR A baseline. PLAIN/DICT parquet paths are unchanged. Test plan - velox_dwio_parquet_delta_bp_decoder_test: 43 tests pass (11 boundary + 32 parameterized bw 1..32). - velox_dwio_parquet_table_scan_test: 54 tests pass (includes the 7 delta tests added in PR A). - velox_parquet_e2e_filter_test: 34 tests pass. Tracking: #2.
jaylisde
force-pushed
the
pr/delta-bp-perf
branch
from
June 3, 2026 06:49
a53751d to
4dde7ca
Compare
Three layered optimizations on the DELTA_BYTE_ARRAY hot path:
1. Drop the DeltaByteArrayDecoderBase virtual base. Both
DeltaByteArrayDecoder and DeltaLengthByteArrayDecoder are concrete
classes now; PageReader holds the right pointer type for each
encoding. Removes vtable dispatch from per-row readString().
2. Lazy-init child decoders via std::optional + reset(pageData_).
Previously every page boundary called make_unique on three
RleBpDecoders + reset their state; now the decoders are constructed
once per column-chunk and re-pointed at the next page via reset().
Saves ~3 heap allocations per page.
3. Replace `std::string lastValue_` with `std::vector<char> lastValueBuf_`
plus an int32 length. perf showed ~6.5% in std::string::_M_replace
on every readString() because the prefix-share encoding repeatedly
resizes the previous-value buffer. The vector path uses raw
resize+memcpy with no allocator interaction once the buffer reaches
max suffix size.
Bench (TPC-H Q12 SF10, on top of #pr-A):
wall scan_cpu
PR A only 0.85 s 2.13 s
+ reset() reuse 0.82 s 2.03 s
+ raw char buffer 0.78 s 1.91 s
+ devirtualize readString 0.74 s 1.74 s
End-to-end: PR A + PR B brings Q12 SF10 from PR facebookincubator#17633's 1.02 s wall
down to 0.74 s wall (-27%). Q12 SF100 16.3 s -> 13.1 s (-20%).
Tracking: #2
jaylisde
force-pushed
the
pr/delta-bytearray-perf
branch
from
June 3, 2026 06:49
e987d8d to
a9359a7
Compare
jaylisde
added a commit
that referenced
this pull request
Jun 3, 2026
…sitor paths Builds on #3 (PR A) and #4 (PR B) to close the remaining DELTA-vs-PLAIN/DICT scan gap on TPC-H Q12. 1. Inline SIMD bit-unpack kernel in DeltaBpDecoder::decodeLongs. When the read aligns at a miniblock start and consumes a whole miniblock, dispatch on bit_width 0..32 to a compile-time specialized kernel: - bw 0: arithmetic-sequence fast path (no bit-extract). - bw 1..16: 4-value/iter, single unaligned 64-bit load (4*16 = 64 bits fit in one u64 window). - bw 17..32: 2-value/iter, __uint128_t funnel-shift (lowers to two u64 loads + SHRD on x86_64). The trailing u64 read is safe because intra-page overshoots fall into the next miniblock and the last miniblock has PageReader::kPageReadPadding (8) trailing bytes guaranteed. bit_widths 33..64 fall through to the per-row scalar inner loop, which is unchanged. 2. New readWithVisitorSparseBuffered path for the !Visitor::dense + !hasNulls + deterministic filter + NoHook + integral DataType case (the hot path on Q12 because filter chaining produces sparse row sets after l_shipmode is applied). Decodes kBatch=1024 physical values into a stack buffer via decodeLongs (which now uses the SIMD kernel above), then walks the visitor's sparse rows array using rows[k] - batchPhysStart as buffer index. The existing per-row visitor.process is preserved — only the decode side is batched. n is capped to the visitor's residual physical span so the decoder never advances past what the visitor will consume. 3. readWithVisitorDenseBatched kBatch raised from 256 to 1024 to amortize the chunk-loop overhead now that decodeLongs is much faster per call. 4. Add velox/dwio/parquet/tests/reader/DeltaBpDecoderTest.cpp: - Hand-rolled DeltaEncoder for byte-stream control in tests. - 11 boundary tests: bit_widths 0/8/10/16/24/32, multi-block in a single readValues, mid-miniblock split across two readValues calls, negative minDelta, bit_width > 32 fallback, narrowing to int32_t. - 32 parameterized roundtrip tests covering bit_widths 1..32, each forcing the encoder to pick exactly that width by saturating one residual to (1<<bw)-1. Bench (TPC-H Q12 SF10, DELTA-encoded lineitem, num_drivers=4, 5-run median): wall #3 baseline 897 ms +#4 (PR B) 748 ms +this PR 626 ms That is -16.3% on top of PR B, -30.2% from the PR A baseline. PLAIN/DICT parquet paths are unchanged. Test plan - velox_dwio_parquet_delta_bp_decoder_test: 43 tests pass (11 boundary + 32 parameterized bw 1..32). - velox_dwio_parquet_table_scan_test: 54 tests pass (includes the 7 delta tests added in PR A). - velox_parquet_e2e_filter_test: 34 tests pass. Tracking: #2.
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three layered optimizations on the DELTA_BYTE_ARRAY hot path. Stacks on top of #3 (PR A); the base of this PR is
pr/delta-bp-perf.DeltaByteArrayDecoderBase. BothDeltaByteArrayDecoderandDeltaLengthByteArrayDecoderare concrete classes;PageReaderholds the right pointer type for each encoding. Removes vtable dispatch from per-rowreadString().std::optionalplus a newreset(pageData_)method instead ofmake_unique<RleBpDecoder>()per page. Saves three heap allocations per page boundary.std::string lastValue_. Replace withstd::vector<char>+ length. Profiling showed ~6.5% instd::string::_M_replacebecause the prefix-share encoding repeatedly mutates the previous-value buffer insidereadString(). The vector path uses rawresize+memcpywith no allocator interaction once the buffer reaches max suffix size.Bench
TPC-H Q12 SF10, on top of PR A:
End-to-end (PR A + PR B vs facebookincubator#17633 baseline):
Test plan
E2EFilterTest.stringDeltaByteArray(covers prefix+suffix) passes (2.8 s)E2EFilterTest.stringDeltaLengthByteArray(covers length-only encoding, no prefix) passes (1.9 s)velox_parquet_e2e_filter_test34/34 greenTracking
Part of #2. Stacked on #3.