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

Mryange 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 6875132c3d2 [improvement](be) Optimize bit unpacking with PDEP (#65738)
6875132c3d2 is described below

commit 6875132c3d2b0416d95ef6849658fb5959331735
Author: HappenLee <[email protected]>
AuthorDate: Tue Jul 21 18:03:40 2026 +0800

    [improvement](be) Optimize bit unpacking with PDEP (#65738)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    `BitPacking::UnpackValues` currently decodes every complete 32-value
    batch with scalar shifts and masks. This PR adds an x86 PDEP
    implementation for `uint8_t`, `uint16_t`, and `uint32_t`, with AVX2
    widening for narrow `uint16_t` and `uint32_t` values.
    
    The optimized functions are compiled with `target("bmi2,avx2")`. Runtime
    dispatch uses `__builtin_cpu_supports("bmi2")` and
    `__builtin_cpu_supports("avx2")`; unsupported CPUs, architectures,
    output types, and remainder values continue to use the generic scalar
    implementation. Doris therefore does not require a global `-mbmi2` build
    flag.
    
    Production dispatch deliberately uses PDEP only when `BIT_WIDTH < 16`.
    The generic PDEP implementation remains available to the benchmark for
    all supported widths, but repeated measurements found non-monotonic,
    CPU- and working-set-dependent results at 16-32 bits. A detailed code
    comment explains why the scalar implementation is retained for these
    widths instead of adding an irregular CPU-specific allowlist.
    
    The PR also adds unit tests for every supported bit width, including
    full batches and truncated/remainder input, plus a reproducible
    benchmark comparing:
    
    - the generic scalar kernel;
    - the direct PDEP kernel;
    - the actual `BitPacking::UnpackValues` dispatch path.
    
    The benchmark covers 4K, 256K, and 1M values and validates optimized
    output against the scalar reference before measuring.
    
    #### Benchmark results
    
    Hardware: Intel Xeon Platinum 8457C, 48 KiB L1D and 2 MiB private L2 per
    core. Release build pinned to one CPU. Values below are CPU-time medians
    in microseconds.
    
    L1-sized working set: 4K `uint32_t` values, measured with 9 randomized
    repetitions. Each iteration touches a 16 KiB output and 1-7 KiB of
    packed input; including the benchmark's reference output, all buffers
    total 33-39 KiB and fit in the 48 KiB L1D.
    
    | bit width | scalar | PDEP | speedup |
    | ---: | ---: | ---: | ---: |
    | 2 | 0.628 | 0.338 | 1.86x |
    | 4 | 0.551 | 0.336 | 1.64x |
    | 6 | 0.969 | 0.355 | 2.73x |
    | 8 | 0.224 | 0.221 | 1.01x |
    | 10 | 1.216 | 0.565 | 2.15x |
    | 12 | 0.870 | 0.564 | 1.54x |
    | 14 | 0.828 | 0.737 | 1.12x |
    
    L2-sized working set: 256K `uint32_t` values. The output is 1 MiB and
    the packed input is 64-448 KiB.
    
    | bit width | scalar | PDEP | speedup |
    | ---: | ---: | ---: | ---: |
    | 2 | 41.0 | 27.9 | 1.47x |
    | 4 | 38.5 | 29.0 | 1.33x |
    | 6 | 61.6 | 28.6 | 2.15x |
    | 8 | 32.5 | 31.5 | 1.03x |
    | 10 | 77.6 | 36.0 | 2.16x |
    | 12 | 51.1 | 35.9 | 1.42x |
    | 14 | 50.6 | 47.1 | 1.07x |
    
    The result is workload- and bit-width-dependent. PDEP is faster for all
    measured widths while the working set remains in L1 or L2, although the
    gains at widths 8 and 14 are marginal. At 1M values, the actual PDEP
    path is faster than the scalar path at widths 10, 12, and 14, but slower
    at widths 2, 4, 6, and 8.
    
    #### High-width dispatch validation
    
    Widths 16-32 were measured twice independently. The following table is
    the second run with 9 randomized repetitions; `scalar/PDEP` greater than
    1 means PDEP is faster.
    
    | bit width | 256K scalar | 256K PDEP | scalar/PDEP | 1M scalar | 1M
    PDEP | scalar/PDEP |
    | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
    | 16 | 27.84 | 55.89 | 0.498x | 234.49 | 273.85 | 0.856x |
    | 17 | 62.60 | 60.04 | 1.043x | 253.42 | 255.63 | 0.991x |
    | 18 | 65.04 | 56.52 | 1.151x | 263.04 | 258.88 | 1.016x |
    | 19 | 77.90 | 59.92 | 1.300x | 313.56 | 261.44 | 1.199x |
    | 20 | 63.27 | 55.17 | 1.147x | 283.19 | 264.64 | 1.070x |
    | 21 | 67.23 | 58.28 | 1.154x | 277.33 | 269.93 | 1.027x |
    | 22 | 70.41 | 57.38 | 1.227x | 284.12 | 271.71 | 1.046x |
    | 23 | 68.79 | 68.49 | 1.004x | 281.42 | 289.78 | 0.971x |
    | 24 | 73.75 | 56.84 | 1.297x | 342.26 | 278.40 | 1.229x |
    | 25 | 67.05 | 81.08 | 0.827x | 291.52 | 330.73 | 0.881x |
    | 26 | 69.09 | 79.70 | 0.867x | 290.12 | 324.68 | 0.894x |
    | 27 | 66.78 | 61.01 | 1.095x | 285.06 | 293.61 | 0.971x |
    | 28 | 63.18 | 75.05 | 0.842x | 290.85 | 310.27 | 0.937x |
    | 29 | 64.87 | 58.74 | 1.104x | 295.88 | 304.93 | 0.970x |
    | 30 | 64.47 | 60.25 | 1.070x | 303.09 | 307.15 | 0.987x |
    | 31 | 53.94 | 66.08 | 0.816x | 316.73 | 310.85 | 1.019x |
    | 32 | 42.03 | 71.03 | 0.592x | 297.63 | 333.45 | 0.893x |
    
    The first independent run showed the same material regressions at widths
    16, 25, 26, 28, and 32. The `uint16_t` width-16 boundary was also slower
    with PDEP at 4K, 256K, and 1M values. Some high widths improve, but the
    profitable set changes with the working set and has no monotonic
    boundary. The production path therefore conservatively retains scalar
    unpacking for all widths at or above 16. After this change, the actual
    entry point was benchmarked again: width 15 follows direct PDEP, while
    widths 16-32 follow the scalar path and all outputs match the scalar
    reference.
    
    ### Release note
    
    Improve bit-packed integer decoding for bit widths below 16 on BMI2 and
    AVX2 capable x86 CPUs.
    
    ### Check List (For Author)
    
    - Test:
    - [x] Unit Test: `./run-be-ut.sh -j 48 --run --filter=BitPackingTest.*`
    - [x] Manual test: compiled and linked `benchmark_test`; ran scalar,
    direct PDEP, and actual-path cases at 4K, 256K, and 1M values
        - [x] `build-support/check-format.sh`
    - [x] `build-support/run-clang-tidy.sh --build-dir be/build_Release
    --files be/benchmark/benchmark_main.cpp
    be/test/util/bit_packing_test.cpp`
    - Behavior changed: Yes. Supported x86 CPUs use PDEP for complete
    32-value batches only when `BIT_WIDTH < 16`; all other cases retain the
    scalar path.
    - Does this need documentation: No
    
    ---------
    
    Co-authored-by: Dongyang Li <[email protected]>
---
 be/benchmark/benchmark_main.cpp        |   4 +
 be/benchmark/benchmark_pdep_unpack.hpp | 207 +++++++++++++++++++++++++++++++++
 be/src/common/config.cpp               |   4 +
 be/src/common/config.h                 |   1 +
 be/src/util/bit_packing.inline.h       |  22 +++-
 be/src/util/pdep_unpack.h              | 169 +++++++++++++++++++++++++++
 be/test/util/bit_packing_test.cpp      | 130 +++++++++++++++++++++
 7 files changed, 536 insertions(+), 1 deletion(-)

diff --git a/be/benchmark/benchmark_main.cpp b/be/benchmark/benchmark_main.cpp
index efb2930f836..d6ac9c113f4 100644
--- a/be/benchmark/benchmark_main.cpp
+++ b/be/benchmark/benchmark_main.cpp
@@ -29,10 +29,12 @@
 #include "benchmark_fmod.hpp"
 #include "benchmark_hll_merge.hpp"
 #include "benchmark_hybrid_set.hpp"
+#include "benchmark_pdep_unpack.hpp"
 #include "benchmark_string.hpp"
 #include "benchmark_string_replace.hpp"
 #include "benchmark_zone_map_index.hpp"
 #include "binary_cast_benchmark.hpp"
+#include "common/config.h"
 #include "core/block/block.h"
 #include "core/column/column_string.h"
 #include "core/data_type/data_type.h"
@@ -76,6 +78,8 @@ BENCHMARK(Example1);
 // ThreadContext + mem tracker, otherwise the allocator throws E-7412. Mirrors
 // the minimal subset of be/test/testutil/run_all_tests.cpp::main.
 int main(int argc, char** argv) {
+    doris::config::enable_bmi2_optimizations = true;
+
     SCOPED_INIT_THREAD_CONTEXT();
     doris::ExecEnv::GetInstance()->init_mem_tracker();
     doris::thread_context()->thread_mem_tracker_mgr->init();
diff --git a/be/benchmark/benchmark_pdep_unpack.hpp 
b/be/benchmark/benchmark_pdep_unpack.hpp
new file mode 100644
index 00000000000..65899475917
--- /dev/null
+++ b/be/benchmark/benchmark_pdep_unpack.hpp
@@ -0,0 +1,207 @@
+// 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
+
+#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
+
+#include <benchmark/benchmark.h>
+
+#include <cstdint>
+#include <random>
+#include <vector>
+
+#include "util/bit_stream_utils.inline.h"
+#include "util/pdep_unpack.h"
+
+namespace doris {
+namespace {
+
+constexpr int kPdepUnpackBatchSize = 32;
+constexpr int kPdepUnpackL1NumValues = 1 << 12;
+constexpr int kPdepUnpackL2NumValues = 1 << 18;
+constexpr int kPdepUnpackLargeNumValues = 1 << 20;
+const std::vector<int64_t> kPdepUnpackNumValues = {
+        32, 64, 128, kPdepUnpackL1NumValues, kPdepUnpackL2NumValues, 
kPdepUnpackLargeNumValues};
+
+template <typename T, int BIT_WIDTH>
+void scalar_unpack(const uint8_t* input, int num_values, T* output) {
+    constexpr int bytes_per_batch = kPdepUnpackBatchSize * BIT_WIDTH / 8;
+    for (int i = 0; i < num_values; i += kPdepUnpackBatchSize) {
+        BitPacking::Unpack32Values<T, BIT_WIDTH>(input, bytes_per_batch, 
output + i);
+        input += bytes_per_batch;
+    }
+}
+
+template <typename T, int BIT_WIDTH>
+void pdep_unpack(const uint8_t* input, int num_values, T* output) {
+    for (int i = 0; i < num_values; i += kPdepUnpackBatchSize) {
+        PdepUnpack::unpack32<T, BIT_WIDTH>(input, output + i);
+        input += kPdepUnpackBatchSize * BIT_WIDTH / 8;
+    }
+}
+
+template <typename T, int BIT_WIDTH, bool USE_PDEP>
+void unpack_if_supported(const uint8_t* input, int num_values, T* output) {
+    if constexpr (PdepUnpack::is_supported_type<T, BIT_WIDTH>()) {
+        if constexpr (USE_PDEP) {
+            pdep_unpack<T, BIT_WIDTH>(input, num_values, output);
+        } else {
+            scalar_unpack<T, BIT_WIDTH>(input, num_values, output);
+        }
+    } else {
+        __builtin_unreachable();
+    }
+}
+
+template <typename T, bool USE_PDEP>
+void unpack(int bit_width, const uint8_t* input, int num_values, T* output) {
+#define UNPACK_CASE(width)                                                  \
+    case width:                                                             \
+        unpack_if_supported<T, width, USE_PDEP>(input, num_values, output); \
+        return
+    switch (bit_width) {
+        UNPACK_CASE(1);
+        UNPACK_CASE(2);
+        UNPACK_CASE(3);
+        UNPACK_CASE(4);
+        UNPACK_CASE(5);
+        UNPACK_CASE(6);
+        UNPACK_CASE(7);
+        UNPACK_CASE(8);
+        UNPACK_CASE(9);
+        UNPACK_CASE(10);
+        UNPACK_CASE(11);
+        UNPACK_CASE(12);
+        UNPACK_CASE(13);
+        UNPACK_CASE(14);
+        UNPACK_CASE(15);
+        UNPACK_CASE(16);
+        UNPACK_CASE(17);
+        UNPACK_CASE(18);
+        UNPACK_CASE(19);
+        UNPACK_CASE(20);
+        UNPACK_CASE(21);
+        UNPACK_CASE(22);
+        UNPACK_CASE(23);
+        UNPACK_CASE(24);
+        UNPACK_CASE(25);
+        UNPACK_CASE(26);
+        UNPACK_CASE(27);
+        UNPACK_CASE(28);
+        UNPACK_CASE(29);
+        UNPACK_CASE(30);
+        UNPACK_CASE(31);
+        UNPACK_CASE(32);
+    default:
+        __builtin_unreachable();
+    }
+#undef UNPACK_CASE
+}
+
+template <typename T>
+struct PdepUnpackBenchmarkData {
+    PdepUnpackBenchmarkData(int bit_width, int num_values)
+            : input(num_values * bit_width / 8),
+              scalar_output(num_values),
+              pdep_output(num_values) {
+        std::mt19937_64 rng(0x17993);
+        for (auto& byte : input) {
+            byte = static_cast<uint8_t>(rng());
+        }
+        unpack<T, false>(bit_width, input.data(), num_values, 
scalar_output.data());
+    }
+
+    std::vector<uint8_t> input;
+    std::vector<T> scalar_output;
+    std::vector<T> pdep_output;
+};
+
+template <typename T>
+void BM_ScalarUnpack(benchmark::State& state) {
+    const int bit_width = static_cast<int>(state.range(0));
+    const int num_values = static_cast<int>(state.range(1));
+    PdepUnpackBenchmarkData<T> data(bit_width, num_values);
+    for (auto _ : state) {
+        unpack<T, false>(bit_width, data.input.data(), num_values, 
data.scalar_output.data());
+        benchmark::ClobberMemory();
+    }
+    state.SetItemsProcessed(state.iterations() * num_values);
+}
+
+template <typename T>
+void BM_PdepUnpack(benchmark::State& state) {
+    const int bit_width = static_cast<int>(state.range(0));
+    const int num_values = static_cast<int>(state.range(1));
+    if (!PdepUnpack::is_supported()) {
+        state.SkipWithError("CPU does not support BMI2 and AVX2");
+        return;
+    }
+    PdepUnpackBenchmarkData<T> data(bit_width, num_values);
+    unpack<T, true>(bit_width, data.input.data(), num_values, 
data.pdep_output.data());
+    if (data.scalar_output != data.pdep_output) {
+        state.SkipWithError("PDEP+AVX2 output differs from scalar output");
+        return;
+    }
+    for (auto _ : state) {
+        unpack<T, true>(bit_width, data.input.data(), num_values, 
data.pdep_output.data());
+        benchmark::ClobberMemory();
+    }
+    state.SetItemsProcessed(state.iterations() * num_values);
+}
+
+template <typename T>
+void BM_ActualUnpack(benchmark::State& state) {
+    const int bit_width = static_cast<int>(state.range(0));
+    const int num_values = static_cast<int>(state.range(1));
+    PdepUnpackBenchmarkData<T> data(bit_width, num_values);
+    auto result = BitPacking::UnpackValues(bit_width, data.input.data(), 
data.input.size(),
+                                           num_values, 
data.pdep_output.data());
+    if (data.scalar_output != data.pdep_output) {
+        state.SkipWithError("Actual output differs from scalar output");
+        return;
+    }
+    for (auto _ : state) {
+        result = BitPacking::UnpackValues(bit_width, data.input.data(), 
data.input.size(),
+                                          num_values, data.pdep_output.data());
+        benchmark::DoNotOptimize(result);
+        benchmark::ClobberMemory();
+    }
+    state.SetItemsProcessed(state.iterations() * num_values);
+}
+
+#define REGISTER_PDEP_UNPACK_BENCHMARK(type, max_bit_width)                    
                \
+    BENCHMARK_TEMPLATE(BM_ScalarUnpack, type)                                  
                \
+            ->ArgsProduct(                                                     
                \
+                    {benchmark::CreateDenseRange(1, max_bit_width, 1), 
kPdepUnpackNumValues}); \
+    BENCHMARK_TEMPLATE(BM_PdepUnpack, type)                                    
                \
+            ->ArgsProduct(                                                     
                \
+                    {benchmark::CreateDenseRange(1, max_bit_width, 1), 
kPdepUnpackNumValues}); \
+    BENCHMARK_TEMPLATE(BM_ActualUnpack, type)                                  
                \
+            ->ArgsProduct(                                                     
                \
+                    {benchmark::CreateDenseRange(1, max_bit_width, 1), 
kPdepUnpackNumValues})
+
+REGISTER_PDEP_UNPACK_BENCHMARK(uint8_t, 8);
+REGISTER_PDEP_UNPACK_BENCHMARK(uint16_t, 16);
+REGISTER_PDEP_UNPACK_BENCHMARK(uint32_t, 32);
+
+#undef REGISTER_PDEP_UNPACK_BENCHMARK
+
+} // namespace
+} // namespace doris
+
+#endif
diff --git a/be/src/common/config.cpp b/be/src/common/config.cpp
index 9f697a2dcbe..ffb6cde5392 100644
--- a/be/src/common/config.cpp
+++ b/be/src/common/config.cpp
@@ -467,6 +467,10 @@ DEFINE_mDouble(sparse_column_compaction_threshold_percent, 
"0.05");
 // Enable RLE batch Put optimization for compaction
 DEFINE_mBool(enable_rle_batch_put_optimization, "true");
 
+// Enable PDEP-based bit unpacking. Disable it on CPUs where PDEP is 
microcoded and slower than
+// the scalar implementation, such as AMD Zen+ and Zen 2.
+DEFINE_Bool(enable_bmi2_optimizations, "true");
+
 // If enabled, segments will be flushed column by column
 DEFINE_mBool(enable_vertical_segment_writer, "true");
 
diff --git a/be/src/common/config.h b/be/src/common/config.h
index f5c72eb776d..85f6a2bf146 100644
--- a/be/src/common/config.h
+++ b/be/src/common/config.h
@@ -536,6 +536,7 @@ DECLARE_mInt64(vertical_compaction_max_segment_size);
 DECLARE_mDouble(sparse_column_compaction_threshold_percent);
 // Enable RLE batch Put optimization for compaction
 DECLARE_mBool(enable_rle_batch_put_optimization);
+DECLARE_Bool(enable_bmi2_optimizations);
 
 // If enabled, segments will be flushed column by column
 DECLARE_mBool(enable_vertical_segment_writer);
diff --git a/be/src/util/bit_packing.inline.h b/be/src/util/bit_packing.inline.h
index b4e0805d8ae..d0de57a836b 100644
--- a/be/src/util/bit_packing.inline.h
+++ b/be/src/util/bit_packing.inline.h
@@ -20,6 +20,9 @@
 #include <boost/preprocessor/repetition/repeat_from_to.hpp>
 
 #include "util/bit_packing.h"
+#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
+#include "util/pdep_unpack.h"
+#endif
 
 namespace doris {
 inline int64_t BitPacking::NumValuesToUnpack(int bit_width, int64_t in_bytes, 
int64_t num_values) {
@@ -83,8 +86,25 @@ std::pair<const uint8_t*, int64_t> 
BitPacking::UnpackValues(const uint8_t* __res
     const uint8_t* in_pos = in;
     OutType* out_pos = out;
 
+#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
+    constexpr int MIN_PDEP_BATCHES = 4;
+    int64_t batches_read = 0;
+    if constexpr (PdepUnpack::should_use<OutType, BIT_WIDTH>()) {
+        if (batches_to_read >= MIN_PDEP_BATCHES && PdepUnpack::is_supported()) 
{
+            for (; batches_read < batches_to_read; ++batches_read) {
+                PdepUnpack::unpack32<OutType, BIT_WIDTH>(in_pos, out_pos);
+                in_pos += (BATCH_SIZE * BIT_WIDTH) / CHAR_BIT;
+                out_pos += BATCH_SIZE;
+                in_bytes -= (BATCH_SIZE * BIT_WIDTH) / CHAR_BIT;
+            }
+        }
+    }
+#else
+    constexpr int64_t batches_read = 0;
+#endif
+
     // First unpack as many full batches as possible.
-    for (int64_t i = 0; i < batches_to_read; ++i) {
+    for (int64_t i = batches_read; i < batches_to_read; ++i) {
         in_pos = Unpack32Values<OutType, BIT_WIDTH>(in_pos, in_bytes, out_pos);
         out_pos += BATCH_SIZE;
         in_bytes -= (BATCH_SIZE * BIT_WIDTH) / CHAR_BIT;
diff --git a/be/src/util/pdep_unpack.h b/be/src/util/pdep_unpack.h
new file mode 100644
index 00000000000..10403d94e4a
--- /dev/null
+++ b/be/src/util/pdep_unpack.h
@@ -0,0 +1,169 @@
+// 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
+
+#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
+
+#include <immintrin.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <limits>
+#include <type_traits>
+#include <utility>
+
+#include "common/config.h"
+
+namespace doris {
+
+class PdepUnpack {
+public:
+    static bool is_supported() {
+        return config::enable_bmi2_optimizations && 
__builtin_cpu_supports("bmi2") &&
+               __builtin_cpu_supports("avx2");
+    }
+
+    template <typename T, int BIT_WIDTH>
+    static constexpr bool is_supported_type() {
+        return BIT_WIDTH > 0 && BIT_WIDTH <= std::numeric_limits<T>::digits &&
+               (std::is_same_v<T, uint8_t> || std::is_same_v<T, uint16_t> ||
+                std::is_same_v<T, uint32_t>);
+    }
+
+    template <typename T, int BIT_WIDTH>
+    static constexpr bool should_use() {
+        // Keep the generic implementation available for benchmarking all 
supported widths, but
+        // only select PDEP in the production path for widths below 16. These 
widths can use the
+        // byte/word deposit layouts and the AVX2 widening specializations 
below. At 16 bits and
+        // above, unpack32() falls back to multiple generic 64-bit PDEP groups 
per batch and
+        // competes with efficient scalar specializations, including copy-like 
16- and 32-bit
+        // cases. Benchmarks with L1-, L2-, and larger working sets show 
non-monotonic results and
+        // repeatable regressions for multiple high widths. Because the 
profitable high widths are
+        // CPU- and working-set-dependent, an irregular per-width allowlist 
would not be portable;
+        // use the scalar implementation conservatively instead.
+        return is_supported_type<T, BIT_WIDTH>() && BIT_WIDTH < 16;
+    }
+
+    template <typename T, int BIT_WIDTH>
+    __attribute__((target("bmi2,avx2"))) static void unpack32(const uint8_t* 
input, T* output) {
+        static_assert(is_supported_type<T, BIT_WIDTH>());
+        if constexpr (std::is_same_v<T, uint8_t>) {
+            unpack32_with_pdep<T, BIT_WIDTH>(input, output);
+        } else if constexpr (std::is_same_v<T, uint16_t> && BIT_WIDTH <= 8) {
+            for (int group = 0; group < 2; ++group) {
+                const int first_value = group * 16;
+                const uint8_t* group_input = input + group * 2 * BIT_WIDTH;
+                uint64_t expanded0 = _pdep_u64(load_packed_group<0, 8 * 
BIT_WIDTH>(group_input),
+                                               pdep_mask<uint8_t, 
BIT_WIDTH>());
+                uint64_t expanded1 =
+                        _pdep_u64(load_packed_group<8 * BIT_WIDTH, 8 * 
BIT_WIDTH>(group_input),
+                                  pdep_mask<uint8_t, BIT_WIDTH>());
+                __m128i packed8 = 
_mm_set_epi64x(static_cast<int64_t>(expanded1),
+                                                 
static_cast<int64_t>(expanded0));
+                __m256i unpacked16 = _mm256_cvtepu8_epi16(packed8);
+                _mm256_storeu_si256(reinterpret_cast<__m256i*>(output + 
first_value), unpacked16);
+            }
+        } else if constexpr (std::is_same_v<T, uint32_t> && BIT_WIDTH <= 8) {
+            for (int group = 0; group < 4; ++group) {
+                uint64_t expanded =
+                        _pdep_u64(load_packed_group<0, 8 * BIT_WIDTH>(input + 
group * BIT_WIDTH),
+                                  pdep_mask<uint8_t, BIT_WIDTH>());
+                __m256i unpacked32 = 
_mm256_cvtepu8_epi32(_mm_cvtsi64_si128(expanded));
+                _mm256_storeu_si256(reinterpret_cast<__m256i*>(output + group 
* 8), unpacked32);
+            }
+        } else if constexpr (std::is_same_v<T, uint32_t> && BIT_WIDTH <= 15) {
+            for (int group = 0; group < 4; ++group) {
+                const int first_value = group * 8;
+                const uint8_t* group_input = input + group * BIT_WIDTH;
+                uint64_t expanded0 = _pdep_u64(load_packed_group<0, 4 * 
BIT_WIDTH>(group_input),
+                                               pdep_mask<uint16_t, 
BIT_WIDTH>());
+                uint64_t expanded1 =
+                        _pdep_u64(load_packed_group<4 * BIT_WIDTH, 4 * 
BIT_WIDTH>(group_input),
+                                  pdep_mask<uint16_t, BIT_WIDTH>());
+                __m128i packed16 = 
_mm_set_epi64x(static_cast<int64_t>(expanded1),
+                                                  
static_cast<int64_t>(expanded0));
+                __m256i unpacked32 = _mm256_cvtepu16_epi32(packed16);
+                _mm256_storeu_si256(reinterpret_cast<__m256i*>(output + 
first_value), unpacked32);
+            }
+        } else {
+            unpack32_with_pdep<T, BIT_WIDTH>(input, output);
+        }
+    }
+
+private:
+    template <typename T, int BIT_WIDTH>
+    static constexpr uint64_t pdep_mask() {
+        constexpr int lane_bits = sizeof(T) * 8;
+        constexpr int lanes = 64 / lane_bits;
+        constexpr uint64_t value_mask = (1ULL << BIT_WIDTH) - 1;
+        uint64_t mask = 0;
+        for (int lane = 0; lane < lanes; ++lane) {
+            mask |= value_mask << (lane * lane_bits);
+        }
+        return mask;
+    }
+
+    template <int BIT_OFFSET, int PACKED_BITS>
+    static uint64_t load_packed_group(const uint8_t* input) {
+        constexpr int byte_offset = BIT_OFFSET / 8;
+        constexpr int shift = BIT_OFFSET % 8;
+        constexpr int bytes_needed = (shift + PACKED_BITS + 7) / 8;
+        static_assert(PACKED_BITS <= 64);
+        static_assert(bytes_needed <= 9);
+
+        uint64_t low = 0;
+        std::memcpy(&low, input + byte_offset, bytes_needed < 8 ? bytes_needed 
: 8);
+        if constexpr (bytes_needed <= 8) {
+            return low >> shift;
+        } else {
+            uint8_t high = input[byte_offset + 8];
+            return static_cast<uint64_t>((static_cast<unsigned __int128>(high) 
<< 64 | low) >>
+                                         shift);
+        }
+    }
+
+    template <typename T, int BIT_WIDTH, std::size_t GROUP>
+    __attribute__((target("bmi2"))) static void unpack_group(const uint8_t* 
input, T* output) {
+        constexpr int lanes = 64 / (sizeof(T) * 8);
+        constexpr int first_value = GROUP * lanes;
+        constexpr int bit_offset = first_value * BIT_WIDTH;
+        constexpr int packed_bits = lanes * BIT_WIDTH;
+        uint64_t packed = load_packed_group<bit_offset, packed_bits>(input);
+        uint64_t expanded = _pdep_u64(packed, pdep_mask<T, BIT_WIDTH>());
+        std::memcpy(output + first_value, &expanded, sizeof(expanded));
+    }
+
+    template <typename T, int BIT_WIDTH, std::size_t... GROUPS>
+    __attribute__((target("bmi2"))) static void unpack32_with_pdep_impl(
+            const uint8_t* input, T* output, std::index_sequence<GROUPS...>) {
+        (unpack_group<T, BIT_WIDTH, GROUPS>(input, output), ...);
+    }
+
+    template <typename T, int BIT_WIDTH>
+    __attribute__((target("bmi2"))) static void unpack32_with_pdep(const 
uint8_t* input,
+                                                                   T* output) {
+        constexpr int lanes = 64 / (sizeof(T) * 8);
+        unpack32_with_pdep_impl<T, BIT_WIDTH>(input, output,
+                                              std::make_index_sequence<32 / 
lanes> {});
+    }
+};
+
+} // namespace doris
+
+#endif
diff --git a/be/test/util/bit_packing_test.cpp 
b/be/test/util/bit_packing_test.cpp
new file mode 100644
index 00000000000..26d6708b63e
--- /dev/null
+++ b/be/test/util/bit_packing_test.cpp
@@ -0,0 +1,130 @@
+// 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 <algorithm>
+#include <cstdint>
+#include <limits>
+#include <vector>
+
+#include "util/bit_stream_utils.inline.h"
+#include "util/faststring.h"
+
+namespace doris {
+namespace {
+
+#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
+static_assert(PdepUnpack::should_use<uint16_t, 15>());
+static_assert(PdepUnpack::should_use<uint32_t, 15>());
+static_assert(!PdepUnpack::should_use<uint16_t, 16>());
+static_assert(!PdepUnpack::should_use<uint32_t, 16>());
+static_assert(!PdepUnpack::should_use<uint32_t, 32>());
+#endif
+
+template <typename T>
+std::vector<T> make_values(int bit_width, int num_values) {
+    const uint64_t mask = bit_width == std::numeric_limits<T>::digits
+                                  ? std::numeric_limits<T>::max()
+                                  : (1ULL << bit_width) - 1;
+    std::vector<T> values(num_values);
+    for (int i = 0; i < num_values; ++i) {
+        values[i] = static_cast<T>((0x9E3779B9ULL * i + 0x7F4A7C15ULL) & mask);
+    }
+    return values;
+}
+
+template <typename T>
+void pack_values(const std::vector<T>& values, int bit_width, faststring* 
packed) {
+    BitWriter writer(packed);
+    for (T value : values) {
+        writer.PutValue(value, bit_width);
+    }
+    writer.Flush();
+}
+
+template <typename T>
+void test_all_bit_widths(int num_values) {
+    for (int bit_width = 1; bit_width <= std::numeric_limits<T>::digits; 
++bit_width) {
+        std::vector<T> expected = make_values<T>(bit_width, num_values);
+        faststring packed;
+        pack_values(expected, bit_width, &packed);
+        std::vector<T> actual(num_values);
+
+        auto [end, values_read] =
+                BitPacking::UnpackValues(bit_width, reinterpret_cast<const 
uint8_t*>(packed.data()),
+                                         packed.size(), num_values, 
actual.data());
+
+        EXPECT_EQ(values_read, num_values) << "bit_width=" << bit_width;
+        EXPECT_EQ(end, reinterpret_cast<const uint8_t*>(packed.data()) + 
packed.size())
+                << "bit_width=" << bit_width;
+        EXPECT_EQ(actual, expected) << "bit_width=" << bit_width;
+    }
+}
+
+template <typename T>
+void test_truncated_input() {
+    constexpr int num_values = 160;
+    constexpr T sentinel = std::numeric_limits<T>::max();
+    for (int bit_width = 1; bit_width <= std::numeric_limits<T>::digits; 
++bit_width) {
+        std::vector<T> expected = make_values<T>(bit_width, num_values);
+        faststring packed;
+        pack_values(expected, bit_width, &packed);
+        const auto* packed_data = reinterpret_cast<const 
uint8_t*>(packed.data());
+        std::vector<uint8_t> input(packed_data, packed_data + packed.size() - 
1);
+        const int64_t input_bytes = input.size();
+        const int64_t expected_values_read = input_bytes * 8 / bit_width;
+        const int64_t expected_bytes_read = (expected_values_read * bit_width 
+ 7) / 8;
+        std::vector<T> actual(num_values, sentinel);
+
+        auto [end, values_read] = BitPacking::UnpackValues(bit_width, 
input.data(), input_bytes,
+                                                           num_values, 
actual.data());
+
+        EXPECT_EQ(values_read, expected_values_read) << "bit_width=" << 
bit_width;
+        EXPECT_EQ(end, input.data() + expected_bytes_read) << "bit_width=" << 
bit_width;
+        EXPECT_TRUE(std::equal(expected.begin(), expected.begin() + 
expected_values_read,
+                               actual.begin()))
+                << "bit_width=" << bit_width;
+        EXPECT_TRUE(std::all_of(actual.begin() + expected_values_read, 
actual.end(),
+                                [](T value) { return value == sentinel; }))
+                << "bit_width=" << bit_width;
+    }
+}
+
+TEST(BitPackingTest, PdepUnpackAllBitWidths) {
+    for (int num_values : {127, 128, 129}) {
+        test_all_bit_widths<uint8_t>(num_values);
+        test_all_bit_widths<uint16_t>(num_values);
+        test_all_bit_widths<uint32_t>(num_values);
+    }
+}
+
+TEST(BitPackingTest, PdepUnpackTruncatedInput) {
+    test_truncated_input<uint8_t>();
+    test_truncated_input<uint16_t>();
+    test_truncated_input<uint32_t>();
+}
+
+#if defined(__x86_64__) && (defined(__GNUC__) || defined(__clang__))
+TEST(BitPackingTest, DisableBmi2Optimizations) {
+    const bool was_enabled = config::enable_bmi2_optimizations;
+    config::enable_bmi2_optimizations = false;
+    EXPECT_FALSE(PdepUnpack::is_supported());
+    config::enable_bmi2_optimizations = was_enabled;
+}
+#endif
+
+} // namespace
+} // namespace doris


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

Reply via email to