Skip to content

[improvement](parquet) Vectorize File Scanner V2 decode hot paths#65972

Open
Gabriel39 wants to merge 2 commits into
apache:masterfrom
Gabriel39:agent/parquet-v2-simd-decode
Open

[improvement](parquet) Vectorize File Scanner V2 decode hot paths#65972
Gabriel39 wants to merge 2 commits into
apache:masterfrom
Gabriel39:agent/parquet-v2-simd-decode

Conversation

@Gabriel39

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

File Scanner V2 still executes several fixed-width Parquet decode and filter stages with scalar row loops. On hot scans, byte-stream-split transpose, DELTA_BINARY_PACKED reconstruction, numeric dictionary materialization, nullable expansion, and raw predicate comparison can become CPU bottlenecks after I/O is cached.

What is changed?

  • Add runtime-dispatched AVX2 kernels with scalar fallbacks for:
    • BYTE_STREAM_SPLIT 4-byte and 8-byte transpose
    • DELTA_BINARY_PACKED INT32/INT64 prefix reconstruction
    • 4-byte and 8-byte numeric dictionary ID gather
    • in-place nullable compact-value expansion
    • raw INT32/INT64/FLOAT/DOUBLE predicate comparison
  • Wire the kernels only into File Scanner V2:
    • native byte-stream-split and delta decoders
    • cache-resident direct dictionary materialization
    • ColumnChunkReader nullable POD expansion
    • Parquet raw fixed-width predicate evaluation
  • Preserve the existing generic paths for unsupported widths, small batches, non-AVX2 CPUs, and non-cache-resident dictionary strategies.
  • Preserve Parquet wrapping arithmetic and Doris floating-point ordering (NaN == NaN, and NaN sorts above finite values).
  • Extend the Parquet benchmark matrix:
    • decoder selectivity boundaries: 0%, 1%, 10%, 50%, 90%, 100%
    • 80 isolated ParquetKernel scenarios
    • applicable INT32/INT64/FLOAT/DOUBLE physical types
    • nullable rates and clustered/alternating null placement
    • 32, 4,096, and 262,144 entry dictionary working sets

File Scanner V1 is unchanged.

Check list

  • ParquetSimdKernelsTest: 5/5 passed
  • ParquetBenchmarkScenariosTest: 6/6 passed
  • ASAN CMake objects and Core/Exprs/Format/Util static libraries compiled
  • Real Release benchmark_main.cpp compiled with Doris project flags and -Werror
  • Release kernel benchmark runner registered all 80 scenarios
  • Representative smoke cases for all five kernels passed their correctness oracles
  • git diff --check

The local full benchmark build was not used for performance claims because the host's broken git-submodule pyenv shim forced an unrelated 14k-target OpenBLAS rebuild. Stable before/after performance comparison should use the checked-in matrix on a controlled host.

Add runtime-dispatched AVX2 kernels with scalar fallbacks for byte-stream-split transpose, delta prefix sums, numeric dictionary gather, nullable expansion, and raw predicate comparisons.

Wire the kernels into File Scanner V2 and extend the Parquet benchmark matrix with 80 fixed-width kernel scenarios.
@Gabriel39
Gabriel39 requested a review from yiguolei as a code owner July 23, 2026 13:15
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for three supported-build blockers in the new SIMD translation unit and one benchmark validation/documentation gap.

Critical-checkpoint conclusions:

  • Goal and scope: the patch is focused on vectorizing five File Scanner V2 decode hot paths and leaves V1 unchanged, but it is not portable across Doris's supported build matrix yet.
  • Concurrency, lifecycle, and memory safety: no new threads, locks, shared mutable state, ownership cycles, or non-intuitive resource lifecycles were introduced. The immutable lookup tables, runtime CPU check, dictionary materialization, COW boundary, and rollback paths were reviewed without another substantiated issue.
  • Error handling, data correctness, and nullable behavior: decode statuses, cursor advancement, dictionary-ID validation, rollback, byte-stream-split ordering, delta carry propagation, raw-comparison masks/NaNs, and nullable expansion were traced. No additional correctness defect remained after resolving the nullable-payload candidate against the exact pre-patch constructors and downstream null replacement.
  • Configuration, compatibility, and parallel paths: there are no new configs, persisted formats, protocol symbols, FE-BE variables, transactions, or data-write semantics. The AVX2, baseline-x86, and non-x86 paths were all reviewed; the inline P1 comments identify failures in the latter two supported configurations.
  • Conditions, tests, and results: the width/threshold/tail conditions and focused SIMD tests are generally appropriate, but there is no coverage for the failing non-x86, no-PCH, or USE_AVX2=0 compilation modes. The expanded decoder cases also lack an untimed result oracle and leave the mandatory registration-count guide stale. No generated result files changed.
  • Observability and performance: no new production logging or metrics are required for these local kernels. The microbenchmarks improve coverage, but the PR provides no controlled before/after comparison, so no unmeasured speedup or scalar regression was treated as established.

Validation was static as required by the review task; no local build or test was run, and this checkout lacks .worktree_initialized, thirdparty/installed, and protoc. At submission time, CI compile/style checks passed, the performance job failed, and several test/regression jobs were still pending. No additional user focus was supplied. All 16 changed files were swept, all candidates were resolved, and the production, benchmark, and risk reviewers converged on this four-comment set.

Comment thread be/src/util/simd/parquet_kernels.cpp Outdated
namespace doris::parquet_simd {
namespace {

bool has_avx2() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Guard this helper on non-x86 builds

When DORIS_PARQUET_X86_SIMD is absent, every call to has_avx2() is compiled out, but this anonymous-namespace definition remains. Doris builds ARM with -Wall -Wextra -Werror (and Clang enables -Wunused), so the unused internal function is a build error before the scalar fallback can run. Please put the helper itself under the x86 guard (or otherwise remove the non-x86 definition) and cover an arm64/AArch64 compile.

for (size_t lane = 0; lane < 8; ++lane) {
valid_mask |= static_cast<uint32_t>(nulls[start + lane] == 0) << lane;
}
const size_t valid = std::popcount(valid_mask);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Include the header that declares std::popcount

This translation unit uses std::popcount here and at line 237 but never includes <bit>. GCC builds explicitly default to ENABLE_PCH=OFF, so a transitive declaration from Clang's PCH cannot make this a supported build contract. Please add a direct #include <bit> and validate the no-PCH configuration.

Comment thread be/src/util/simd/parquet_kernels.cpp Outdated
}

template <typename Vec>
Vec combine_comparison(Vec equal, Vec greater, Vec less, RawComparisonOp op) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep every AVX2 intrinsic helper in an AVX2 target scope

combine_comparison (including its generated lambda call operators) directly invokes _mm256_* intrinsics and returns 256-bit vectors, but it has no target("avx2") scope. The attribute on raw_compare_*_avx2 does not propagate to a separately instantiated helper, so the supported USE_AVX2=0 x86 build encounters AVX ABI/always-inline target-mismatch diagnostics before runtime dispatch. Please inline this selection into the annotated functions or place the complete helper implementation in an AVX2 target scope, then compile-check USE_AVX2=0.

inline bool register_decoder_benchmarks() {
for (const auto& scenario : decoder_scenarios()) {
for (const int selectivity : {1, 10, 50, 100}) {
for (const int selectivity : {0, 1, 10, 50, 90, 100}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make the new decoder boundary cases verifiable

This loop now registers 19 * 6 * 2 = 228 decoder cases, plus 80 new kernel cases, while the mandatory be/benchmark/parquet/AGENTS.md still tells maintainers to expect 152 and omits ParquetKernel. Also, run_decoder only resets each sink's consumed field; it never checks that field against plan.selected_rows or validates representative decoded values outside timing. Consequently sel_0 can emit rows, or sel_90 can return the wrong selection/payload, and still publish a successful benchmark. Please update the current count guidance and add an untimed count/checksum oracle before measuring these new cases.

### What problem does this PR solve?

Issue Number: None

Related PR: apache#65972

Problem Summary: GCC 15 rejected the vector comparison helper because it inspected attributed AVX vector types through std::is_same, baseline and non-x86 builds did not keep every architecture-specific helper in a valid scope, and the expanded decoder benchmark matrix did not verify selected output. Keep all AVX2 operations in explicitly targeted helpers, keep scalar comparison available on non-x86 builds, include the direct popcount dependency, move the reusable kernels to the doris::simd namespace, and validate decoder counts plus deterministic value checksums outside the timed region. Update the documented and tested benchmark registration counts.

### Release note

None

### Check List (For Author)

- Test: Unit Test and Manual test
    - Parquet SIMD kernel unit tests: 5 passed
    - Parquet benchmark scenario unit tests: 7 passed
    - GCC 15 AVX2, baseline x86 without AVX2, and scalar-only AArch64-emulated compile checks
    - Release benchmark target compiled and linked; 228 decoder and 80 kernel smoke cases completed without errors
- Behavior changed: No
- Does this need documentation: No
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for one remaining BYTE_STREAM_SPLIT fallback-performance regression.

Critical-checkpoint conclusions:

  • Goal, scope, and proof: the patch coherently adds runtime-dispatched kernels for five File Scanner V2 hot paths and leaves V1 unchanged. The functional SIMD/scalar contracts are generally preserved, but the BSS integration does not preserve the existing generic path for unsupported widths, short batches, and non-AVX2 CPUs as the PR states.
  • Size and clarity: the shared kernel abstraction and benchmark additions are understandable, but replacing the established BSS scalar decoder is an avoidable expansion of the optimization's scope; the inline P2 identifies the remaining change needed.
  • Concurrency, lifecycle, and memory safety: these kernels introduce no threads, locks, shared mutable state, cross-TU initializer dependency, ownership cycle, or special resource lifecycle. Dictionary COW/materialization and late-error rollback were traced without another finding.
  • Data correctness, error handling, and conditions: BSS coordinates/tails, delta carry and wrapping arithmetic, dictionary-ID validation, nullable in-place expansion, raw-predicate operand reversal, masks, NaNs, and pre-consumption fallback behavior were checked. No data-correctness issue remains. The TIMESTAMPTZ NULL-payload candidate was dismissed because nullable access is null-map-gated and validity-sensitive consumers replace NULL payloads with the type default before evaluation.
  • Configuration, compatibility, and parallel paths: no configuration, persisted format, protocol, function symbol, FE-BE variable, transaction, or data-write behavior changes. File Scanner V1 is unchanged. The supported arm64, baseline-x86, unsupported-width, and short-batch BSS paths are precisely where the inline performance issue applies.
  • Tests and results: the 228 decoder, 80 kernel, and 151 reader registrations, decoder checksums, setup timing, and mutable-input resets are coherent. Current-head commit metadata records seven scenario tests although the PR body still says 6/6. Existing tests cover scalar correctness for a 31-value 4/8-byte case, but there is no forced-scalar before/after guard for widths 1/2/4/8/12/16 or arbitrary fixed lengths, so the fallback replacement is not performance-protected.
  • Observability: no additional production logging or metrics are needed for these local kernels.
  • Performance: the inline P2 is implementation-based rather than an inferred speedup claim: production now reaches a row-major byte loop where the existing decoder provides width-1 memcpy and an explicitly tuned blocked scalar implementation.

Validation was static as required by the review task; no local Doris build or test was run. This checkout lacks .worktree_initialized, thirdparty/installed, and thirdparty/installed/bin/protoc. At submission time, compile, Cloud UT, FE UT, formatting, style, license, and secret checks passed; coverage and performance checks failed; BE UT, macOS BE UT, code review, and several regression checks remained pending. No additional user-provided review focus was supplied. All 17 changed files were swept, every candidate was adjudicated, and three review rounds converged on this single non-duplicate comment.

return;
}
#endif
byte_stream_split_decode_scalar(src, width, offset, num_values, stride, dest);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the optimized BYTE_STREAM_SPLIT scalar fallback

Both production BYTE_STREAM_SPLIT read paths now enter this dispatcher for every width, but AVX2 only handles 4/8-byte batches with at least 32 rows. Every other case reaches this byte-at-a-time fallback, replacing the existing width-1 bulk memcpy and the explicitly performance-tuned 128-row scalar implementation for widths 2/4/8/16 and dynamic widths. That regresses supported INT96 and arbitrary-width FIXED_LEN_BYTE_ARRAY decodes, short 4/8-byte batches, and all reads on arm64 or baseline x86—the exact fallback configurations the PR says remain preserved. Call the existing optimized decoder here (or retain an equivalent implementation), and add forced-scalar before/after coverage for the supported widths.

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.

2 participants