Skip to content

perf(parquet): Batch DELTA decode + SIMD filter pushdown#3

Closed
jaylisde wants to merge 1 commit into
mainfrom
pr/delta-bp-perf
Closed

perf(parquet): Batch DELTA decode + SIMD filter pushdown#3
jaylisde wants to merge 1 commit into
mainfrom
pr/delta-bp-perf

Conversation

@jaylisde

@jaylisde jaylisde commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

Layered optimizations on top of facebookincubator#17633 to close the remaining DELTA-vs-PLAIN/DICT gap on TPC-H Q12 (DELTA-encoded lineitem).

  1. Batched fast path in DeltaBpDecoder::readWithVisitor — for dense integer reads with deterministic filters and no hook, decode a chunk into the visitor's output buffer and dispatch one visitor.processRun() per chunk.
  2. Inline readLong state hoist — keep bufferStart_, the two remaining counters, lastValue_, minDelta_ and deltaBitWidth_ in registers across the inner loop; advance a running bitOffset instead of recomputing valuesConsumed * deltaBitWidth_ per row.
  3. New base ColumnVisitor::processRun with two SIMD subpaths via dwio::common::processFixedFilter. Specialized visitors keep their own overrides; this base entry is reachable only from the new DELTA batched path. Includes a scalar-tail / SIMD-counter sync fix (commit 5e5071fc) — without it, numInput < kWidth cases let the scalar tail overwrite earlier rows because the surrounding setNumValues(local) doesn't include the tail's appends. New test deltaBinaryPackedFilterScalarTail reproduces the bug on the pre-fix variant.
  4. Seven boundary tests in ParquetTableScanTest covering bit_width=0 constant deltas, narrow widths, the 32/33-bit boundary, mixed miniblock widths, a wide-and-negative pattern, and the scalar-tail regression above.

Bench

TPC-H Q12 SF10, DELTA-encoded lineitem (3 runs, median):

Wall Scan CPU
facebookincubator#17633 baseline 1.02 s 2.67 s
+ readLong inline hoist 0.92 s 2.42 s
+ decodeLongs batch decode 0.86 s 2.18 s
+ SIMD filter via processRun 0.85 s 2.13 s

PLAIN/DICT (default) parquet unchanged within noise (614 → 608 ms scan CPU on Q12 SF10 PLAIN).

Test plan

  • 6 new boundary tests pass: velox_dwio_parquet_table_scan_test --gtest_filter=*deltaBinaryPacked*
  • Full velox_dwio_parquet_table_scan_test suite still green
  • TPC-H Q12 SF10 wall reduces from 1.02 s → 0.85 s on this branch
  • PR B (DeltaByteArrayDecoder rework) brings Q12 SF10 to 0.74 s

Tracking

Part of #2.

@jaylisde
jaylisde force-pushed the pr/delta-bp-perf branch from 92e33b7 to 2d17343 Compare May 30, 2026 07:14
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
jaylisde force-pushed the pr/delta-bp-perf branch from 5e5071f to a53751d Compare June 3, 2026 06:30
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.
Layered optimizations on top of facebookincubator#17633 to close the remaining
DELTA-vs-PLAIN/DICT scan gap on TPC-H Q12 (DELTA-encoded lineitem).

1. Batched fast path in DeltaBpDecoder::readWithVisitor — for dense
   integer reads with deterministic filters and no hook, decode a chunk
   into the visitor's output buffer and dispatch one
   visitor.processRun() per chunk.

2. Inline readLong() state hoist in decodeLongs — keeps bufferStart_,
   the two remaining counters, lastValue_, minDelta_ and deltaBitWidth_
   in registers across the inner loop; advances a running bitOffset
   instead of recomputing valuesConsumed * deltaBitWidth_ per row.

3. New base ColumnVisitor::processRun with two SIMD subpaths via
   dwio::common::processFixedFilter. Specialized visitors keep their
   own overrides; this base entry is reachable only from the new DELTA
   batched path.

4. Fix scalar-tail / SIMD-counter sync in processRun. The dense
   batched caller commits the SIMD loop's local counter via
   setNumValues(numValues), but the scalar tail's process() advances
   the reader's own numValues_ counter, so any rows produced by the
   tail when numInput < kWidth were silently dropped. Sync the local
   into the reader before the tail and pull the tail's increments back
   into the local after, so the final commit covers both phases.

5. Seven boundary tests in ParquetTableScanTest covering bit_width=0
   constant deltas, narrow widths, the 32/33-bit boundary, mixed
   miniblock widths, a wide-and-negative pattern, and the scalar-tail
   regression above (deltaBinaryPackedFilterScalarTail).
@jaylisde
jaylisde force-pushed the pr/delta-bp-perf branch from a53751d to 4dde7ca Compare June 3, 2026 06:49
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

jaylisde commented Jun 3, 2026

Copy link
Copy Markdown
Owner Author

Squashed into combined PR #6 (perf/delta-decoder-overhaul). Closing this stacked sub-PR — review continues on #6.

@jaylisde jaylisde closed this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant