This is an automated email from the ASF dual-hosted git repository.

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new a765b36ebc2 [improvement](parquet) Vectorize File Scanner V2 decode 
hot paths (#65972)
a765b36ebc2 is described below

commit a765b36ebc2cb9ea882ae1bdcfb37fedb3a9599b
Author: Gabriel <[email protected]>
AuthorDate: Fri Jul 24 15:59:27 2026 +0800

    [improvement](parquet) Vectorize File Scanner V2 decode hot paths (#65972)
    
    ### 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
    
    - [x] `ParquetSimdKernelsTest`: 6/6 passed
    - [x] `ParquetBenchmarkScenariosTest`: 9/9 passed
    - [x] ASAN CMake objects and the Release `benchmark_test` target
    compiled and linked
    - [x] Real Release `benchmark_main.cpp` compiled with Doris project
    flags and `-Werror`
    - [x] Release benchmark runner registered 228 decoder, 80 kernel, and
    152 reader scenarios
    - [x] All 308 decoder and kernel smoke cases passed with zero benchmark
    errors
    - [x] `git diff --check`
    
    The Release benchmark build and smoke run are execution validation only.
    Stable before/after performance comparison should use the checked-in
    matrix on a controlled host.
---
 be/benchmark/benchmark_main.cpp                    |   1 +
 be/benchmark/parquet/AGENTS.md                     |  54 +-
 be/benchmark/parquet/README.md                     |  19 +
 be/benchmark/parquet/benchmark_parquet_decoder.hpp | 187 +++++-
 be/benchmark/parquet/benchmark_parquet_kernels.hpp | 243 ++++++++
 be/benchmark/parquet/parquet_benchmark_scenarios.h |  71 +++
 .../core/data_type_serde/parquet_decode_source.cpp |  78 +++
 .../core/data_type_serde/parquet_decode_source.h   |   8 +-
 be/src/exprs/vectorized_fn_call.cpp                |  33 +-
 .../reader/native/byte_stream_split_decoder.cpp    |  23 +-
 .../parquet/reader/native/column_chunk_reader.cpp  |   8 +
 .../parquet/reader/native/delta_bit_pack_decoder.h |  11 +-
 be/src/util/simd/parquet_kernels.cpp               | 651 +++++++++++++++++++++
 be/src/util/simd/parquet_kernels.h                 |  48 ++
 be/test/format_v2/parquet/native_decoder_test.cpp  |   8 +-
 .../parquet/parquet_benchmark_scenarios_test.cpp   |  64 +-
 .../parquet/parquet_simd_kernels_test.cpp          | 222 +++++++
 17 files changed, 1654 insertions(+), 75 deletions(-)

diff --git a/be/benchmark/benchmark_main.cpp b/be/benchmark/benchmark_main.cpp
index 582db25ff6d..7c64fa2729c 100644
--- a/be/benchmark/benchmark_main.cpp
+++ b/be/benchmark/benchmark_main.cpp
@@ -45,6 +45,7 @@
 #include "core/data_type/data_type.h"
 #include "core/data_type/data_type_string.h"
 #include "parquet/benchmark_parquet_decoder.hpp"
+#include "parquet/benchmark_parquet_kernels.hpp"
 #include "parquet/benchmark_parquet_reader.hpp"
 #include "runtime/exec_env.h"
 #include "runtime/memory/mem_tracker_limiter.h"
diff --git a/be/benchmark/parquet/AGENTS.md b/be/benchmark/parquet/AGENTS.md
index a558f69937c..ba52658a966 100644
--- a/be/benchmark/parquet/AGENTS.md
+++ b/be/benchmark/parquet/AGENTS.md
@@ -6,9 +6,10 @@ benchmark system described in the design document.
 
 ## What exists today
 
-The benchmark binary registers two groups:
+The benchmark binary registers three groups:
 
 - `ParquetDecoder`: native page decoder benchmarks using in-memory encoded 
pages.
+- `ParquetKernel`: isolated SIMD-sensitive decode and predicate kernels.
 - `ParquetReader`: local-file benchmarks that call the format V2 Parquet 
reader directly.
 
 The relevant files are:
@@ -36,10 +37,13 @@ List all Parquet cases and verify the expected registration 
counts:
 
 ```shell
 be/output/lib/benchmark_test --benchmark_list_tests \
-  | grep -E '^Parquet(Decoder|Reader)/'
+  | grep -E '^Parquet(Decoder|Kernel|Reader)/'
 
 be/output/lib/benchmark_test --benchmark_list_tests \
-  | grep -c '^ParquetDecoder/'  # currently 152
+  | grep -c '^ParquetDecoder/'  # currently 228
+
+be/output/lib/benchmark_test --benchmark_list_tests \
+  | grep -c '^ParquetKernel/'   # currently 80
 
 be/output/lib/benchmark_test --benchmark_list_tests \
   | grep -c '^ParquetReader/'   # currently 152
@@ -61,6 +65,12 @@ be/output/lib/benchmark_test \
   --benchmark_out=parquet-decoder-smoke.json \
   --benchmark_out_format=json
 
+be/output/lib/benchmark_test \
+  --benchmark_filter='^ParquetKernel/' \
+  --benchmark_min_time=0.001s \
+  --benchmark_out=parquet-kernel-smoke.json \
+  --benchmark_out_format=json
+
 be/output/lib/benchmark_test \
   --benchmark_filter='^ParquetReader/' \
   --benchmark_min_time=0.001s \
@@ -98,8 +108,8 @@ cache to manufacture a cold run.
 
 ## Current scenario matrix
 
-`ParquetDecoder` contains 19 encoding/type pairs. Each pair is run at 1%, 10%, 
50%, and 100%
-selection with clustered and alternating selection ranges, for 152 registered 
cases.
+`ParquetDecoder` contains 19 encoding/type pairs. Each pair is run at 0%, 1%, 
10%, 50%, 90%, and
+100% selection with clustered and alternating selection ranges, for 228 
registered cases.
 
 | Encoding | Physical types |
 |---|---|
@@ -110,6 +120,11 @@ selection with clustered and alternating selection ranges, 
for 152 registered ca
 | DELTA_LENGTH_BYTE_ARRAY | BYTE_ARRAY |
 | DELTA_BYTE_ARRAY | BYTE_ARRAY |
 
+`ParquetKernel` contains 80 cases across five SIMD-sensitive stages: 
BYTE_STREAM_SPLIT,
+DELTA_PREFIX_SUM, DICTIONARY_GATHER, NULLABLE_EXPAND, and RAW_PREDICATE. It 
covers the applicable
+four- and eight-byte types, three dictionary working-set sizes, 0% through 90% 
null rates with both
+placement patterns, and 0% through 100% raw-predicate selectivities.
+
 `ParquetReader` deliberately uses a single-variable matrix rather than a 
Cartesian product. After
 deduplication it contains 152 cases covering:
 
@@ -147,10 +162,12 @@ generator, random seed, or manifest involved.
   and intentionally produces many one-row physical ranges.
 
 Page generation, selection construction, decoder creation, dictionary setup, 
and `set_data` are
-outside the timed decode call. The sinks consume decoder callbacks and prevent 
compiler removal,
-but they do not build a Doris `Column`. Consequently these cases isolate 
decoder traversal and
-selection cost; they do not measure definition-level decoding, nullable 
reconstruction, type
-conversion, or full column materialization.
+outside the timed decode call. Before timing, every decoder case verifies the 
consumed value count
+and a checksum of all selected values against the deterministic source 
generator. The timed sinks
+then consume decoder callbacks and prevent compiler removal, but they do not 
build a Doris
+`Column`. Consequently these cases isolate decoder traversal and selection 
cost; they do not
+measure definition-level decoding, nullable reconstruction, type conversion, 
or full column
+materialization.
 
 ## How reader Parquet files are generated
 
@@ -238,13 +255,14 @@ The current matrix is not comprehensive. Preserve this 
distinction in PR descrip
 1. Add the design's `matrix.yaml`, deterministic corpus generator, 
`manifest.json`, checksum, and
    standalone corpus verifier. The current runtime-generated files cannot be 
shared unchanged with
    V1, StarRocks, or DuckDB.
-2. Add correctness oracles. Benchmarks must validate consumed counts and 
representative checksums
-   outside the timed region before their performance samples are trusted.
+2. Add full reader correctness oracles. Decoder cases validate consumed counts 
and selected-value
+   checksums outside the timed region, and kernel cases compare representative 
output. Reader cases
+   still need value checksums in addition to their output-row counters.
 3. Add reader-level INT64, FLOAT, DOUBLE, BYTE_ARRAY/string, 
FIXED_LEN_BYTE_ARRAY, DATE,
    TIMESTAMP, and DECIMAL cases. Today only nullable INT32 reaches the 
complete reader path.
-4. Extend decoder coverage with 0% and 90% selection, definition levels/null 
reconstruction,
-   dictionary conversion, and real Doris `Column` materialization. The current 
decoder sink does
-   not cover those costs or report decoded bytes per second.
+4. Extend decoder coverage with definition levels/null reconstruction, 
dictionary conversion, and
+   real Doris `Column` materialization. The current decoder sink does not 
cover those costs or
+   report decoded bytes per second.
 5. Add the representative nullable sparse corpus requested by the design: 32 
INT64 columns, 128
    row groups, and enough rows to exercise many pages. The current 
16K-row/four-row-group fixture
    is a smoke-sized workload.
@@ -281,10 +299,10 @@ be simulated by silently changing the local reader 
benchmark.
 
 ## Current validation record
 
-At commit `16e05dd5c71`, a Release build completed and the matrix unit test 
passed 4/4. A 1 ms smoke
-run executed 152 decoder and 137 reader cases with zero benchmark errors. This 
is an execution
-record only. It is not a reviewed performance baseline because repetitions, 
host isolation,
-warmups, cache control, `perf` data, variance, and before/after comparison 
were not collected.
+The current expected registration counts are 228 decoder, 80 kernel, and 152 
reader cases. A smoke
+run is an execution record only, not a reviewed performance baseline, because 
repetitions, host
+isolation, warmups, cache control, `perf` data, variance, and before/after 
comparison are not
+collected.
 
 ## Rules for extending the suite
 
diff --git a/be/benchmark/parquet/README.md b/be/benchmark/parquet/README.md
index f024f7d6a24..e6eab6362ce 100644
--- a/be/benchmark/parquet/README.md
+++ b/be/benchmark/parquet/README.md
@@ -28,12 +28,31 @@ timed region. It covers PLAIN, dictionary, 
byte-stream-split, and DELTA encoding
 supported fixed-width and binary physical types. Sparse selections are 
provided as both one
 clustered range and many alternating ranges.
 
+The decoder selection axis includes 0%, 1%, 10%, 50%, 90%, and 100% so 
boundary and
+high-selectivity behavior are visible.
+
 ```shell
 be/output/lib/benchmark_test \
   --benchmark_filter='^ParquetDecoder/plain/int64/sel_10/alternating$' \
   --benchmark_min_time=0.1s
 ```
 
+## SIMD kernel cases
+
+`ParquetKernel` isolates the five SIMD-sensitive stages from reader setup and 
virtual consumer
+overhead: byte-stream-split transpose, delta prefix sum, numeric dictionary 
gather, nullable
+expansion, and raw predicate evaluation. It covers the applicable 4-byte and 
8-byte integer and
+floating-point physical types, raw-predicate selectivities from 0% through 
100%, and nullable
+rates from 0% through 90% with clustered and alternating placement. Dictionary 
gather uses 32-,
+4,096-, and 262,144-entry working sets to separate cache-resident and 
cache-miss-dominated
+behavior.
+
+```shell
+be/output/lib/benchmark_test \
+  --benchmark_filter='^ParquetKernel/(dictionary_gather|nullable_expand)/' \
+  --benchmark_min_time=0.1s
+```
+
 ## Local reader cases
 
 `ParquetReader` measures local open-to-first-block, full scan, predicate scan, 
complex residual
diff --git a/be/benchmark/parquet/benchmark_parquet_decoder.hpp 
b/be/benchmark/parquet/benchmark_parquet_decoder.hpp
index 25fb05d1449..c1c2e3cb332 100644
--- a/be/benchmark/parquet/benchmark_parquet_decoder.hpp
+++ b/be/benchmark/parquet/benchmark_parquet_decoder.hpp
@@ -101,31 +101,87 @@ inline std::shared_ptr<::parquet::ColumnDescriptor> 
descriptor(::parquet::Type::
     return std::make_shared<::parquet::ColumnDescriptor>(node, 0, 0);
 }
 
+template <typename T>
+T fixed_value(size_t row) {
+    if constexpr (std::is_floating_point_v<T>) {
+        return static_cast<T>((row % 1009) * 0.25 - 100.0);
+    }
+    return static_cast<T>((row * 17) % 1000003);
+}
+
 template <typename T>
 std::vector<T> fixed_values(size_t rows) {
     std::vector<T> values(rows);
     for (size_t row = 0; row < rows; ++row) {
-        if constexpr (std::is_floating_point_v<T>) {
-            values[row] = static_cast<T>((row % 1009) * 0.25 - 100.0);
-        } else {
-            values[row] = static_cast<T>((row * 17) % 1000003);
-        }
+        values[row] = fixed_value<T>(row);
     }
     return values;
 }
 
+inline std::string binary_value(size_t row, size_t width = FIXED_BINARY_WIDTH) 
{
+    std::string value(width, 'a');
+    const uint64_t id = row % 1009;
+    memcpy(value.data(), &id, std::min(width, sizeof(id)));
+    return value;
+}
+
 inline std::vector<std::string> binary_values(size_t rows, size_t width = 
FIXED_BINARY_WIDTH) {
     std::vector<std::string> values;
     values.reserve(rows);
     for (size_t row = 0; row < rows; ++row) {
-        std::string value(width, 'a');
-        const uint64_t id = row % 1009;
-        memcpy(value.data(), &id, std::min(width, sizeof(id)));
-        values.push_back(std::move(value));
+        values.push_back(binary_value(row, width));
     }
     return values;
 }
 
+struct DecoderDigest {
+    size_t consumed = 0;
+    uint64_t checksum = 1469598103934665603ULL;
+};
+
+inline void add_digest_value(DecoderDigest* digest, const uint8_t* bytes, 
size_t size) {
+    constexpr uint64_t FNV_PRIME = 1099511628211ULL;
+    digest->checksum ^= size;
+    digest->checksum *= FNV_PRIME;
+    for (size_t byte = 0; byte < size; ++byte) {
+        digest->checksum ^= bytes[byte];
+        digest->checksum *= FNV_PRIME;
+    }
+    ++digest->consumed;
+}
+
+inline void add_generated_value(DecoderDigest* digest, ValueType value_type, 
size_t row) {
+    switch (value_type) {
+    case ValueType::INT32: {
+        const auto value = fixed_value<int32_t>(row);
+        add_digest_value(digest, reinterpret_cast<const uint8_t*>(&value), 
sizeof(value));
+        return;
+    }
+    case ValueType::INT64: {
+        const auto value = fixed_value<int64_t>(row);
+        add_digest_value(digest, reinterpret_cast<const uint8_t*>(&value), 
sizeof(value));
+        return;
+    }
+    case ValueType::FLOAT: {
+        const auto value = fixed_value<float>(row);
+        add_digest_value(digest, reinterpret_cast<const uint8_t*>(&value), 
sizeof(value));
+        return;
+    }
+    case ValueType::DOUBLE: {
+        const auto value = fixed_value<double>(row);
+        add_digest_value(digest, reinterpret_cast<const uint8_t*>(&value), 
sizeof(value));
+        return;
+    }
+    case ValueType::BYTE_ARRAY:
+    case ValueType::FIXED_LEN_BYTE_ARRAY: {
+        const auto value = binary_value(row);
+        add_digest_value(digest, reinterpret_cast<const 
uint8_t*>(value.data()), value.size());
+        return;
+    }
+    }
+    throw std::logic_error("unknown Parquet benchmark value type");
+}
+
 inline std::vector<uint8_t> encode_plain_binary(const 
std::vector<std::string>& values) {
     size_t bytes = 0;
     for (const auto& value : values) {
@@ -376,6 +432,67 @@ private:
     const size_t _value_width;
 };
 
+class FixedVerificationSink final : public ParquetFixedValueConsumer {
+public:
+    Status consume(const uint8_t* values, size_t num_values, size_t 
value_width) override {
+        for (size_t row = 0; row < num_values; ++row) {
+            add_digest_value(&digest, values + row * value_width, value_width);
+        }
+        return Status::OK();
+    }
+
+    DecoderDigest digest;
+};
+
+class BinaryVerificationSink final : public ParquetBinaryValueConsumer {
+public:
+    Status consume(const StringRef* values, size_t num_values) override {
+        for (size_t row = 0; row < num_values; ++row) {
+            add_digest_value(&digest, reinterpret_cast<const 
uint8_t*>(values[row].data),
+                             values[row].size);
+        }
+        return Status::OK();
+    }
+
+    Status consume_plain_byte_array(
+            const char* encoded_data, const uint32_t* payload_offsets,
+            const uint32_t* value_offsets, size_t num_values,
+            const std::vector<ParquetSelectionRange>& value_spans) override {
+        for (size_t row = 0; row < num_values; ++row) {
+            add_digest_value(&digest,
+                             reinterpret_cast<const uint8_t*>(encoded_data + 
payload_offsets[row]),
+                             value_offsets[row + 1] - value_offsets[row]);
+        }
+        return Status::OK();
+    }
+
+    DecoderDigest digest;
+};
+
+class DictionaryVerificationSink final : public ParquetDictionaryValueConsumer 
{
+public:
+    explicit DictionaryVerificationSink(ValueType value_type) : 
_value_type(value_type) {}
+
+    Status consume_indices(const uint32_t* indices, size_t num_values) 
override {
+        for (size_t row = 0; row < num_values; ++row) {
+            add_generated_value(&digest, _value_type, indices[row]);
+        }
+        return Status::OK();
+    }
+
+    Status consume_repeated(uint32_t index, size_t num_values) override {
+        for (size_t row = 0; row < num_values; ++row) {
+            add_generated_value(&digest, _value_type, index);
+        }
+        return Status::OK();
+    }
+
+    DecoderDigest digest;
+
+private:
+    const ValueType _value_type;
+};
+
 inline ParquetSelection native_selection(const SelectionPlan& plan) {
     ParquetSelection selection {
             .total_values = plan.total_rows, .selected_values = 
plan.selected_rows, .ranges = {}};
@@ -386,6 +503,50 @@ inline ParquetSelection native_selection(const 
SelectionPlan& plan) {
     return selection;
 }
 
+inline DecoderDigest expected_decoder_digest(const DecoderScenario& scenario,
+                                             const SelectionPlan& plan) {
+    DecoderDigest digest;
+    visit_selected_rows(plan, [&](size_t row) {
+        const size_t value_row =
+                scenario.encoding == Encoding::DICTIONARY ? row % 
DICTIONARY_ENTRIES : row;
+        add_generated_value(&digest, scenario.value_type, value_row);
+    });
+    return digest;
+}
+
+inline Status verify_decoder_output(format::parquet::native::Decoder* decoder, 
Slice* encoded,
+                                    const DecoderScenario& scenario, bool 
binary,
+                                    const ParquetSelection& selection,
+                                    const SelectionPlan& plan) {
+    RETURN_IF_ERROR(decoder->set_data(encoded));
+    DecoderDigest actual;
+    if (scenario.encoding == Encoding::DICTIONARY) {
+        DictionaryVerificationSink sink(scenario.value_type);
+        RETURN_IF_ERROR(decoder->decode_selected_dictionary_values(selection, 
sink));
+        actual = sink.digest;
+    } else if (binary) {
+        BinaryVerificationSink sink;
+        RETURN_IF_ERROR(decoder->decode_selected_binary_values(selection, 
sink));
+        actual = sink.digest;
+    } else {
+        FixedVerificationSink sink;
+        RETURN_IF_ERROR(decoder->decode_selected_fixed_values(selection, 
sink));
+        actual = sink.digest;
+    }
+
+    const auto expected = expected_decoder_digest(scenario, plan);
+    // Validate sparse/boundary selections before timing; counters alone can 
hide wrong selected
+    // rows while still producing a plausible benchmark result.
+    if (actual.consumed != plan.selected_rows || actual.consumed != 
expected.consumed ||
+        actual.checksum != expected.checksum) {
+        return Status::InternalError(
+                "Parquet decoder benchmark oracle mismatch: consumed {} 
expected {}, checksum {} "
+                "expected {}",
+                actual.consumed, plan.selected_rows, actual.checksum, 
expected.checksum);
+    }
+    return Status::OK();
+}
+
 inline void run_decoder(benchmark::State& state, DecoderScenario scenario, int 
selectivity,
                         Pattern pattern) {
     auto page = encoded_page(scenario);
@@ -415,6 +576,12 @@ inline void run_decoder(benchmark::State& state, 
DecoderScenario scenario, int s
     }
 
     Slice encoded(page.data.data(), page.data.size());
+    status = verify_decoder_output(decoder.get(), &encoded, scenario, 
page.binary, selection, plan);
+    if (!status.ok()) {
+        state.SkipWithError(status.to_string().c_str());
+        return;
+    }
+
     FixedSink fixed_sink;
     BinarySink binary_sink;
     DictionarySink dictionary_sink(dictionary_for_sink.data(), 
page.value_width);
@@ -459,7 +626,7 @@ inline void run_decoder(benchmark::State& state, 
DecoderScenario scenario, int s
 
 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}) {
             for (const auto pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
                 const std::string name = "ParquetDecoder/" + 
to_string(scenario.encoding) + "/" +
                                          to_string(scenario.value_type) + 
"/sel_" +
diff --git a/be/benchmark/parquet/benchmark_parquet_kernels.hpp 
b/be/benchmark/parquet/benchmark_parquet_kernels.hpp
new file mode 100644
index 00000000000..e064e69f64f
--- /dev/null
+++ b/be/benchmark/parquet/benchmark_parquet_kernels.hpp
@@ -0,0 +1,243 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <benchmark/benchmark.h>
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include "parquet_benchmark_scenarios.h"
+#include "util/byte_stream_split.h"
+#include "util/simd/parquet_kernels.h"
+
+namespace doris::parquet_benchmark {
+namespace detail {
+
+constexpr size_t KERNEL_ROWS = 1UL << 16;
+
+inline void decode_byte_stream_split(const uint8_t* src, size_t width, size_t 
offset,
+                                     size_t num_values, size_t stride, 
uint8_t* dest) {
+    if (!simd::try_byte_stream_split_decode(src, width, offset, num_values, 
stride, dest)) {
+        doris::byte_stream_split_decode(src, static_cast<int>(width), offset, 
num_values, stride,
+                                        dest);
+    }
+}
+
+template <typename T>
+void run_kernel(benchmark::State& state, const KernelScenario& scenario) {
+    constexpr size_t width = sizeof(T);
+    std::vector<T> input(KERNEL_ROWS);
+    for (size_t row = 0; row < input.size(); ++row) {
+        input[row] = scenario.kernel == Kernel::RAW_PREDICATE ? 
static_cast<T>(row % 100)
+                                                              : 
static_cast<T>((row * 17) % 1009);
+    }
+    std::vector<T> output(KERNEL_ROWS);
+
+    std::vector<uint8_t> encoded(KERNEL_ROWS * width);
+    for (size_t row = 0; row < KERNEL_ROWS; ++row) {
+        for (size_t byte = 0; byte < width; ++byte) {
+            encoded[byte * KERNEL_ROWS + row] =
+                    reinterpret_cast<const uint8_t*>(input.data())[row * width 
+ byte];
+        }
+    }
+
+    std::vector<T> dictionary(scenario.dictionary_entries);
+    for (size_t row = 0; row < dictionary.size(); ++row) {
+        dictionary[row] = static_cast<T>(row * 31);
+    }
+    std::vector<uint32_t> ids(KERNEL_ROWS);
+    for (size_t row = 0; row < ids.size(); ++row) {
+        ids[row] = static_cast<uint32_t>((row * 13) % dictionary.size());
+    }
+
+    std::vector<uint8_t> nulls(KERNEL_ROWS, 0);
+    const size_t null_count = KERNEL_ROWS * 
static_cast<size_t>(scenario.null_percent) / 100;
+    if (scenario.pattern == Pattern::CLUSTERED) {
+        std::fill_n(nulls.begin(), null_count, uint8_t {1});
+    } else if (null_count != 0) {
+        for (size_t value = 0; value < null_count; ++value) {
+            nulls[value * KERNEL_ROWS / null_count] = 1;
+        }
+    }
+    std::vector<T> compact;
+    compact.reserve(KERNEL_ROWS - null_count);
+    for (size_t row = 0; row < KERNEL_ROWS; ++row) {
+        if (nulls[row] == 0) {
+            compact.push_back(input[row]);
+        }
+    }
+
+    const T literal = static_cast<T>(scenario.selectivity_percent);
+    std::vector<uint8_t> matches(KERNEL_ROWS, 1);
+
+    switch (scenario.kernel) {
+    case Kernel::BYTE_STREAM_SPLIT:
+        decode_byte_stream_split(encoded.data(), width, 0, KERNEL_ROWS, 
KERNEL_ROWS,
+                                 reinterpret_cast<uint8_t*>(output.data()));
+        if (output != input) {
+            state.SkipWithError("byte-stream-split kernel produced incorrect 
values");
+            return;
+        }
+        break;
+    case Kernel::DELTA_PREFIX_SUM: {
+        if constexpr (std::is_integral_v<T>) {
+            std::copy(input.begin(), input.end(), output.begin());
+            auto expected = output;
+            T expected_last = 7;
+            using Unsigned = std::make_unsigned_t<T>;
+            for (auto& value : expected) {
+                value = static_cast<T>(static_cast<Unsigned>(value) +
+                                       
static_cast<Unsigned>(static_cast<T>(-3)) +
+                                       static_cast<Unsigned>(expected_last));
+                expected_last = value;
+            }
+            T last = 7;
+            simd::delta_decode(output.data(), output.size(), 
static_cast<T>(-3), &last);
+            if (output != expected || last != expected_last) {
+                state.SkipWithError("delta prefix-sum kernel produced 
incorrect values");
+                return;
+            }
+        } else {
+            state.SkipWithError("delta prefix-sum requires an integer physical 
type");
+            return;
+        }
+        break;
+    }
+    case Kernel::DICTIONARY_GATHER:
+        simd::dictionary_gather(reinterpret_cast<const 
uint8_t*>(dictionary.data()), ids.data(),
+                                ids.size(), width, 
reinterpret_cast<uint8_t*>(output.data()));
+        for (size_t row = 0; row < output.size(); ++row) {
+            if (output[row] != dictionary[ids[row]]) {
+                state.SkipWithError("dictionary gather kernel produced 
incorrect values");
+                return;
+            }
+        }
+        break;
+    case Kernel::NULLABLE_EXPAND:
+        std::copy(compact.begin(), compact.end(), output.begin());
+        
simd::expand_nullable_values(reinterpret_cast<uint8_t*>(output.data()), 
compact.size(),
+                                     nulls.data(), nulls.size(), width);
+        for (size_t row = 0; row < output.size(); ++row) {
+            const T expected = nulls[row] == 0 ? input[row] : T {};
+            if (output[row] != expected) {
+                state.SkipWithError("nullable expansion kernel produced 
incorrect values");
+                return;
+            }
+        }
+        break;
+    case Kernel::RAW_PREDICATE:
+        simd::raw_compare(reinterpret_cast<const uint8_t*>(input.data()), 
input.size(), literal,
+                          simd::RawComparisonOp::LT, matches.data());
+        for (size_t row = 0; row < matches.size(); ++row) {
+            if (matches[row] != static_cast<uint8_t>(input[row] < literal)) {
+                state.SkipWithError("raw predicate kernel produced incorrect 
values");
+                return;
+            }
+        }
+        break;
+    }
+
+    for (auto _ : state) {
+        state.PauseTiming();
+        if (scenario.kernel == Kernel::DELTA_PREFIX_SUM) {
+            std::copy(input.begin(), input.end(), output.begin());
+        } else if (scenario.kernel == Kernel::NULLABLE_EXPAND) {
+            std::copy(compact.begin(), compact.end(), output.begin());
+        } else if (scenario.kernel == Kernel::RAW_PREDICATE) {
+            std::fill(matches.begin(), matches.end(), uint8_t {1});
+        }
+        state.ResumeTiming();
+
+        switch (scenario.kernel) {
+        case Kernel::BYTE_STREAM_SPLIT:
+            decode_byte_stream_split(encoded.data(), width, 0, KERNEL_ROWS, 
KERNEL_ROWS,
+                                     
reinterpret_cast<uint8_t*>(output.data()));
+            break;
+        case Kernel::DELTA_PREFIX_SUM: {
+            if constexpr (std::is_integral_v<T>) {
+                T last = 7;
+                simd::delta_decode(output.data(), output.size(), 
static_cast<T>(-3), &last);
+                benchmark::DoNotOptimize(last);
+            }
+            break;
+        }
+        case Kernel::DICTIONARY_GATHER:
+            simd::dictionary_gather(reinterpret_cast<const 
uint8_t*>(dictionary.data()), ids.data(),
+                                    ids.size(), width, 
reinterpret_cast<uint8_t*>(output.data()));
+            break;
+        case Kernel::NULLABLE_EXPAND:
+            
simd::expand_nullable_values(reinterpret_cast<uint8_t*>(output.data()), 
compact.size(),
+                                         nulls.data(), nulls.size(), width);
+            break;
+        case Kernel::RAW_PREDICATE:
+            simd::raw_compare(reinterpret_cast<const uint8_t*>(input.data()), 
input.size(), literal,
+                              simd::RawComparisonOp::LT, matches.data());
+            break;
+        }
+        benchmark::ClobberMemory();
+    }
+
+    state.SetItemsProcessed(static_cast<int64_t>(state.iterations()) *
+                            static_cast<int64_t>(KERNEL_ROWS));
+    state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
+                            static_cast<int64_t>(KERNEL_ROWS * width));
+    state.counters["rows"] = static_cast<double>(KERNEL_ROWS);
+    state.counters["value_width"] = static_cast<double>(width);
+}
+
+inline bool register_kernel_benchmarks() {
+    for (const auto& scenario : kernel_scenarios()) {
+        const std::string name = "ParquetKernel/" + to_string(scenario.kernel) 
+ "/" +
+                                 to_string(scenario.value_type) + "/sel_" +
+                                 std::to_string(scenario.selectivity_percent) 
+ "/null_" +
+                                 std::to_string(scenario.null_percent) + "/" +
+                                 to_string(scenario.pattern) + "/dict_" +
+                                 std::to_string(scenario.dictionary_entries);
+        benchmark::RegisterBenchmark(name.c_str(), [=](benchmark::State& 
state) {
+            switch (scenario.value_type) {
+            case ValueType::INT32:
+                run_kernel<int32_t>(state, scenario);
+                break;
+            case ValueType::INT64:
+                run_kernel<int64_t>(state, scenario);
+                break;
+            case ValueType::FLOAT:
+                run_kernel<float>(state, scenario);
+                break;
+            case ValueType::DOUBLE:
+                run_kernel<double>(state, scenario);
+                break;
+            case ValueType::BYTE_ARRAY:
+            case ValueType::FIXED_LEN_BYTE_ARRAY:
+                state.SkipWithError("kernel benchmark requires a fixed-width 
primitive type");
+                break;
+            }
+        })->Unit(benchmark::kNanosecond);
+    }
+    return true;
+}
+
+inline const bool KERNEL_BENCHMARKS_REGISTERED = register_kernel_benchmarks();
+
+} // namespace detail
+} // namespace doris::parquet_benchmark
diff --git a/be/benchmark/parquet/parquet_benchmark_scenarios.h 
b/be/benchmark/parquet/parquet_benchmark_scenarios.h
index 1db6b3c8fd2..e6ef9f367be 100644
--- a/be/benchmark/parquet/parquet_benchmark_scenarios.h
+++ b/be/benchmark/parquet/parquet_benchmark_scenarios.h
@@ -45,6 +45,13 @@ enum class ReaderOperation {
     LIMIT_1,
     LIMIT_1000
 };
+enum class Kernel {
+    BYTE_STREAM_SPLIT,
+    DELTA_PREFIX_SUM,
+    DICTIONARY_GATHER,
+    NULLABLE_EXPAND,
+    RAW_PREDICATE
+};
 
 struct DecoderScenario {
     Encoding encoding;
@@ -62,6 +69,15 @@ struct ReaderScenario {
     int predicate_position;
 };
 
+struct KernelScenario {
+    Kernel kernel;
+    ValueType value_type;
+    int selectivity_percent;
+    int null_percent;
+    Pattern pattern;
+    size_t dictionary_entries;
+};
+
 struct SelectionRange {
     size_t first;
     size_t count;
@@ -97,6 +113,36 @@ inline std::vector<DecoderScenario> decoder_scenarios() {
     };
 }
 
+inline std::vector<KernelScenario> kernel_scenarios() {
+    std::vector<KernelScenario> scenarios;
+    for (const auto value_type : {ValueType::FLOAT, ValueType::DOUBLE}) {
+        scenarios.push_back(
+                {Kernel::BYTE_STREAM_SPLIT, value_type, 100, 0, 
Pattern::CLUSTERED, 256});
+    }
+    for (const auto value_type : {ValueType::INT32, ValueType::INT64}) {
+        scenarios.push_back(
+                {Kernel::DELTA_PREFIX_SUM, value_type, 100, 0, 
Pattern::CLUSTERED, 256});
+    }
+    for (const auto value_type :
+         {ValueType::INT32, ValueType::INT64, ValueType::FLOAT, 
ValueType::DOUBLE}) {
+        for (const size_t dictionary_entries : {32, 4096, 262144}) {
+            scenarios.push_back({Kernel::DICTIONARY_GATHER, value_type, 100, 
0, Pattern::CLUSTERED,
+                                 dictionary_entries});
+        }
+        for (const int null_percent : {0, 1, 10, 50, 90}) {
+            for (const auto pattern : {Pattern::CLUSTERED, 
Pattern::ALTERNATING}) {
+                scenarios.push_back(
+                        {Kernel::NULLABLE_EXPAND, value_type, 100, 
null_percent, pattern, 256});
+            }
+        }
+        for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
+            scenarios.push_back(
+                    {Kernel::RAW_PREDICATE, value_type, selectivity, 0, 
Pattern::ALTERNATING, 256});
+        }
+    }
+    return scenarios;
+}
+
 inline std::vector<ReaderScenario> reader_scenarios() {
     std::vector<ReaderScenario> scenarios;
     std::set<std::tuple<ReaderOperation, Encoding, int, Pattern, int, 
Projection, int, int>> seen;
@@ -208,6 +254,15 @@ inline SelectionPlan make_selection_plan(size_t 
total_rows, int selectivity_perc
     return plan;
 }
 
+template <typename Visitor>
+inline void visit_selected_rows(const SelectionPlan& plan, Visitor visitor) {
+    for (const auto& range : plan.ranges) {
+        for (size_t offset = 0; offset < range.count; ++offset) {
+            visitor(range.first + offset);
+        }
+    }
+}
+
 inline std::string to_string(Encoding value) {
     switch (value) {
     case Encoding::PLAIN:
@@ -278,4 +333,20 @@ inline std::string reader_scenario_name(const 
ReaderScenario& scenario) {
            "/predicate_" + std::to_string(scenario.predicate_position);
 }
 
+inline std::string to_string(Kernel value) {
+    switch (value) {
+    case Kernel::BYTE_STREAM_SPLIT:
+        return "byte_stream_split";
+    case Kernel::DELTA_PREFIX_SUM:
+        return "delta_prefix_sum";
+    case Kernel::DICTIONARY_GATHER:
+        return "dictionary_gather";
+    case Kernel::NULLABLE_EXPAND:
+        return "nullable_expand";
+    case Kernel::RAW_PREDICATE:
+        return "raw_predicate";
+    }
+    return "unknown";
+}
+
 } // namespace doris::parquet_benchmark
diff --git a/be/src/core/data_type_serde/parquet_decode_source.cpp 
b/be/src/core/data_type_serde/parquet_decode_source.cpp
new file mode 100644
index 00000000000..8a60288811c
--- /dev/null
+++ b/be/src/core/data_type_serde/parquet_decode_source.cpp
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "core/data_type_serde/parquet_decode_source.h"
+
+#include "core/column/column_vector.h"
+#include "util/simd/parquet_kernels.h"
+
+namespace doris {
+namespace {
+
+template <PrimitiveType TYPE>
+bool try_gather_vector(IColumn& destination, const IColumn& dictionary, const 
uint32_t* indices,
+                       size_t num_values) {
+    using ColumnType = ColumnVector<TYPE>;
+    using ValueType = typename ColumnType::value_type;
+    if constexpr (sizeof(ValueType) != 4 && sizeof(ValueType) != 8) {
+        return false;
+    } else {
+        auto* destination_vector = dynamic_cast<ColumnType*>(&destination);
+        const auto* dictionary_vector = dynamic_cast<const 
ColumnType*>(&dictionary);
+        if (destination_vector == nullptr || dictionary_vector == nullptr) {
+            return false;
+        }
+        // The direct strategy is chosen only while the typed dictionary is 
cache-resident. Keep
+        // the existing generic insertion path for tiny batches where gather 
setup cannot amortize.
+        constexpr size_t SIMD_LANES = sizeof(ValueType) == 4 ? 8 : 4;
+        if (num_values < SIMD_LANES) {
+            return false;
+        }
+        auto& destination_data = destination_vector->get_data();
+        const auto& dictionary_data = dictionary_vector->get_data();
+        const size_t old_size = destination_data.size();
+        destination_data.resize(old_size + num_values);
+        simd::dictionary_gather(reinterpret_cast<const 
uint8_t*>(dictionary_data.data()), indices,
+                                num_values, sizeof(ValueType),
+                                
reinterpret_cast<uint8_t*>(destination_data.data() + old_size));
+        return true;
+    }
+}
+
+} // namespace
+
+bool try_simd_insert_parquet_dictionary_indices(IColumn& destination, const 
IColumn& dictionary,
+                                                const uint32_t* indices, 
size_t num_values) {
+#define TRY_PARQUET_GATHER(TYPE) \
+    if (try_gather_vector<TYPE>(destination, dictionary, indices, num_values)) 
return true
+    TRY_PARQUET_GATHER(TYPE_INT);
+    TRY_PARQUET_GATHER(TYPE_BIGINT);
+    TRY_PARQUET_GATHER(TYPE_FLOAT);
+    TRY_PARQUET_GATHER(TYPE_DOUBLE);
+    TRY_PARQUET_GATHER(TYPE_DATE);
+    TRY_PARQUET_GATHER(TYPE_DATETIME);
+    TRY_PARQUET_GATHER(TYPE_DATEV2);
+    TRY_PARQUET_GATHER(TYPE_DATETIMEV2);
+    TRY_PARQUET_GATHER(TYPE_IPV4);
+    TRY_PARQUET_GATHER(TYPE_TIMEV2);
+    TRY_PARQUET_GATHER(TYPE_UINT32);
+    TRY_PARQUET_GATHER(TYPE_UINT64);
+#undef TRY_PARQUET_GATHER
+    return false;
+}
+
+} // namespace doris
diff --git a/be/src/core/data_type_serde/parquet_decode_source.h 
b/be/src/core/data_type_serde/parquet_decode_source.h
index e9762385689..18624abf45e 100644
--- a/be/src/core/data_type_serde/parquet_decode_source.h
+++ b/be/src/core/data_type_serde/parquet_decode_source.h
@@ -253,6 +253,9 @@ public:
 
 enum class ParquetDictionaryMaterializationStrategy : uint8_t { DIRECT, 
INDICES };
 
+bool try_simd_insert_parquet_dictionary_indices(IColumn& destination, const 
IColumn& dictionary,
+                                                const uint32_t* indices, 
size_t num_values);
+
 // Dictionary values are materialized once into the selected Doris type. The 
state belongs to a
 // column reader rather than DataTypeSerDe because a SerDe instance can be 
shared by many files.
 struct ParquetMaterializationState {
@@ -363,7 +366,10 @@ struct ParquetMaterializationState {
                     : _destination(destination), _dictionary(dictionary) {}
 
             Status consume_indices(const uint32_t* indices, size_t num_values) 
override {
-                _destination.insert_indices_from(_dictionary, indices, indices 
+ num_values);
+                if (!try_simd_insert_parquet_dictionary_indices(_destination, 
_dictionary, indices,
+                                                                num_values)) {
+                    _destination.insert_indices_from(_dictionary, indices, 
indices + num_values);
+                }
                 return Status::OK();
             }
 
diff --git a/be/src/exprs/vectorized_fn_call.cpp 
b/be/src/exprs/vectorized_fn_call.cpp
index 48b7ed3eaff..56ba95cd755 100644
--- a/be/src/exprs/vectorized_fn_call.cpp
+++ b/be/src/exprs/vectorized_fn_call.cpp
@@ -28,7 +28,6 @@
 #include <ostream>
 #include <set>
 
-#include "common/compare.h"
 #include "common/config.h"
 #include "common/exception.h"
 #include "common/logging.h"
@@ -67,6 +66,7 @@
 #include "storage/index/zone_map/zonemap_eval_context.h"
 #include "storage/segment/column_reader.h"
 #include "storage/segment/virtual_column_iterator.h"
+#include "util/simd/parquet_kernels.h"
 
 namespace doris {
 class RowDescriptor;
@@ -86,7 +86,7 @@ const static std::set<TExprOpcode::type> 
OPS_FOR_ANN_RANGE_SEARCH = {
 
 namespace {
 
-enum class RawComparisonOp : uint8_t { EQ, NE, LT, LE, GT, GE };
+using simd::RawComparisonOp;
 
 std::optional<RawComparisonOp> raw_comparison_op(std::string_view 
function_name, bool reverse) {
     RawComparisonOp op;
@@ -128,34 +128,7 @@ template <typename T, PrimitiveType PT>
 void execute_raw_comparison(const uint8_t* values, size_t num_values, const 
Field& literal,
                             RawComparisonOp op, uint8_t* matches) {
     const T rhs = literal.get<PT>();
-    for (size_t row = 0; row < num_values; ++row) {
-        if (matches[row] == 0) {
-            continue;
-        }
-        const T lhs = unaligned_load<T>(values + row * sizeof(T));
-        bool keep = false;
-        switch (op) {
-        case RawComparisonOp::EQ:
-            keep = Compare::equal(lhs, rhs);
-            break;
-        case RawComparisonOp::NE:
-            keep = Compare::not_equal(lhs, rhs);
-            break;
-        case RawComparisonOp::LT:
-            keep = Compare::less(lhs, rhs);
-            break;
-        case RawComparisonOp::LE:
-            keep = Compare::less_equal(lhs, rhs);
-            break;
-        case RawComparisonOp::GT:
-            keep = Compare::greater(lhs, rhs);
-            break;
-        case RawComparisonOp::GE:
-            keep = Compare::greater_equal(lhs, rhs);
-            break;
-        }
-        matches[row] = static_cast<uint8_t>(keep);
-    }
+    simd::raw_compare(values, num_values, rhs, op, matches);
 }
 
 } // namespace
diff --git 
a/be/src/format_v2/parquet/reader/native/byte_stream_split_decoder.cpp 
b/be/src/format_v2/parquet/reader/native/byte_stream_split_decoder.cpp
index caf3b675e2d..06629a43557 100644
--- a/be/src/format_v2/parquet/reader/native/byte_stream_split_decoder.cpp
+++ b/be/src/format_v2/parquet/reader/native/byte_stream_split_decoder.cpp
@@ -22,6 +22,7 @@
 
 #include "core/column/column_fixed_length_object.h"
 #include "util/byte_stream_split.h"
+#include "util/simd/parquet_kernels.h"
 
 namespace doris::format::parquet::native {
 Status ByteStreamSplitDecoder::decode_fixed_values(size_t num_values,
@@ -32,8 +33,15 @@ Status ByteStreamSplitDecoder::decode_fixed_values(size_t 
num_values,
     }
     const int64_t stride = static_cast<int64_t>(_data->size / _type_length);
     _decoded_values.resize(byte_size);
-    byte_stream_split_decode(reinterpret_cast<const uint8_t*>(_data->data), 
_type_length,
-                             _offset / _type_length, num_values, stride, 
_decoded_values.data());
+    if (!simd::try_byte_stream_split_decode(reinterpret_cast<const 
uint8_t*>(_data->data),
+                                            _type_length, _offset / 
_type_length, num_values,
+                                            stride, _decoded_values.data())) {
+        // Unsupported widths, short batches, and non-AVX2 hosts must retain 
the tuned blocked
+        // decoder instead of silently falling back to a slower row-by-row 
transpose.
+        doris::byte_stream_split_decode(reinterpret_cast<const 
uint8_t*>(_data->data), _type_length,
+                                        _offset / _type_length, num_values, 
stride,
+                                        _decoded_values.data());
+    }
     _offset += byte_size;
     return consumer.consume(_decoded_values.data(), num_values, 
static_cast<size_t>(_type_length));
 }
@@ -57,9 +65,14 @@ Status 
ByteStreamSplitDecoder::decode_selected_fixed_values(const ParquetSelecti
     size_t output = 0;
     const size_t first_row = _offset / value_width;
     for (const auto& range : selection.ranges) {
-        byte_stream_split_decode(reinterpret_cast<const 
uint8_t*>(_data->data), _type_length,
-                                 first_row + range.first, range.count, stride,
-                                 _decoded_values.data() + output * 
value_width);
+        uint8_t* destination = _decoded_values.data() + output * value_width;
+        if (!simd::try_byte_stream_split_decode(reinterpret_cast<const 
uint8_t*>(_data->data),
+                                                _type_length, first_row + 
range.first, range.count,
+                                                stride, destination)) {
+            doris::byte_stream_split_decode(reinterpret_cast<const 
uint8_t*>(_data->data),
+                                            _type_length, first_row + 
range.first, range.count,
+                                            stride, destination);
+        }
         output += range.count;
     }
     DORIS_CHECK_EQ(output, selection.selected_values);
diff --git a/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp 
b/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp
index 26afaf15bf4..4a97daaf21e 100644
--- a/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp
+++ b/be/src/format_v2/parquet/reader/native/column_chunk_reader.cpp
@@ -50,6 +50,7 @@
 #include "util/bit_util.h"
 #include "util/block_compression.h"
 #include "util/cpu_info.h"
+#include "util/simd/parquet_kernels.h"
 #include "util/unaligned.h"
 
 namespace cctz {
@@ -603,6 +604,13 @@ void expand_nullable_pod_values(ColumnType& column, size_t 
old_size, size_t comp
     auto& data = column.get_data();
     DORIS_CHECK_EQ(data.size(), old_size + compact_values);
     data.resize(old_size + selected_nulls.size());
+    if constexpr (sizeof(typename ColumnType::value_type) == 4 ||
+                  sizeof(typename ColumnType::value_type) == 8) {
+        simd::expand_nullable_values(reinterpret_cast<uint8_t*>(data.data() + 
old_size),
+                                     compact_values, selected_nulls.data(), 
selected_nulls.size(),
+                                     sizeof(typename ColumnType::value_type));
+        return;
+    }
     size_t source = compact_values;
     for (size_t output = selected_nulls.size(); output > 0;) {
         --output;
diff --git a/be/src/format_v2/parquet/reader/native/delta_bit_pack_decoder.h 
b/be/src/format_v2/parquet/reader/native/delta_bit_pack_decoder.h
index db9efecaced..94e248c5fe5 100644
--- a/be/src/format_v2/parquet/reader/native/delta_bit_pack_decoder.h
+++ b/be/src/format_v2/parquet/reader/native/delta_bit_pack_decoder.h
@@ -39,6 +39,7 @@
 #include "format_v2/parquet/reader/native/decoder.h"
 #include "util/bit_stream_utils.h"
 #include "util/bit_stream_utils.inline.h"
+#include "util/simd/parquet_kernels.h"
 #include "util/slice.h"
 
 namespace doris::format::parquet::native {
@@ -787,13 +788,9 @@ Status DeltaBitPackDecoder<T>::_get_internal(T* buffer, 
uint32_t num_values,
                 return Status::IOError("Get batch EOF");
             }
         }
-        for (int j = 0; j < values_decode; ++j) {
-            // Addition between min_delta, packed int and last_value should be 
treated as
-            // unsigned addition. Overflow is as expected.
-            buffer[i + j] = static_cast<UT>(_min_delta) + 
static_cast<UT>(buffer[i + j]) +
-                            static_cast<UT>(_last_value);
-            _last_value = buffer[i + j];
-        }
+        // Parquet defines this recurrence with wrapping integer arithmetic. 
The SIMD kernel keeps
+        // the same unsigned-overflow invariant while resolving the prefix 
dependency in batches.
+        simd::delta_decode(buffer + i, values_decode, _min_delta, 
&_last_value);
         _values_remaining_current_mini_block -= values_decode;
         i += values_decode;
     }
diff --git a/be/src/util/simd/parquet_kernels.cpp 
b/be/src/util/simd/parquet_kernels.cpp
new file mode 100644
index 00000000000..9514ac445b0
--- /dev/null
+++ b/be/src/util/simd/parquet_kernels.cpp
@@ -0,0 +1,651 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "util/simd/parquet_kernels.h"
+
+#include <array>
+#include <bit>
+#include <cmath>
+#include <cstring>
+#include <type_traits>
+
+#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
+#include <immintrin.h>
+#define DORIS_PARQUET_X86_SIMD
+#endif
+
+namespace doris::simd {
+namespace {
+
+#ifdef DORIS_PARQUET_X86_SIMD
+// Keep x86-only dispatch helpers out of scalar-only builds so warning-clean 
ARM builds do not
+// depend on compiler-specific unused-function behavior.
+bool has_avx2() {
+    return __builtin_cpu_supports("avx2");
+}
+
+void byte_stream_split_decode_scalar(const uint8_t* src, size_t width, size_t 
offset,
+                                     size_t num_values, size_t stride, 
uint8_t* dest) {
+    for (size_t row = 0; row < num_values; ++row) {
+        for (size_t byte = 0; byte < width; ++byte) {
+            dest[row * width + byte] = src[byte * stride + offset + row];
+        }
+    }
+}
+#endif
+
+template <typename T>
+bool scalar_compare(T lhs, T rhs, RawComparisonOp op) {
+    const auto equal = [](T left, T right) {
+        if constexpr (std::is_floating_point_v<T>) {
+            return (std::isnan(left) && std::isnan(right)) || left == right;
+        }
+        return left == right;
+    };
+    const auto greater = [](T left, T right) {
+        if constexpr (std::is_floating_point_v<T>) {
+            // Match Doris Compare: NaN is equal to NaN and greater than every 
finite value.
+            if (std::isnan(right)) {
+                return false;
+            }
+            if (std::isnan(left)) {
+                return true;
+            }
+        }
+        return left > right;
+    };
+    switch (op) {
+    case RawComparisonOp::EQ:
+        return equal(lhs, rhs);
+    case RawComparisonOp::NE:
+        return !equal(lhs, rhs);
+    case RawComparisonOp::LT:
+        return greater(rhs, lhs);
+    case RawComparisonOp::LE:
+        return !greater(lhs, rhs);
+    case RawComparisonOp::GT:
+        return greater(lhs, rhs);
+    case RawComparisonOp::GE:
+        return !greater(rhs, lhs);
+    }
+    __builtin_unreachable();
+}
+
+#ifdef DORIS_PARQUET_X86_SIMD
+template <size_t WIDTH>
+__attribute__((target("avx2"))) void byte_stream_split_decode_avx2(const 
uint8_t* src,
+                                                                   size_t 
offset, size_t num_values,
+                                                                   size_t 
stride, uint8_t* dest) {
+    static_assert(WIDTH == 4 || WIDTH == 8);
+    constexpr size_t STEPS = WIDTH == 8 ? 3 : 2;
+    constexpr size_t LANES = sizeof(__m256i);
+    const size_t blocks = num_values / LANES;
+    __m256i stage[STEPS + 1][WIDTH];
+    __m256i result[WIDTH];
+
+    for (size_t block = 0; block < blocks; ++block) {
+        for (size_t stream = 0; stream < WIDTH; ++stream) {
+            stage[0][stream] = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(
+                    src + stream * stride + offset + block * LANES));
+        }
+        for (size_t step = 0; step < STEPS; ++step) {
+            // AVX2 unpack instructions operate independently in each 128-bit 
half. Keep the
+            // byte-transpose hierarchy lane-local, then stitch the halves 
only after the last
+            // unpack so every output vector contains contiguous decoded rows.
+            for (size_t pair = 0; pair < WIDTH / 2; ++pair) {
+                stage[step + 1][pair * 2] =
+                        _mm256_unpacklo_epi8(stage[step][pair], 
stage[step][WIDTH / 2 + pair]);
+                stage[step + 1][pair * 2 + 1] =
+                        _mm256_unpackhi_epi8(stage[step][pair], 
stage[step][WIDTH / 2 + pair]);
+            }
+        }
+        for (size_t pair = 0; pair < WIDTH / 2; ++pair) {
+            result[pair] = _mm256_permute2x128_si256(stage[STEPS][pair * 2],
+                                                     stage[STEPS][pair * 2 + 
1], 0x20);
+            result[WIDTH / 2 + pair] = 
_mm256_permute2x128_si256(stage[STEPS][pair * 2],
+                                                                 
stage[STEPS][pair * 2 + 1], 0x31);
+        }
+        for (size_t lane = 0; lane < WIDTH; ++lane) {
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(dest + (block * 
WIDTH + lane) * LANES),
+                                result[lane]);
+        }
+    }
+    const size_t processed = blocks * LANES;
+    byte_stream_split_decode_scalar(src, WIDTH, offset + processed, num_values 
- processed, stride,
+                                    dest + processed * WIDTH);
+}
+
+__attribute__((target("avx2"))) void delta_decode_int32_avx2(int32_t* values, 
size_t count,
+                                                             int32_t min_delta,
+                                                             int32_t* 
last_value) {
+    const __m256i min_delta_vec = _mm256_set1_epi32(min_delta);
+    const size_t vector_count = count / 8 * 8;
+    for (size_t row = 0; row < vector_count; row += 8) {
+        __m256i value = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(values + row));
+        value = _mm256_add_epi32(value, min_delta_vec);
+        value = _mm256_add_epi32(value, _mm256_slli_si256(value, 4));
+        value = _mm256_add_epi32(value, _mm256_slli_si256(value, 8));
+        _mm256_storeu_si256(reinterpret_cast<__m256i*>(values + row), value);
+    }
+    __m128i carry = _mm_set1_epi32(*last_value);
+    for (size_t row = 0; row < vector_count; row += 4) {
+        __m128i value = _mm_loadu_si128(reinterpret_cast<const 
__m128i*>(values + row));
+        value = _mm_add_epi32(value, carry);
+        _mm_storeu_si128(reinterpret_cast<__m128i*>(values + row), value);
+        carry = _mm_shuffle_epi32(value, _MM_SHUFFLE(3, 3, 3, 3));
+    }
+    if (vector_count != 0) {
+        *last_value = _mm_cvtsi128_si32(carry);
+    }
+    using Unsigned = uint32_t;
+    for (size_t row = vector_count; row < count; ++row) {
+        values[row] = static_cast<int32_t>(static_cast<Unsigned>(values[row]) +
+                                           static_cast<Unsigned>(min_delta) +
+                                           static_cast<Unsigned>(*last_value));
+        *last_value = values[row];
+    }
+}
+
+__attribute__((target("avx2"))) void delta_decode_int64_avx2(int64_t* values, 
size_t count,
+                                                             int64_t min_delta,
+                                                             int64_t* 
last_value) {
+    const __m256i min_delta_vec = _mm256_set1_epi64x(min_delta);
+    const size_t vector_count = count / 4 * 4;
+    for (size_t row = 0; row < vector_count; row += 4) {
+        __m256i value = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(values + row));
+        value = _mm256_add_epi64(value, min_delta_vec);
+        value = _mm256_add_epi64(value, _mm256_slli_si256(value, 8));
+        _mm256_storeu_si256(reinterpret_cast<__m256i*>(values + row), value);
+    }
+    __m128i carry = _mm_set1_epi64x(*last_value);
+    for (size_t row = 0; row < vector_count; row += 2) {
+        __m128i value = _mm_loadu_si128(reinterpret_cast<const 
__m128i*>(values + row));
+        value = _mm_add_epi64(value, carry);
+        _mm_storeu_si128(reinterpret_cast<__m128i*>(values + row), value);
+        carry = _mm_unpackhi_epi64(value, value);
+    }
+    if (vector_count != 0) {
+        *last_value = _mm_cvtsi128_si64(carry);
+    }
+    using Unsigned = uint64_t;
+    for (size_t row = vector_count; row < count; ++row) {
+        values[row] = static_cast<int64_t>(static_cast<Unsigned>(values[row]) +
+                                           static_cast<Unsigned>(min_delta) +
+                                           static_cast<Unsigned>(*last_value));
+        *last_value = values[row];
+    }
+}
+
+__attribute__((target("avx2"))) void dictionary_gather_avx2(const uint8_t* 
dictionary,
+                                                            const uint32_t* 
indices, size_t count,
+                                                            size_t 
value_width, uint8_t* dest) {
+    size_t row = 0;
+    if (value_width == 4) {
+        for (; row + 8 <= count; row += 8) {
+            const __m256i ids = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(indices + row));
+            const __m256i gathered =
+                    _mm256_i32gather_epi32(reinterpret_cast<const 
int*>(dictionary), ids, 4);
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(dest + row * 4), 
gathered);
+        }
+    } else {
+        for (; row + 4 <= count; row += 4) {
+            const __m128i ids = _mm_loadu_si128(reinterpret_cast<const 
__m128i*>(indices + row));
+            const __m256i gathered =
+                    _mm256_i32gather_epi64(reinterpret_cast<const long 
long*>(dictionary), ids, 8);
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(dest + row * 8), 
gathered);
+        }
+    }
+    for (; row < count; ++row) {
+        memcpy(dest + row * value_width, dictionary + indices[row] * 
value_width, value_width);
+    }
+}
+
+template <size_t LANES>
+constexpr auto make_expand_permute_lut() {
+    std::array<std::array<int32_t, 8>, 1U << LANES> lut {};
+    for (size_t mask = 0; mask < lut.size(); ++mask) {
+        int32_t source = 0;
+        for (size_t lane = 0; lane < LANES; ++lane) {
+            const int32_t value = (mask & (1U << lane)) != 0 ? source++ : 0;
+            if constexpr (LANES == 8) {
+                lut[mask][lane] = value;
+            } else {
+                lut[mask][lane * 2] = value * 2;
+                lut[mask][lane * 2 + 1] = value * 2 + 1;
+            }
+        }
+    }
+    return lut;
+}
+
+constexpr auto EXPAND_PERMUTE_32 = make_expand_permute_lut<8>();
+constexpr auto EXPAND_PERMUTE_64 = make_expand_permute_lut<4>();
+
+__attribute__((target("avx2"))) void expand_nullable_avx2(uint8_t* bytes, 
size_t compact_count,
+                                                          const uint8_t* 
nulls, size_t output_count,
+                                                          size_t value_width) {
+    size_t source = compact_count;
+    size_t output = output_count;
+    if (value_width == 4) {
+        auto* values = reinterpret_cast<int32_t*>(bytes);
+        while (output >= 8) {
+            const size_t start = output - 8;
+            uint32_t valid_mask = 0;
+            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);
+            source -= valid;
+            const __m256i load_mask = 
_mm256_cmpgt_epi32(_mm256_set1_epi32(static_cast<int>(valid)),
+                                                         _mm256_setr_epi32(0, 
1, 2, 3, 4, 5, 6, 7));
+            const __m256i compact = _mm256_maskload_epi32(values + source, 
load_mask);
+            const __m256i permute = _mm256_loadu_si256(
+                    reinterpret_cast<const 
__m256i*>(EXPAND_PERMUTE_32[valid_mask].data()));
+            __m256i expanded = _mm256_permutevar8x32_epi32(compact, permute);
+            const __m256i valid_lanes = _mm256_setr_epi32(
+                    -(valid_mask & 1U), -((valid_mask >> 1) & 1U), 
-((valid_mask >> 2) & 1U),
+                    -((valid_mask >> 3) & 1U), -((valid_mask >> 4) & 1U), 
-((valid_mask >> 5) & 1U),
+                    -((valid_mask >> 6) & 1U), -((valid_mask >> 7) & 1U));
+            expanded = _mm256_and_si256(expanded, valid_lanes);
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(values + start), 
expanded);
+            output = start;
+        }
+    } else {
+        auto* values = reinterpret_cast<int64_t*>(bytes);
+        while (output >= 4) {
+            const size_t start = output - 4;
+            uint32_t valid_mask = 0;
+            for (size_t lane = 0; lane < 4; ++lane) {
+                valid_mask |= static_cast<uint32_t>(nulls[start + lane] == 0) 
<< lane;
+            }
+            const size_t valid = std::popcount(valid_mask);
+            source -= valid;
+            const __m256i load_mask =
+                    _mm256_setr_epi64x(valid > 0 ? -1LL : 0, valid > 1 ? -1LL 
: 0,
+                                       valid > 2 ? -1LL : 0, valid > 3 ? -1LL 
: 0);
+            const __m256i compact = _mm256_maskload_epi64(
+                    reinterpret_cast<const long long*>(values + source), 
load_mask);
+            const __m256i permute = _mm256_loadu_si256(
+                    reinterpret_cast<const 
__m256i*>(EXPAND_PERMUTE_64[valid_mask].data()));
+            __m256i expanded = _mm256_permutevar8x32_epi32(compact, permute);
+            const __m256i valid_lanes = _mm256_setr_epi64x(
+                    (valid_mask & 1U) != 0 ? -1LL : 0, (valid_mask & 2U) != 0 
? -1LL : 0,
+                    (valid_mask & 4U) != 0 ? -1LL : 0, (valid_mask & 8U) != 0 
? -1LL : 0);
+            expanded = _mm256_and_si256(expanded, valid_lanes);
+            _mm256_storeu_si256(reinterpret_cast<__m256i*>(values + start), 
expanded);
+            output = start;
+        }
+    }
+    while (output > 0) {
+        --output;
+        if (nulls[output] != 0) {
+            memset(bytes + output * value_width, 0, value_width);
+        } else {
+            --source;
+            memmove(bytes + output * value_width, bytes + source * 
value_width, value_width);
+        }
+    }
+}
+
+__attribute__((target("avx2"))) __m256i vector_all(__m256i) {
+    return _mm256_set1_epi32(-1);
+}
+
+__attribute__((target("avx2"))) __m256 vector_all(__m256) {
+    return _mm256_castsi256_ps(_mm256_set1_epi32(-1));
+}
+
+__attribute__((target("avx2"))) __m256d vector_all(__m256d) {
+    return _mm256_castsi256_pd(_mm256_set1_epi32(-1));
+}
+
+__attribute__((target("avx2"))) __m256i vector_or(__m256i lhs, __m256i rhs) {
+    return _mm256_or_si256(lhs, rhs);
+}
+
+__attribute__((target("avx2"))) __m256 vector_or(__m256 lhs, __m256 rhs) {
+    return _mm256_or_ps(lhs, rhs);
+}
+
+__attribute__((target("avx2"))) __m256d vector_or(__m256d lhs, __m256d rhs) {
+    return _mm256_or_pd(lhs, rhs);
+}
+
+__attribute__((target("avx2"))) __m256i vector_xor(__m256i lhs, __m256i rhs) {
+    return _mm256_xor_si256(lhs, rhs);
+}
+
+__attribute__((target("avx2"))) __m256 vector_xor(__m256 lhs, __m256 rhs) {
+    return _mm256_xor_ps(lhs, rhs);
+}
+
+__attribute__((target("avx2"))) __m256d vector_xor(__m256d lhs, __m256d rhs) {
+    return _mm256_xor_pd(lhs, rhs);
+}
+
+template <typename Vec>
+__attribute__((target("avx2"))) Vec combine_comparison(Vec equal, Vec greater, 
Vec less,
+                                                       RawComparisonOp op) {
+    // Target features do not propagate from AVX2 callers into separately 
instantiated helpers.
+    // Keep the complete vector operation inside its own target scope for 
baseline x86 builds.
+    const Vec all = vector_all(equal);
+    switch (op) {
+    case RawComparisonOp::EQ:
+        return equal;
+    case RawComparisonOp::NE:
+        return vector_xor(equal, all);
+    case RawComparisonOp::LT:
+        return less;
+    case RawComparisonOp::LE:
+        return vector_or(less, equal);
+    case RawComparisonOp::GT:
+        return greater;
+    case RawComparisonOp::GE:
+        return vector_or(greater, equal);
+    }
+    __builtin_unreachable();
+}
+
+__attribute__((target("avx2"))) void raw_compare_int32_avx2(const uint8_t* 
bytes, size_t count,
+                                                            int32_t literal, 
RawComparisonOp op,
+                                                            uint8_t* matches) {
+    size_t row = 0;
+    const __m256i rhs = _mm256_set1_epi32(literal);
+    for (; row + 8 <= count; row += 8) {
+        const __m256i lhs = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(bytes + row * 4));
+        const __m256i equal = _mm256_cmpeq_epi32(lhs, rhs);
+        const __m256i greater = _mm256_cmpgt_epi32(lhs, rhs);
+        const __m256i less = _mm256_cmpgt_epi32(rhs, lhs);
+        const uint32_t mask = static_cast<uint32_t>(_mm256_movemask_ps(
+                _mm256_castsi256_ps(combine_comparison(equal, greater, less, 
op))));
+        for (size_t lane = 0; lane < 8; ++lane) {
+            matches[row + lane] &= static_cast<uint8_t>((mask >> lane) & 1U);
+        }
+    }
+    for (; row < count; ++row) {
+        int32_t value;
+        memcpy(&value, bytes + row * 4, sizeof(value));
+        bool keep = false;
+        switch (op) {
+        case RawComparisonOp::EQ:
+            keep = value == literal;
+            break;
+        case RawComparisonOp::NE:
+            keep = value != literal;
+            break;
+        case RawComparisonOp::LT:
+            keep = value < literal;
+            break;
+        case RawComparisonOp::LE:
+            keep = value <= literal;
+            break;
+        case RawComparisonOp::GT:
+            keep = value > literal;
+            break;
+        case RawComparisonOp::GE:
+            keep = value >= literal;
+            break;
+        }
+        matches[row] &= static_cast<uint8_t>(keep);
+    }
+}
+
+__attribute__((target("avx2"))) void raw_compare_int64_avx2(const uint8_t* 
bytes, size_t count,
+                                                            int64_t literal, 
RawComparisonOp op,
+                                                            uint8_t* matches) {
+    size_t row = 0;
+    const __m256i rhs = _mm256_set1_epi64x(literal);
+    for (; row + 4 <= count; row += 4) {
+        const __m256i lhs = _mm256_loadu_si256(reinterpret_cast<const 
__m256i*>(bytes + row * 8));
+        const __m256i equal = _mm256_cmpeq_epi64(lhs, rhs);
+        const __m256i greater = _mm256_cmpgt_epi64(lhs, rhs);
+        const __m256i less = _mm256_cmpgt_epi64(rhs, lhs);
+        const uint32_t mask = static_cast<uint32_t>(_mm256_movemask_pd(
+                _mm256_castsi256_pd(combine_comparison(equal, greater, less, 
op))));
+        for (size_t lane = 0; lane < 4; ++lane) {
+            matches[row + lane] &= static_cast<uint8_t>((mask >> lane) & 1U);
+        }
+    }
+    for (; row < count; ++row) {
+        int64_t value;
+        memcpy(&value, bytes + row * 8, sizeof(value));
+        bool keep = false;
+        switch (op) {
+        case RawComparisonOp::EQ:
+            keep = value == literal;
+            break;
+        case RawComparisonOp::NE:
+            keep = value != literal;
+            break;
+        case RawComparisonOp::LT:
+            keep = value < literal;
+            break;
+        case RawComparisonOp::LE:
+            keep = value <= literal;
+            break;
+        case RawComparisonOp::GT:
+            keep = value > literal;
+            break;
+        case RawComparisonOp::GE:
+            keep = value >= literal;
+            break;
+        }
+        matches[row] &= static_cast<uint8_t>(keep);
+    }
+}
+
+__attribute__((target("avx2"))) void raw_compare_float_avx2(const uint8_t* 
bytes, size_t count,
+                                                            float literal, 
RawComparisonOp op,
+                                                            uint8_t* matches) {
+    size_t row = 0;
+    const __m256 rhs = _mm256_set1_ps(literal);
+    for (; row + 8 <= count; row += 8) {
+        const __m256 lhs = _mm256_loadu_ps(reinterpret_cast<const 
float*>(bytes + row * 4));
+        const __m256 lhs_nan = _mm256_cmp_ps(lhs, lhs, _CMP_UNORD_Q);
+        const __m256 rhs_nan = _mm256_cmp_ps(rhs, rhs, _CMP_UNORD_Q);
+        const __m256 both_nan = _mm256_and_ps(lhs_nan, rhs_nan);
+        const __m256 equal = _mm256_or_ps(_mm256_cmp_ps(lhs, rhs, _CMP_EQ_OQ), 
both_nan);
+        const __m256 greater = _mm256_or_ps(_mm256_cmp_ps(lhs, rhs, 
_CMP_GT_OQ),
+                                            _mm256_andnot_ps(rhs_nan, 
lhs_nan));
+        const __m256 less = _mm256_or_ps(_mm256_cmp_ps(lhs, rhs, _CMP_LT_OQ),
+                                         _mm256_andnot_ps(lhs_nan, rhs_nan));
+        const uint32_t mask = static_cast<uint32_t>(
+                _mm256_movemask_ps(combine_comparison(equal, greater, less, 
op)));
+        for (size_t lane = 0; lane < 8; ++lane) {
+            matches[row + lane] &= static_cast<uint8_t>((mask >> lane) & 1U);
+        }
+    }
+    for (; row < count; ++row) {
+        float value;
+        memcpy(&value, bytes + row * 4, sizeof(value));
+        matches[row] &= static_cast<uint8_t>(scalar_compare(value, literal, 
op));
+    }
+}
+
+__attribute__((target("avx2"))) void raw_compare_double_avx2(const uint8_t* 
bytes, size_t count,
+                                                             double literal, 
RawComparisonOp op,
+                                                             uint8_t* matches) 
{
+    size_t row = 0;
+    const __m256d rhs = _mm256_set1_pd(literal);
+    for (; row + 4 <= count; row += 4) {
+        const __m256d lhs = _mm256_loadu_pd(reinterpret_cast<const 
double*>(bytes + row * 8));
+        const __m256d lhs_nan = _mm256_cmp_pd(lhs, lhs, _CMP_UNORD_Q);
+        const __m256d rhs_nan = _mm256_cmp_pd(rhs, rhs, _CMP_UNORD_Q);
+        const __m256d both_nan = _mm256_and_pd(lhs_nan, rhs_nan);
+        const __m256d equal = _mm256_or_pd(_mm256_cmp_pd(lhs, rhs, 
_CMP_EQ_OQ), both_nan);
+        const __m256d greater = _mm256_or_pd(_mm256_cmp_pd(lhs, rhs, 
_CMP_GT_OQ),
+                                             _mm256_andnot_pd(rhs_nan, 
lhs_nan));
+        const __m256d less = _mm256_or_pd(_mm256_cmp_pd(lhs, rhs, _CMP_LT_OQ),
+                                          _mm256_andnot_pd(lhs_nan, rhs_nan));
+        const uint32_t mask = static_cast<uint32_t>(
+                _mm256_movemask_pd(combine_comparison(equal, greater, less, 
op)));
+        for (size_t lane = 0; lane < 4; ++lane) {
+            matches[row + lane] &= static_cast<uint8_t>((mask >> lane) & 1U);
+        }
+    }
+    for (; row < count; ++row) {
+        double value;
+        memcpy(&value, bytes + row * 8, sizeof(value));
+        matches[row] &= static_cast<uint8_t>(scalar_compare(value, literal, 
op));
+    }
+}
+#endif
+
+template <typename T>
+void delta_decode_scalar(T* values, size_t count, T min_delta, T* last_value) {
+    using Unsigned = std::make_unsigned_t<T>;
+    for (size_t row = 0; row < count; ++row) {
+        values[row] = static_cast<T>(static_cast<Unsigned>(values[row]) +
+                                     static_cast<Unsigned>(min_delta) +
+                                     static_cast<Unsigned>(*last_value));
+        *last_value = values[row];
+    }
+}
+
+template <typename T>
+void raw_compare_scalar(const uint8_t* bytes, size_t count, T literal, 
RawComparisonOp op,
+                        uint8_t* matches) {
+    for (size_t row = 0; row < count; ++row) {
+        if (matches[row] == 0) {
+            continue;
+        }
+        T value;
+        memcpy(&value, bytes + row * sizeof(T), sizeof(T));
+        matches[row] = static_cast<uint8_t>(scalar_compare(value, literal, 
op));
+    }
+}
+
+} // namespace
+
+bool try_byte_stream_split_decode(const uint8_t* src, size_t width, size_t 
offset,
+                                  size_t num_values, size_t stride, uint8_t* 
dest) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && num_values >= 32 && (width == 4 || width == 8)) {
+        if (width == 4) {
+            byte_stream_split_decode_avx2<4>(src, offset, num_values, stride, 
dest);
+        } else {
+            byte_stream_split_decode_avx2<8>(src, offset, num_values, stride, 
dest);
+        }
+        return true;
+    }
+#endif
+    return false;
+}
+
+void delta_decode(int32_t* values, size_t count, int32_t min_delta, int32_t* 
last_value) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && count >= 8) {
+        delta_decode_int32_avx2(values, count, min_delta, last_value);
+        return;
+    }
+#endif
+    delta_decode_scalar(values, count, min_delta, last_value);
+}
+
+void delta_decode(int64_t* values, size_t count, int64_t min_delta, int64_t* 
last_value) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && count >= 4) {
+        delta_decode_int64_avx2(values, count, min_delta, last_value);
+        return;
+    }
+#endif
+    delta_decode_scalar(values, count, min_delta, last_value);
+}
+
+void dictionary_gather(const uint8_t* dictionary, const uint32_t* indices, 
size_t count,
+                       size_t value_width, uint8_t* dest) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && ((value_width == 4 && count >= 8) || (value_width == 8 
&& count >= 4))) {
+        dictionary_gather_avx2(dictionary, indices, count, value_width, dest);
+        return;
+    }
+#endif
+    for (size_t row = 0; row < count; ++row) {
+        memcpy(dest + row * value_width, dictionary + indices[row] * 
value_width, value_width);
+    }
+}
+
+void expand_nullable_values(uint8_t* values, size_t compact_count, const 
uint8_t* nulls,
+                            size_t output_count, size_t value_width) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() &&
+        ((value_width == 4 && output_count >= 8) || (value_width == 8 && 
output_count >= 4))) {
+        // Backward expansion is required because the compact input and 
expanded output alias.
+        // Each SIMD block loads all of its source lanes before overwriting 
the wider destination.
+        expand_nullable_avx2(values, compact_count, nulls, output_count, 
value_width);
+        return;
+    }
+#endif
+    size_t source = compact_count;
+    for (size_t output = output_count; output > 0;) {
+        --output;
+        if (nulls[output] != 0) {
+            memset(values + output * value_width, 0, value_width);
+        } else {
+            --source;
+            memmove(values + output * value_width, values + source * 
value_width, value_width);
+        }
+    }
+}
+
+void raw_compare(const uint8_t* values, size_t count, int32_t literal, 
RawComparisonOp op,
+                 uint8_t* matches) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && count >= 8) {
+        raw_compare_int32_avx2(values, count, literal, op, matches);
+        return;
+    }
+#endif
+    raw_compare_scalar(values, count, literal, op, matches);
+}
+
+void raw_compare(const uint8_t* values, size_t count, int64_t literal, 
RawComparisonOp op,
+                 uint8_t* matches) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && count >= 4) {
+        raw_compare_int64_avx2(values, count, literal, op, matches);
+        return;
+    }
+#endif
+    raw_compare_scalar(values, count, literal, op, matches);
+}
+
+void raw_compare(const uint8_t* values, size_t count, float literal, 
RawComparisonOp op,
+                 uint8_t* matches) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && count >= 8) {
+        // Doris orders NaN above every finite value and considers NaN equal 
to NaN. The SIMD
+        // masks deliberately reconstruct that total order instead of using 
ordered FP compares.
+        raw_compare_float_avx2(values, count, literal, op, matches);
+        return;
+    }
+#endif
+    raw_compare_scalar(values, count, literal, op, matches);
+}
+
+void raw_compare(const uint8_t* values, size_t count, double literal, 
RawComparisonOp op,
+                 uint8_t* matches) {
+#ifdef DORIS_PARQUET_X86_SIMD
+    if (has_avx2() && count >= 4) {
+        raw_compare_double_avx2(values, count, literal, op, matches);
+        return;
+    }
+#endif
+    raw_compare_scalar(values, count, literal, op, matches);
+}
+
+} // namespace doris::simd
diff --git a/be/src/util/simd/parquet_kernels.h 
b/be/src/util/simd/parquet_kernels.h
new file mode 100644
index 00000000000..2381b85e41c
--- /dev/null
+++ b/be/src/util/simd/parquet_kernels.h
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+namespace doris::simd {
+
+enum class RawComparisonOp : uint8_t { EQ, NE, LT, LE, GT, GE };
+
+bool try_byte_stream_split_decode(const uint8_t* src, size_t width, size_t 
offset,
+                                  size_t num_values, size_t stride, uint8_t* 
dest);
+
+void delta_decode(int32_t* values, size_t count, int32_t min_delta, int32_t* 
last_value);
+void delta_decode(int64_t* values, size_t count, int64_t min_delta, int64_t* 
last_value);
+
+void dictionary_gather(const uint8_t* dictionary, const uint32_t* indices, 
size_t count,
+                       size_t value_width, uint8_t* dest);
+
+void expand_nullable_values(uint8_t* values, size_t compact_count, const 
uint8_t* nulls,
+                            size_t output_count, size_t value_width);
+
+void raw_compare(const uint8_t* values, size_t count, int32_t literal, 
RawComparisonOp op,
+                 uint8_t* matches);
+void raw_compare(const uint8_t* values, size_t count, int64_t literal, 
RawComparisonOp op,
+                 uint8_t* matches);
+void raw_compare(const uint8_t* values, size_t count, float literal, 
RawComparisonOp op,
+                 uint8_t* matches);
+void raw_compare(const uint8_t* values, size_t count, double literal, 
RawComparisonOp op,
+                 uint8_t* matches);
+
+} // namespace doris::simd
diff --git a/be/test/format_v2/parquet/native_decoder_test.cpp 
b/be/test/format_v2/parquet/native_decoder_test.cpp
index 599c930f312..9192a055f01 100644
--- a/be/test/format_v2/parquet/native_decoder_test.cpp
+++ b/be/test/format_v2/parquet/native_decoder_test.cpp
@@ -1269,12 +1269,14 @@ TEST(ParquetV2NativeDecoderTest, 
DictionaryMaterializationUsesCacheAwareExecutio
         ParquetMaterializationState state;
         state.typed_dictionary = ColumnInt32::create();
         assert_cast<ColumnInt32&>(*state.typed_dictionary).get_data() = {10, 
20, 30, 40};
-        ScriptedDictionaryMaterializationSource source({3, 0, 2, 1, 3}, 
prefer_indices);
+        const std::vector<uint32_t> ids {3, 0, 2, 1, 3, 1, 0, 2, 3, 3, 1};
+        ScriptedDictionaryMaterializationSource source(ids, prefer_indices);
         auto output = ColumnInt32::create();
 
-        const auto status = state.materialize_dictionary(*output, source, 5);
+        const auto status = state.materialize_dictionary(*output, source, 
ids.size());
         EXPECT_TRUE(status.ok()) << status;
-        EXPECT_EQ(output->get_data(), (ColumnInt32::Container {40, 10, 30, 20, 
40}));
+        EXPECT_EQ(output->get_data(),
+                  (ColumnInt32::Container {40, 10, 30, 20, 40, 20, 10, 30, 40, 
40, 20}));
         EXPECT_EQ(state.dictionary_materialization_strategy, 
expected_strategy);
         EXPECT_EQ(source.direct_batches, expected_direct_batches);
         EXPECT_EQ(source.index_batches, expected_index_batches);
diff --git a/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp 
b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
index 45c1fe9dd44..7cc0838e125 100644
--- a/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
+++ b/be/test/format_v2/parquet/parquet_benchmark_scenarios_test.cpp
@@ -21,6 +21,7 @@
 
 #include <algorithm>
 #include <array>
+#include <map>
 #include <set>
 #include <string>
 #include <tuple>
@@ -30,6 +31,7 @@ namespace {
 
 TEST(ParquetBenchmarkScenariosTest, 
DecoderMatrixCoversNativeEncodingAndTypeFamilies) {
     const auto scenarios = decoder_scenarios();
+    EXPECT_EQ(scenarios.size() * 6 * 2, size_t {228});
     const std::set<std::pair<Encoding, ValueType>> actual = [&] {
         std::set<std::pair<Encoding, ValueType>> values;
         for (const auto& scenario : scenarios) {
@@ -62,8 +64,55 @@ TEST(ParquetBenchmarkScenariosTest, 
DecoderMatrixCoversNativeEncodingAndTypeFami
     EXPECT_EQ(actual, expected);
 }
 
+TEST(ParquetBenchmarkScenariosTest, 
KernelMatrixCoversEverySimdStageAndBoundaryShape) {
+    const auto scenarios = kernel_scenarios();
+    EXPECT_EQ(scenarios.size(), size_t {80});
+    const std::map<Kernel, std::vector<ValueType>> expected_types {
+            {Kernel::BYTE_STREAM_SPLIT, {ValueType::FLOAT, ValueType::DOUBLE}},
+            {Kernel::DELTA_PREFIX_SUM, {ValueType::INT32, ValueType::INT64}},
+            {Kernel::DICTIONARY_GATHER,
+             {ValueType::INT32, ValueType::INT64, ValueType::FLOAT, 
ValueType::DOUBLE}},
+            {Kernel::NULLABLE_EXPAND,
+             {ValueType::INT32, ValueType::INT64, ValueType::FLOAT, 
ValueType::DOUBLE}},
+            {Kernel::RAW_PREDICATE,
+             {ValueType::INT32, ValueType::INT64, ValueType::FLOAT, 
ValueType::DOUBLE}},
+    };
+    for (const auto& [kernel, value_types] : expected_types) {
+        for (const auto value_type : value_types) {
+            EXPECT_TRUE(std::ranges::any_of(scenarios,
+                                            [&](const KernelScenario& 
scenario) {
+                                                return scenario.kernel == 
kernel &&
+                                                       scenario.value_type == 
value_type;
+                                            }))
+                    << "missing SIMD width for kernel " << to_string(kernel);
+        }
+    }
+    for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
+        EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const KernelScenario& 
scenario) {
+            return scenario.kernel == Kernel::RAW_PREDICATE &&
+                   scenario.selectivity_percent == selectivity;
+        })) << "missing raw predicate selectivity";
+    }
+    for (const int null_percent : {0, 1, 10, 50, 90}) {
+        for (const auto pattern : {Pattern::CLUSTERED, Pattern::ALTERNATING}) {
+            EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const 
KernelScenario& scenario) {
+                return scenario.kernel == Kernel::NULLABLE_EXPAND &&
+                       scenario.null_percent == null_percent && 
scenario.pattern == pattern;
+            })) << "missing nullable expansion shape";
+        }
+    }
+    for (const size_t dictionary_entries : {32, 4096, 262144}) {
+        EXPECT_TRUE(std::ranges::any_of(scenarios, [&](const KernelScenario& 
scenario) {
+            return scenario.kernel == Kernel::DICTIONARY_GATHER &&
+                   scenario.dictionary_entries == dictionary_entries;
+        })) << "missing dictionary working-set size";
+    }
+}
+
 TEST(ParquetBenchmarkScenariosTest, 
ReaderMatrixCoversNullableSparseAndProjectionAxes) {
     const auto scenarios = reader_scenarios();
+    // Keep the exact count aligned with the upstream complex-residual 
scenario retained by rebase.
+    EXPECT_EQ(scenarios.size(), size_t {152});
     for (const int null_percent : {0, 1, 10, 50, 90}) {
         for (const auto pattern : {Pattern::CLUSTERED, Pattern::ALTERNATING}) {
             for (const int selectivity : {0, 1, 10, 50, 90, 100}) {
@@ -113,7 +162,7 @@ TEST(ParquetBenchmarkScenariosTest, 
ReaderMatrixCoversOperationsEncodingsAndSche
 
 TEST(ParquetBenchmarkScenariosTest, 
ReaderMatrixHasExactUniqueRegistrationNames) {
     const auto scenarios = reader_scenarios();
-    EXPECT_EQ(scenarios.size(), 152);
+    EXPECT_EQ(scenarios.size(), size_t {152});
 
     std::set<std::string> names;
     for (const auto& scenario : scenarios) {
@@ -166,5 +215,18 @@ TEST(ParquetBenchmarkScenariosTest, 
SelectionPlanDistinguishesClusteredAndSparse
     EXPECT_EQ(make_selection_plan(1000, 100, 
Pattern::ALTERNATING).ranges.size(), 1);
 }
 
+TEST(ParquetBenchmarkScenariosTest, 
SelectedRowVisitorPreservesRangeOrderAndBoundaries) {
+    const SelectionPlan plan {
+            .total_rows = 12,
+            .selected_rows = 5,
+            .ranges = {{.first = 0, .count = 2},
+                       {.first = 5, .count = 1},
+                       {.first = 9, .count = 2}},
+    };
+    std::vector<size_t> rows;
+    visit_selected_rows(plan, [&](size_t row) { rows.push_back(row); });
+    EXPECT_EQ(rows, (std::vector<size_t> {0, 1, 5, 9, 10}));
+}
+
 } // namespace
 } // namespace doris::parquet_benchmark
diff --git a/be/test/format_v2/parquet/parquet_simd_kernels_test.cpp 
b/be/test/format_v2/parquet/parquet_simd_kernels_test.cpp
new file mode 100644
index 00000000000..c1a674f7aaa
--- /dev/null
+++ b/be/test/format_v2/parquet/parquet_simd_kernels_test.cpp
@@ -0,0 +1,222 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <array>
+#include <cmath>
+#include <cstdint>
+#include <cstring>
+#include <limits>
+#include <type_traits>
+#include <vector>
+
+#include "util/byte_stream_split.h"
+#include "util/simd/parquet_kernels.h"
+
+namespace doris::simd {
+namespace {
+
+TEST(ParquetSimdKernelsTest, ByteStreamSplitRestoresFourAndEightByteValues) {
+    for (const size_t width : {4, 8}) {
+        constexpr size_t rows = 67;
+        std::vector<uint8_t> plain(rows * width);
+        for (size_t byte = 0; byte < plain.size(); ++byte) {
+            plain[byte] = static_cast<uint8_t>((byte * 29 + 17) & 0xff);
+        }
+        std::vector<uint8_t> encoded(plain.size());
+        for (size_t row = 0; row < rows; ++row) {
+            for (size_t byte = 0; byte < width; ++byte) {
+                encoded[byte * rows + row] = plain[row * width + byte];
+            }
+        }
+
+        for (const auto [offset, count] :
+             {std::pair<size_t, size_t> {0, rows}, {3, 31}, {17, 33}}) {
+            std::vector<uint8_t> decoded(count * width);
+            if (!try_byte_stream_split_decode(encoded.data(), width, offset, 
count, rows,
+                                              decoded.data())) {
+                doris::byte_stream_split_decode(encoded.data(), 
static_cast<int>(width), offset,
+                                                count, rows, decoded.data());
+            }
+            EXPECT_EQ(0, memcmp(decoded.data(), plain.data() + offset * width, 
decoded.size()));
+        }
+    }
+}
+
+TEST(ParquetSimdKernelsTest, 
ByteStreamSplitDeclinesToReplaceOptimizedScalarFallback) {
+    constexpr size_t rows = 191;
+    constexpr size_t offset = 17;
+    constexpr size_t count = 31;
+    for (const size_t width : {1, 2, 4, 8, 12, 16, 23}) {
+        std::vector<uint8_t> plain(rows * width);
+        for (size_t byte = 0; byte < plain.size(); ++byte) {
+            plain[byte] = static_cast<uint8_t>((byte * 29 + 17) & 0xff);
+        }
+        std::vector<uint8_t> encoded(plain.size());
+        for (size_t row = 0; row < rows; ++row) {
+            for (size_t byte = 0; byte < width; ++byte) {
+                encoded[byte * rows + row] = plain[row * width + byte];
+            }
+        }
+
+        std::vector<uint8_t> decoded(count * width);
+        EXPECT_FALSE(try_byte_stream_split_decode(encoded.data(), width, 
offset, count, rows,
+                                                  decoded.data()))
+                << "width=" << width;
+        doris::byte_stream_split_decode(encoded.data(), 
static_cast<int>(width), offset, count,
+                                        rows, decoded.data());
+        EXPECT_EQ(0, memcmp(decoded.data(), plain.data() + offset * width, 
decoded.size()))
+                << "width=" << width;
+    }
+}
+
+template <typename T>
+void test_delta_prefix_sum() {
+    std::vector<T> deltas(71);
+    for (size_t row = 0; row < deltas.size(); ++row) {
+        deltas[row] = static_cast<T>((row * 7) % 19);
+    }
+    auto expected = deltas;
+    T expected_last = std::numeric_limits<T>::max() - 37;
+    constexpr T min_delta = static_cast<T>(-11);
+    using Unsigned = std::make_unsigned_t<T>;
+    for (auto& value : expected) {
+        value = static_cast<T>(static_cast<Unsigned>(value) + 
static_cast<Unsigned>(min_delta) +
+                               static_cast<Unsigned>(expected_last));
+        expected_last = value;
+    }
+
+    T last = std::numeric_limits<T>::max() - 37;
+    delta_decode(deltas.data(), deltas.size(), min_delta, &last);
+    EXPECT_EQ(deltas, expected);
+    EXPECT_EQ(last, expected_last);
+}
+
+TEST(ParquetSimdKernelsTest, DeltaPrefixSumPreservesParquetUnsignedOverflow) {
+    test_delta_prefix_sum<int32_t>();
+    test_delta_prefix_sum<int64_t>();
+}
+
+TEST(ParquetSimdKernelsTest, DictionaryGatherHandlesTailsAndRepeatedIds) {
+    for (const size_t width : {4, 8}) {
+        constexpr size_t entries = 257;
+        std::vector<uint8_t> dictionary(entries * width);
+        for (size_t byte = 0; byte < dictionary.size(); ++byte) {
+            dictionary[byte] = static_cast<uint8_t>((byte * 13 + 5) & 0xff);
+        }
+        std::vector<uint32_t> ids(69);
+        for (size_t row = 0; row < ids.size(); ++row) {
+            ids[row] = row % 9 == 0 ? 3 : static_cast<uint32_t>((row * 31) % 
entries);
+        }
+        std::vector<uint8_t> actual(ids.size() * width);
+        dictionary_gather(dictionary.data(), ids.data(), ids.size(), width, 
actual.data());
+        for (size_t row = 0; row < ids.size(); ++row) {
+            EXPECT_EQ(0, memcmp(actual.data() + row * width, dictionary.data() 
+ ids[row] * width,
+                                width));
+        }
+    }
+}
+
+TEST(ParquetSimdKernelsTest, NullableExpansionIsSafeForOverlappingStorage) {
+    for (const size_t width : {4, 8}) {
+        const std::vector<std::vector<uint8_t>> null_masks {
+                {1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0},
+                std::vector<uint8_t>(17, 0),
+                std::vector<uint8_t>(17, 1),
+        };
+        for (const auto& nulls : null_masks) {
+            const size_t compact_count =
+                    static_cast<size_t>(std::count(nulls.begin(), nulls.end(), 
uint8_t {0}));
+            std::vector<uint8_t> values(nulls.size() * width, 0xcd);
+            for (size_t row = 0; row < compact_count; ++row) {
+                for (size_t byte = 0; byte < width; ++byte) {
+                    values[row * width + byte] =
+                            static_cast<uint8_t>((row * width + byte + 1) & 
0xff);
+                }
+            }
+            const auto compact = values;
+
+            expand_nullable_values(values.data(), compact_count, nulls.data(), 
nulls.size(), width);
+            size_t source = 0;
+            for (size_t row = 0; row < nulls.size(); ++row) {
+                if (nulls[row] != 0) {
+                    for (size_t byte = 0; byte < width; ++byte) {
+                        EXPECT_EQ(values[row * width + byte], 0);
+                    }
+                } else {
+                    for (size_t byte = 0; byte < width; ++byte) {
+                        EXPECT_EQ(values[row * width + byte], compact[source * 
width + byte])
+                                << "row=" << row << ", byte=" << byte << ", 
width=" << width;
+                    }
+                    ++source;
+                }
+            }
+        }
+    }
+}
+
+template <typename T>
+void expect_raw_comparison(const std::vector<T>& values, T literal, 
RawComparisonOp op,
+                           const std::vector<uint8_t>& expected) {
+    std::vector<uint8_t> matches(values.size(), 1);
+    if (matches.size() > 2) {
+        matches[1] = 0;
+    }
+    auto masked_expected = expected;
+    if (masked_expected.size() > 2) {
+        masked_expected[1] = 0;
+    }
+    raw_compare(reinterpret_cast<const uint8_t*>(values.data()), 
values.size(), literal, op,
+                matches.data());
+    EXPECT_EQ(matches, masked_expected);
+}
+
+TEST(ParquetSimdKernelsTest, 
RawPredicatesPreserveExistingMaskAndDorisNanOrdering) {
+    const float nan = std::numeric_limits<float>::quiet_NaN();
+    const std::vector<float> floats {-3.0F, -2.0F, -1.0F, 0.0F, 1.0F, 2.0F,
+                                     3.0F,  nan,   nan,   4.0F, 5.0F};
+    expect_raw_comparison(floats, 0.0F, RawComparisonOp::GT, {0, 0, 0, 0, 1, 
1, 1, 1, 1, 1, 1});
+    expect_raw_comparison(floats, nan, RawComparisonOp::EQ, {0, 0, 0, 0, 0, 0, 
0, 1, 1, 0, 0});
+    expect_raw_comparison(floats, nan, RawComparisonOp::LE, {1, 1, 1, 1, 1, 1, 
1, 1, 1, 1, 1});
+
+    const std::vector<int32_t> ints {-3, -1, 0, 1, 3, 7, 9, 12, 15, 21, 27};
+    expect_raw_comparison(ints, 3, RawComparisonOp::GE, {0, 0, 0, 0, 1, 1, 1, 
1, 1, 1, 1});
+    expect_raw_comparison(ints, 3, RawComparisonOp::NE, {1, 1, 1, 1, 0, 1, 1, 
1, 1, 1, 1});
+
+    const double double_nan = std::numeric_limits<double>::quiet_NaN();
+    const std::vector<double> doubles {
+            -std::numeric_limits<double>::infinity(), -2.0,       -1.0,      
0.0, 1.0,
+            std::numeric_limits<double>::infinity(),  double_nan, double_nan};
+    expect_raw_comparison(doubles, 0.0, RawComparisonOp::EQ, {0, 0, 0, 1, 0, 
0, 0, 0});
+    expect_raw_comparison(doubles, 0.0, RawComparisonOp::NE, {1, 1, 1, 0, 1, 
1, 1, 1});
+    expect_raw_comparison(doubles, 0.0, RawComparisonOp::LT, {1, 1, 1, 0, 0, 
0, 0, 0});
+    expect_raw_comparison(doubles, 0.0, RawComparisonOp::LE, {1, 1, 1, 1, 0, 
0, 0, 0});
+    expect_raw_comparison(doubles, 0.0, RawComparisonOp::GT, {0, 0, 0, 0, 1, 
1, 1, 1});
+    expect_raw_comparison(doubles, 0.0, RawComparisonOp::GE, {0, 0, 0, 1, 1, 
1, 1, 1});
+    expect_raw_comparison(doubles, double_nan, RawComparisonOp::EQ, {0, 0, 0, 
0, 0, 0, 1, 1});
+    expect_raw_comparison(doubles, double_nan, RawComparisonOp::LT, {1, 1, 1, 
1, 1, 1, 0, 0});
+    expect_raw_comparison(doubles, double_nan, RawComparisonOp::LE, {1, 1, 1, 
1, 1, 1, 1, 1});
+
+    const std::vector<int64_t> bigints {-9, -3, -1, 0, 1, 3, 7, 11, 19};
+    expect_raw_comparison(bigints, int64_t {3}, RawComparisonOp::LT, {1, 1, 1, 
1, 1, 0, 0, 0, 0});
+    expect_raw_comparison(bigints, int64_t {3}, RawComparisonOp::GE, {0, 0, 0, 
0, 0, 1, 1, 1, 1});
+}
+
+} // namespace
+} // namespace doris::simd


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to