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

HappenLee 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 9c8f7b2d2e2 [improvement](be) Compact selected row indices with mask 
lookups (#69818)
9c8f7b2d2e2 is described below

commit 9c8f7b2d2e2c6130707e6d182fd9c2355054133a
Author: HappenLee <[email protected]>
AuthorDate: Fri Oct 30 15:44:06 2026 +0800

    [improvement](be) Compact selected row indices with mask lookups (#69818)
    
    `SegmentIterator` expands partially selected SIMD masks one set bit at a
    time, creating a serial dependency for every selected row. Expand x86
    masks with more than two selected rows using a constexpr eight-row
    lookup table and fixed-width stores. Keep the existing bit iteration for
    sparse masks and other architectures. Empty/full masks, scalar tails,
    row order and downstream column materialization retain their existing
    behavior.
    
    The output selector already has one slot per input row. At a complete
    SIMD block, the number of emitted indices is at most the input position,
    so the fixed-width stores fit within that allocation; only the returned
    prefix is consumed. The lookup table has constant initialization and
    introduces no dynamic allocation or mutable shared state.
    
    Add unit coverage and a standalone benchmark that measures the original
    loop and the actual production helper. On an Intel Xeon Platinum 8457C,
    with 4096-row batches, random 50% selection and Clang 20.1.8 `-O3
    -march=native`:
    
    | Benchmark | Before (ns/input row) | After (ns/input row) | Speedup |
    | --- | ---: | ---: | ---: |
    | Row-index generation | 0.693280 | 0.119248 | 5.81x |
    | Predicate comparison + selection + 8-byte copy | 1.170464 | 0.643518 |
    1.82x |
    
    Performance tradeoff: Clang 16.0.6 with `-march=x86-64-v3` regresses
    from 0.104319 to 0.119946 ns/input row at random 1% selection (about 15%
    slower). The Clang 20 native 1% case is approximately unchanged. These
    are standalone measurements; complete SQL query speedup has not been
    measured.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [ ] Unit Test
        - [x] Manual test (details below)
    
    Validation completed:
    
    - Standalone benchmark with native and AVX2 compiler settings.
    - ASan + UBSan verification of all 256 byte masks, 65536 mixed masks,
    scalar tails, row order, output bounds, near-uint16 row limits and
    payload copies.
    - clang-format 16.0.6, repository build-hygiene checks and focused
    clang-tidy for the helper, test source and standalone benchmark.
    - Prepared this PR as one commit on the latest master; the helper, test
    and benchmark source blobs are identical to the verified versions.
    
    Unit tests added: empty/full/single-bit masks, exhaustive byte and mixed
    masks, large row bases and consecutive appends. They have **not executed
    through the standard BE test runner**. Both `./run-be-ut.sh -j 48 --run
    --filter=SelectionTest.*:BitsTest.*` and `BUILD_TYPE=ASAN ./build.sh
    --be -j 48` were attempted in the development workspace, but compilation
    failed in unrelated existing local `ColumnVector` edits at
    `column_vector.h:95` before the tests could execute. Full BE build and
    full `SegmentIterator` static analysis remain incomplete. No SQL or ARM
    execution.
    
    Standalone reproduction (x86, from the repository root):
    
    ```bash
    c++ -O3 -DNDEBUG -std=c++20 -march=native -Ibe/src \
        be/benchmark/selection_compaction_bench.cpp -o 
/tmp/selection_compaction_bench
    /tmp/selection_compaction_bench --verify-only
    /tmp/selection_compaction_bench --pattern random --rate 50 --mode select \
        --repeats 9 --min-ms 60
    /tmp/selection_compaction_bench --pattern random --rate 50 --input values \
        --mode copy8 --repeats 9 --min-ms 80
    `
---
 be/benchmark/selection_compaction_bench.cpp | 669 ++++++++++++++++++++++++++++
 be/src/storage/segment/segment_iterator.cpp |  20 +-
 be/src/util/simd/selection.h                |  75 ++++
 be/test/util/simd/selection_test.cpp        | 114 +++++
 4 files changed, 873 insertions(+), 5 deletions(-)

diff --git a/be/benchmark/selection_compaction_bench.cpp 
b/be/benchmark/selection_compaction_bench.cpp
new file mode 100644
index 00000000000..56aed8c7bea
--- /dev/null
+++ b/be/benchmark/selection_compaction_bench.cpp
@@ -0,0 +1,669 @@
+// 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.
+
+// Standalone benchmark; no Doris libraries or Google Benchmark are required.
+// Build from the repository root:
+// c++ -O3 -DNDEBUG -std=c++20 -march=x86-64-v3 -Ibe/src 
be/benchmark/selection_compaction_bench.cpp -o /tmp/selection_compaction_bench
+// Run: taskset -c <cpu> /tmp/selection_compaction_bench --help
+
+#include <algorithm>
+#include <array>
+#include <chrono>
+#include <cmath>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <iomanip>
+#include <iostream>
+#include <numeric>
+#include <random>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include "util/simd/bits.h"
+#include "util/simd/selection.h"
+
+namespace selection_compaction_bench {
+
+using RowId = uint16_t;
+using Clock = std::chrono::steady_clock;
+using Select = size_t (*)(const int32_t*, size_t, RowId*);
+using Compact = uint16_t (*)(const uint8_t*, uint16_t, RowId*);
+constexpr int32_t THRESHOLD = 100;
+constexpr size_t COMPARISON_BATCH = 64;
+constexpr RowId CANARY = 0xabcd;
+
+struct SelectionTable {
+    std::array<std::array<RowId, 8>, 256> positions {};
+    std::array<uint8_t, 256> counts {};
+
+    constexpr SelectionTable() {
+        for (size_t mask = 0; mask < 256; ++mask) {
+            for (size_t bit = 0; bit < 8; ++bit) {
+                if ((mask & (1U << bit)) != 0) {
+                    positions[mask][counts[mask]++] = static_cast<RowId>(bit);
+                }
+            }
+        }
+    }
+};
+
+constexpr SelectionTable SELECTION_TABLE;
+
+static_assert(doris::simd::bits_mask_length() == 32,
+              "The lookup candidate in this benchmark implements the x86 
32-row mask layout");
+
+inline uint16_t append_indices32(uint32_t mask, uint16_t base, RowId* output) {
+    uint16_t written = 0;
+    for (unsigned offset = 0; offset < 32; offset += 8) {
+        const auto part = static_cast<uint8_t>(mask >> offset);
+        RowId indices[8];
+        for (unsigned lane = 0; lane < 8; ++lane) {
+            indices[lane] =
+                    static_cast<RowId>(base + offset + 
SELECTION_TABLE.positions[part][lane]);
+        }
+        // At most base + offset rows have been emitted, so eight stores fit 
in the input size.
+        std::memcpy(output + written, indices, sizeof(indices));
+        written += SELECTION_TABLE.counts[part];
+    }
+    return written;
+}
+
+// SPARSE_LIMIT=-1 is the original SegmentIterator compaction loop; -2 uses 
the production
+// append helper. Zero selects lookup for every partial mask. Positive limits 
retain ctz for
+// sparse partial masks. Zero/full-mask handling and the scalar tail stay 
identical.
+template <int SPARSE_LIMIT>
+__attribute__((noinline)) uint16_t doris_compact(const uint8_t* flags, 
uint16_t selected_size,
+                                                 RowId* output) {
+    uint16_t new_size = 0;
+    uint16_t sel_pos = 0;
+    const uint16_t sel_end = sel_pos + selected_size;
+    static constexpr size_t SIMD_BYTES = doris::simd::bits_mask_length();
+    const uint16_t sel_end_simd = sel_pos + selected_size / SIMD_BYTES * 
SIMD_BYTES;
+    while (sel_pos < sel_end_simd) {
+        auto mask = doris::simd::bytes_mask_to_bits_mask(flags + sel_pos);
+        if (mask == 0) {
+            // No selected rows.
+        } else if (mask == doris::simd::bits_mask_all()) {
+            for (uint16_t i = 0; i < SIMD_BYTES; ++i) {
+                output[new_size++] = sel_pos + i;
+            }
+        } else {
+            if constexpr (SPARSE_LIMIT == -2) {
+                if (__builtin_popcount(mask) > 2) {
+                    new_size +=
+                            doris::simd::append_selected_indices(mask, 
sel_pos, output + new_size);
+                } else {
+                    doris::simd::iterate_through_bits_mask(
+                            [&](const int bit_pos) {
+                                output[new_size++] = sel_pos + 
static_cast<uint16_t>(bit_pos);
+                            },
+                            mask);
+                }
+            } else if constexpr (SPARSE_LIMIT == -1) {
+                doris::simd::iterate_through_bits_mask(
+                        [&](const int bit_pos) {
+                            output[new_size++] = sel_pos + 
static_cast<uint16_t>(bit_pos);
+                        },
+                        mask);
+            } else {
+                if (SPARSE_LIMIT > 0 && __builtin_popcount(mask) <= 
SPARSE_LIMIT) {
+                    doris::simd::iterate_through_bits_mask(
+                            [&](const int bit_pos) {
+                                output[new_size++] = sel_pos + 
static_cast<uint16_t>(bit_pos);
+                            },
+                            mask);
+                } else {
+                    new_size += append_indices32(mask, sel_pos, output + 
new_size);
+                }
+            }
+        }
+        sel_pos += SIMD_BYTES;
+    }
+    for (; sel_pos < sel_end; ++sel_pos) {
+        if (flags[sel_pos]) {
+            output[new_size++] = sel_pos;
+        }
+    }
+    return new_size;
+}
+
+__attribute__((noinline)) void evaluate(const int32_t* __restrict values, 
size_t rows,
+                                        uint8_t* __restrict flags) {
+    for (size_t i = 0; i < rows; ++i) {
+        flags[i] = values[i] < THRESHOLD;
+    }
+}
+
+__attribute__((noinline)) size_t branched(const int32_t* __restrict values, 
size_t rows,
+                                          RowId* __restrict output) {
+    size_t count = 0;
+    for (size_t i = 0; i < rows; ++i) {
+        if (values[i] < THRESHOLD) {
+            output[count++] = static_cast<RowId>(i);
+        }
+    }
+    return count;
+}
+
+__attribute__((noinline)) size_t branchless(const int32_t* __restrict values, 
size_t rows,
+                                            RowId* __restrict output) {
+    size_t count = 0;
+    for (size_t i = 0; i < rows; ++i) {
+        output[count] = static_cast<RowId>(i);
+        count += values[i] < THRESHOLD;
+    }
+    return count;
+}
+
+inline uint8_t pack_eight(const uint8_t* matches) {
+    uint64_t word;
+    std::memcpy(&word, matches, sizeof(word));
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+    word = __builtin_bswap64(word);
+#endif
+    return static_cast<uint8_t>((word * UINT64_C(0x0102040810204080)) >> 56);
+}
+
+template <bool LOOKUP>
+__attribute__((noinline)) size_t batched(const int32_t* __restrict values, 
size_t rows,
+                                         RowId* __restrict output) {
+    size_t count = 0;
+    size_t base = 0;
+    for (; base + COMPARISON_BATCH <= rows; base += COMPARISON_BATCH) {
+        uint8_t matches[COMPARISON_BATCH];
+        for (size_t i = 0; i < COMPARISON_BATCH; ++i) {
+            matches[i] = values[base + i] < THRESHOLD;
+        }
+        if constexpr (LOOKUP) {
+            for (size_t group = 0; group < COMPARISON_BATCH; group += 8) {
+                const auto mask = pack_eight(matches + group);
+                RowId positions[8];
+                for (size_t lane = 0; lane < 8; ++lane) {
+                    positions[lane] = static_cast<RowId>(base + group +
+                                                         
SELECTION_TABLE.positions[mask][lane]);
+                }
+                // Always write eight entries; only counts[mask] entries 
become valid.
+                // count <= base + group, so a full group fits in rows output 
entries.
+                std::memcpy(output + count, positions, sizeof(positions));
+                count += SELECTION_TABLE.counts[mask];
+            }
+        } else {
+            uint64_t mask = 0;
+            for (size_t group = 0; group < COMPARISON_BATCH; group += 8) {
+                mask |= static_cast<uint64_t>(pack_eight(matches + group)) << 
group;
+            }
+            while (mask != 0) {
+                output[count++] = static_cast<RowId>(base + 
__builtin_ctzll(mask));
+                mask &= mask - 1;
+            }
+        }
+    }
+    for (; base < rows; ++base) {
+        if (values[base] < THRESHOLD) {
+            output[count++] = static_cast<RowId>(base);
+        }
+    }
+    return count;
+}
+
+enum class Algorithm {
+    BRANCHED,
+    BRANCHLESS,
+    LOOKUP,
+    CTZ,
+    FUSED_COPY,
+    DORIS_CTZ,
+    DORIS_LOOKUP,
+    DORIS_HYBRID1,
+    DORIS_HYBRID2,
+    DORIS_HYBRID4,
+    DORIS_HYBRID8,
+    DORIS_HYBRID16,
+    DORIS_PRODUCTION
+};
+constexpr std::array<Select, 4> SELECTORS {branched, branchless, 
batched<true>, batched<false>};
+constexpr std::array<Compact, 8> COMPACTORS {doris_compact<-1>, 
doris_compact<0>, doris_compact<1>,
+                                             doris_compact<2>,  
doris_compact<4>, doris_compact<8>,
+                                             doris_compact<16>, 
doris_compact<-2>};
+constexpr std::array<const char*, 13> NAMES {
+        "branched",      "branchless",     "batch_lookup",    "batch_ctz",     
"fused_copy",
+        "doris_ctz",     "doris_lookup",   "doris_hybrid1",   "doris_hybrid2", 
"doris_hybrid4",
+        "doris_hybrid8", "doris_hybrid16", "doris_production"};
+
+template <size_t WORDS>
+using Payload = std::array<uint64_t, WORDS>;
+
+template <size_t WORDS>
+__attribute__((noinline)) size_t fused_copy(const int32_t* __restrict values,
+                                            const Payload<WORDS>* __restrict 
payload, size_t rows,
+                                            Payload<WORDS>* __restrict output) 
{
+    size_t count = 0;
+    for (size_t i = 0; i < rows; ++i) {
+        if (values[i] < THRESHOLD) {
+            output[count++] = payload[i];
+        }
+    }
+    return count;
+}
+
+struct Options {
+    size_t rows = 1U << 20;
+    size_t batch = 4096;
+    size_t repeats = 7;
+    double min_ms = 20;
+    int rate = -1;
+    std::string pattern = "all";
+    std::string mode = "all";
+    std::string suite = "doris";
+    std::string input = "flags";
+    bool verify_only = false;
+};
+
+template <size_t WORDS>
+struct Data {
+    std::vector<int32_t> values;
+    std::vector<uint8_t> flags;
+    std::vector<Payload<WORDS>> payload;
+    size_t batch;
+    bool evaluate_values;
+
+    Data(size_t rows, size_t batch_size, int rate, const std::string& pattern,
+         bool with_evaluation = false)
+            : values(rows), flags(rows), batch(batch_size), 
evaluate_values(with_evaluation) {
+        std::mt19937 random(0x51ec710);
+        for (size_t base = 0; base < rows; base += batch) {
+            const auto count = std::min(batch, rows - base);
+            const auto passing = (count * rate + 50) / 100;
+            for (size_t i = 0; i < count; ++i) {
+                values[base + i] = i < passing ? static_cast<int32_t>(random() 
% 100)
+                                               : static_cast<int32_t>(100 + 
random() % 10000);
+            }
+            if (pattern == "random") {
+                std::shuffle(values.begin() + base, values.begin() + base + 
count, random);
+            }
+        }
+        for (size_t i = 0; i < rows; ++i) {
+            flags[i] = values[i] < THRESHOLD;
+        }
+        if constexpr (WORDS != 0) {
+            payload.resize(rows);
+            for (size_t i = 0; i < rows; ++i) {
+                for (size_t word = 0; word < WORDS; ++word) {
+                    payload[i][word] = random() ^ (static_cast<uint64_t>(i) << 
32) ^ word;
+                }
+            }
+        }
+    }
+};
+
+template <size_t WORDS>
+struct Scratch {
+    std::vector<RowId> selection;
+    std::vector<uint8_t> flags;
+    std::vector<Payload<WORDS>> copied;
+
+    explicit Scratch(size_t batch) : selection(batch + 8, CANARY), 
flags(batch) {
+        if constexpr (WORDS != 0) {
+            copied.resize(batch + 1);
+        }
+    }
+};
+
+template <size_t WORDS, Algorithm ALGORITHM>
+uint64_t run(const Data<WORDS>& data, Scratch<WORDS>& scratch, size_t passes) {
+    uint64_t total = 0;
+    for (size_t pass = 0; pass < passes; ++pass) {
+        for (size_t base = 0; base < data.values.size(); base += data.batch) {
+            const auto rows = std::min(data.batch, data.values.size() - base);
+            size_t count;
+            if constexpr (ALGORITHM == Algorithm::FUSED_COPY) {
+                count = fused_copy<WORDS>(data.values.data() + base, 
data.payload.data() + base,
+                                          rows, scratch.copied.data());
+            } else {
+                if constexpr (ALGORITHM >= Algorithm::DORIS_CTZ) {
+                    const uint8_t* flags = data.flags.data() + base;
+                    if (data.evaluate_values) {
+                        evaluate(data.values.data() + base, rows, 
scratch.flags.data());
+                        flags = scratch.flags.data();
+                    }
+                    count = COMPACTORS[static_cast<size_t>(ALGORITHM) -
+                                       
static_cast<size_t>(Algorithm::DORIS_CTZ)](
+                            flags, static_cast<uint16_t>(rows), 
scratch.selection.data());
+                } else {
+                    count = SELECTORS[static_cast<size_t>(ALGORITHM)](
+                            data.values.data() + base, rows, 
scratch.selection.data());
+                }
+                if constexpr (WORDS != 0) {
+                    for (size_t i = 0; i < count; ++i) {
+                        scratch.copied[i] = data.payload[base + 
scratch.selection[i]];
+                    }
+                }
+            }
+            total += count;
+            // Make every batch's stores observable without timing a checksum 
over the output.
+            asm volatile(""
+                         :
+                         : "g"(scratch.selection.data()), 
"g"(scratch.copied.data())
+                         : "memory");
+        }
+    }
+    asm volatile("" : : "g"(total) : "memory");
+    return total;
+}
+
+void verify_compaction(const uint8_t* flags, size_t rows) {
+    std::vector<RowId> expected;
+    for (size_t i = 0; i < rows; ++i) {
+        if (flags[i]) {
+            expected.push_back(static_cast<RowId>(i));
+        }
+    }
+    for (auto compact : COMPACTORS) {
+        std::vector<RowId> output(rows + 8, CANARY);
+        const auto count = compact(flags, static_cast<uint16_t>(rows), 
output.data());
+        if (count != expected.size() ||
+            !std::equal(expected.begin(), expected.end(), output.begin()) ||
+            !std::all_of(output.begin() + rows, output.end(),
+                         [](RowId x) { return x == CANARY; })) {
+            throw std::runtime_error("Doris compaction result, row order, or 
bounds mismatch");
+        }
+    }
+}
+
+void verify_selection(const int32_t* values, size_t rows) {
+    std::vector<uint8_t> flags(rows);
+    evaluate(values, rows, flags.data());
+    verify_compaction(flags.data(), rows);
+    std::vector<RowId> expected;
+    for (size_t i = 0; i < rows; ++i) {
+        if (values[i] < THRESHOLD) {
+            expected.push_back(static_cast<RowId>(i));
+        }
+    }
+    for (auto select : SELECTORS) {
+        std::vector<RowId> output(rows + 8, CANARY);
+        const auto count = select(values, rows, output.data());
+        if (count != expected.size() ||
+            !std::equal(expected.begin(), expected.end(), output.begin()) ||
+            !std::all_of(output.begin() + rows, output.end(),
+                         [](RowId x) { return x == CANARY; })) {
+            throw std::runtime_error("Selector result, row order, or output 
bounds mismatch");
+        }
+    }
+}
+
+template <size_t WORDS>
+void verify(const Data<WORDS>& data) {
+    Scratch<WORDS> scratch(data.batch);
+    for (size_t base = 0; base < data.values.size(); base += data.batch) {
+        const auto rows = std::min(data.batch, data.values.size() - base);
+        verify_selection(data.values.data() + base, rows);
+        if constexpr (WORDS != 0) {
+            std::vector<Payload<WORDS>> expected;
+            for (size_t i = 0; i < rows; ++i) {
+                if (data.values[base + i] < THRESHOLD) {
+                    expected.push_back(data.payload[base + i]);
+                }
+            }
+            for (auto select : SELECTORS) {
+                const auto count =
+                        select(data.values.data() + base, rows, 
scratch.selection.data());
+                for (size_t i = 0; i < count; ++i) {
+                    scratch.copied[i] = data.payload[base + 
scratch.selection[i]];
+                }
+                if (!std::equal(expected.begin(), expected.end(), 
scratch.copied.begin())) {
+                    throw std::runtime_error("Selected payload mismatch");
+                }
+            }
+            evaluate(data.values.data() + base, rows, scratch.flags.data());
+            if (!std::equal(scratch.flags.begin(), scratch.flags.begin() + 
rows,
+                            data.flags.begin() + base)) {
+                throw std::runtime_error("Predicate flags mismatch");
+            }
+            for (auto compact : COMPACTORS) {
+                const auto count = compact(scratch.flags.data(), 
static_cast<uint16_t>(rows),
+                                           scratch.selection.data());
+                for (size_t i = 0; i < count; ++i) {
+                    scratch.copied[i] = data.payload[base + 
scratch.selection[i]];
+                }
+                if (count != expected.size() ||
+                    !std::equal(expected.begin(), expected.end(), 
scratch.copied.begin())) {
+                    throw std::runtime_error("Doris selected payload 
mismatch");
+                }
+            }
+            const auto count =
+                    fused_copy<WORDS>(data.values.data() + base, 
data.payload.data() + base, rows,
+                                      scratch.copied.data());
+            if (count != expected.size() ||
+                !std::equal(expected.begin(), expected.end(), 
scratch.copied.begin())) {
+                throw std::runtime_error("Fused payload mismatch");
+            }
+        }
+    }
+}
+
+void self_test() {
+    verify_selection(nullptr, 0);
+    for (size_t mask = 0; mask < 256; ++mask) {
+        std::array<int32_t, 64> values {};
+        for (size_t i = 0; i < values.size(); ++i) {
+            values[i] = (mask & (1U << (i % 8))) != 0 ? 0 : THRESHOLD;
+        }
+        verify_selection(values.data(), values.size());
+    }
+    for (uint32_t mask = 0; mask < 65536; ++mask) {
+        std::array<uint8_t, 32> flags {};
+        for (unsigned bit = 0; bit < 16; ++bit) {
+            flags[bit] = (mask >> bit) & 1;
+            flags[16 + bit] = ((~mask) >> (15 - bit)) & 1;
+        }
+        verify_compaction(flags.data(), flags.size());
+    }
+    for (const auto rows : {1, 7, 8, 9, 31, 63, 64, 65, 127, 128, 129, 4095, 
4096, 4097, 65535}) {
+        Data<0> data(rows, rows, 50, "random");
+        verify(data);
+    }
+    for (const auto rate : {0, 1, 10, 90, 99, 100}) {
+        verify(Data<0>(65535, 65535, rate, "random"));
+    }
+    for (const auto batch : {7, 64, 65, 256}) {
+        for (const auto rate : {0, 1, 10, 50, 90, 99, 100}) {
+            for (const auto* pattern : {"random", "clustered"}) {
+                verify(Data<1>(1103, batch, rate, pattern));
+                verify(Data<8>(1103, batch, rate, pattern));
+            }
+        }
+    }
+    std::cerr << "Correctness: all 256 table masks, 65536 mixed masks, tails, 
row order, "
+                 "bounds, and payloads passed\n";
+}
+
+template <size_t WORDS>
+void benchmark(const Options& options, int rate, const std::string& pattern) {
+    Data<WORDS> data(options.rows, options.batch, rate, pattern, options.input 
== "values");
+    verify(data);
+    Scratch<WORDS> scratch(options.batch);
+    using Runner = uint64_t (*)(const Data<WORDS>&, Scratch<WORDS>&, size_t);
+    std::vector<Runner> runners;
+    size_t first_algorithm;
+    if (options.suite == "doris") {
+        first_algorithm = static_cast<size_t>(Algorithm::DORIS_CTZ);
+        runners = {run<WORDS, Algorithm::DORIS_CTZ>,      run<WORDS, 
Algorithm::DORIS_LOOKUP>,
+                   run<WORDS, Algorithm::DORIS_HYBRID1>,  run<WORDS, 
Algorithm::DORIS_HYBRID2>,
+                   run<WORDS, Algorithm::DORIS_HYBRID4>,  run<WORDS, 
Algorithm::DORIS_HYBRID8>,
+                   run<WORDS, Algorithm::DORIS_HYBRID16>, run<WORDS, 
Algorithm::DORIS_PRODUCTION>};
+    } else {
+        first_algorithm = 0;
+        runners = {run<WORDS, Algorithm::BRANCHED>, run<WORDS, 
Algorithm::BRANCHLESS>,
+                   run<WORDS, Algorithm::LOOKUP>, run<WORDS, Algorithm::CTZ>};
+        if constexpr (WORDS != 0) {
+            runners.push_back(run<WORDS, Algorithm::FUSED_COPY>);
+        }
+    }
+    std::vector<size_t> passes(runners.size(), 1);
+    std::vector<std::vector<double>> samples(runners.size());
+    const auto target_seconds = options.min_ms / 1000;
+    for (size_t algorithm = 0; algorithm < runners.size(); ++algorithm) {
+        double seconds;
+        do {
+            const auto start = Clock::now();
+            runners[algorithm](data, scratch, passes[algorithm]);
+            seconds = std::chrono::duration<double>(Clock::now() - 
start).count();
+            if (seconds < target_seconds / 4) {
+                passes[algorithm] *= 2;
+            }
+        } while (seconds < target_seconds / 4);
+        passes[algorithm] = std::max<size_t>(
+                1, static_cast<size_t>(std::ceil(passes[algorithm] * 
target_seconds / seconds)));
+    }
+    // Interleave algorithms, changing their order on every repetition.
+    std::vector<size_t> order(runners.size());
+    std::iota(order.begin(), order.end(), 0);
+    std::mt19937 random(0xbec123);
+    for (size_t repetition = 0; repetition < options.repeats; ++repetition) {
+        std::shuffle(order.begin(), order.end(), random);
+        for (auto algorithm : order) {
+            const auto start = Clock::now();
+            runners[algorithm](data, scratch, passes[algorithm]);
+            const auto ns = std::chrono::duration<double, 
std::nano>(Clock::now() - start).count();
+            samples[algorithm].push_back(ns / (passes[algorithm] * 
options.rows));
+        }
+    }
+    for (auto& sample : samples) {
+        std::sort(sample.begin(), sample.end());
+    }
+    const double baseline = samples[0][options.repeats / 2];
+    for (size_t algorithm = 0; algorithm < runners.size(); ++algorithm) {
+        const auto& sample = samples[algorithm];
+        const double median = sample[options.repeats / 2];
+        std::cout << pattern << ',' << options.batch << ',' << options.rows << 
',' << rate << ','
+                  << (WORDS == 0   ? "select"
+                      : WORDS == 1 ? "copy8"
+                                   : "copy64")
+                  << ',' << NAMES[first_algorithm + algorithm] << ',' << 
median << ','
+                  << baseline / median << ',' << sample.front() << ',' << 
sample.back() << ','
+                  << passes[algorithm] << ','
+                  << (options.suite == "scalar" ? "values" : options.input) << 
'\n';
+    }
+    std::cout.flush();
+}
+
+Options parse_options(int argc, char** argv) {
+    Options options;
+    for (int i = 1; i < argc; ++i) {
+        const std::string key = argv[i];
+        if (key == "--help") {
+            std::cout << "Options: --rows N --batch N --repeats N --min-ms M 
--rate 0..100\n"
+                         "         --pattern random|clustered|all --mode 
select|copy8|copy64|all\n"
+                         "         --suite doris|scalar --input flags|values\n"
+                         "         --verify-only\n"
+                         "Defaults: 1048576 rows, 4096 batch, 7 repeats, 20 
ms/sample, all cases.\n"
+                         "Row IDs are uint16; values are int32; copy modes 
include selection.\n"
+                         "Default suite=doris: current SIMD/ctz, lookup, and 
hybrid candidates.\n"
+                         "Default input=flags: predicate evaluation is outside 
timing.\n"
+                         "input=values includes one int32 comparison pass 
before compaction.\n"
+                         "suite=scalar retains the earlier scalar-versus-batch 
experiment.\n"
+                         "clustered places passing values first in every 
batch.\n";
+            std::exit(0);
+        }
+        if (key == "--verify-only") {
+            options.verify_only = true;
+            continue;
+        }
+        if (i + 1 == argc) {
+            throw std::invalid_argument("Missing option value: " + key);
+        }
+        const std::string value = argv[++i];
+        if (key == "--rows") {
+            options.rows = std::stoull(value);
+        } else if (key == "--batch") {
+            options.batch = std::stoull(value);
+        } else if (key == "--repeats") {
+            options.repeats = std::stoull(value);
+        } else if (key == "--min-ms") {
+            options.min_ms = std::stod(value);
+        } else if (key == "--rate") {
+            options.rate = std::stoi(value);
+        } else if (key == "--pattern") {
+            options.pattern = value;
+        } else if (key == "--mode") {
+            options.mode = value;
+        } else if (key == "--suite") {
+            options.suite = value;
+        } else if (key == "--input") {
+            options.input = value;
+        } else {
+            throw std::invalid_argument("Unknown option: " + key);
+        }
+    }
+    if (options.rows == 0 || options.batch == 0 || options.batch > 65535 || 
options.repeats < 3 ||
+        options.repeats % 2 == 0 || !std::isfinite(options.min_ms) || 
options.min_ms <= 0 ||
+        options.rate < -1 || options.rate > 100 ||
+        (options.pattern != "random" && options.pattern != "clustered" &&
+         options.pattern != "all") ||
+        (options.mode != "select" && options.mode != "copy8" && options.mode 
!= "copy64" &&
+         options.mode != "all") ||
+        (options.suite != "doris" && options.suite != "scalar") ||
+        (options.input != "flags" && options.input != "values")) {
+        throw std::invalid_argument("Invalid options; use --help for supported 
values");
+    }
+    return options;
+}
+
+} // namespace selection_compaction_bench
+
+int main(int argc, char** argv) {
+    using namespace selection_compaction_bench;
+    try {
+        const auto options = parse_options(argc, argv);
+        self_test();
+        if (options.verify_only) {
+            return 0;
+        }
+        std::cerr << "Compiler: " << __VERSION__ << "; rows=" << options.rows
+                  << "; batch=" << options.batch << "; repeats=" << 
options.repeats
+                  << "; min_ms=" << options.min_ms << "; suite=" << 
options.suite
+                  << "; input=" << options.input << '\n';
+        std::cout << std::fixed << std::setprecision(6)
+                  << 
"pattern,batch,rows,pass_percent,mode,algorithm,ns_per_input_row,"
+                     
"speedup_vs_baseline,min_ns_per_row,max_ns_per_row,passes,input\n";
+        for (const auto* pattern : {"random", "clustered"}) {
+            if (options.pattern != "all" && options.pattern != pattern) {
+                continue;
+            }
+            const auto rates = options.rate == -1 ? std::vector<int> {0, 1, 
10, 50, 90, 99, 100}
+                                                  : std::vector<int> 
{options.rate};
+            for (auto rate : rates) {
+                if (options.mode == "all" || options.mode == "select") {
+                    benchmark<0>(options, rate, pattern);
+                }
+                if (options.mode == "all" || options.mode == "copy8") {
+                    benchmark<1>(options, rate, pattern);
+                }
+                if (options.mode == "all" || options.mode == "copy64") {
+                    benchmark<8>(options, rate, pattern);
+                }
+            }
+        }
+    } catch (const std::exception& error) {
+        std::cerr << error.what() << '\n';
+        return 1;
+    }
+    return 0;
+}
diff --git a/be/src/storage/segment/segment_iterator.cpp 
b/be/src/storage/segment/segment_iterator.cpp
index 090c3885381..1d627b129f1 100644
--- a/be/src/storage/segment/segment_iterator.cpp
+++ b/be/src/storage/segment/segment_iterator.cpp
@@ -115,6 +115,7 @@
 #include "util/concurrency_stats.h"
 #include "util/defer_op.h"
 #include "util/simd/bits.h"
+#include "util/simd/selection.h"
 
 namespace doris {
 using namespace ErrorCode;
@@ -2816,11 +2817,20 @@ uint16_t 
SegmentIterator::_evaluate_vectorization_predicate(uint16_t* sel_rowid_
                 sel_rowid_idx[new_size++] = sel_pos + i;
             }
         } else {
-            simd::iterate_through_bits_mask(
-                    [&](const int bit_pos) {
-                        sel_rowid_idx[new_size++] = sel_pos + 
(uint16_t)bit_pos;
-                    },
-                    mask);
+#if defined(__x86_64__)
+            // Sparse masks need fewer stores with the original bit iteration.
+            if (__builtin_popcount(mask) > 2) {
+                // Fixed-width stores fit: new_size <= sel_pos and a complete 
SIMD block remains.
+                new_size += simd::append_selected_indices(mask, sel_pos, 
sel_rowid_idx + new_size);
+            } else
+#endif
+            {
+                simd::iterate_through_bits_mask(
+                        [&](const int bit_pos) {
+                            sel_rowid_idx[new_size++] = sel_pos + 
static_cast<uint16_t>(bit_pos);
+                        },
+                        mask);
+            }
         }
         sel_pos += SIMD_BYTES;
     }
diff --git a/be/src/util/simd/selection.h b/be/src/util/simd/selection.h
new file mode 100644
index 00000000000..d316b36827c
--- /dev/null
+++ b/be/src/util/simd/selection.h
@@ -0,0 +1,75 @@
+// 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 <cstdint>
+#include <cstring>
+
+#include "common/compiler_util.h"
+#include "util/simd/bits.h"
+
+namespace doris::simd {
+
+#if defined(__x86_64__)
+namespace detail {
+struct alignas(16) SelectionLookupTable {
+    uint16_t positions[256][8] {};
+    uint8_t counts[256] {};
+
+    constexpr SelectionLookupTable() {
+        for (unsigned mask = 0; mask < 256; ++mask) {
+            for (unsigned bit = 0; bit < 8; ++bit) {
+                if (mask & (1U << bit)) {
+                    positions[mask][counts[mask]++] = 
static_cast<uint16_t>(bit);
+                }
+            }
+        }
+    }
+};
+
+inline constexpr SelectionLookupTable SELECTION_LOOKUP_TABLE;
+} // namespace detail
+#endif
+
+// Append base + bit positions in ascending order. The caller must provide 
room for
+// bits_mask_length() entries, and base + bits_mask_length() - 1 must fit in 
uint16_t.
+// Only the returned number of entries are valid; the lookup path can write 
extra entries.
+ALWAYS_INLINE inline uint16_t append_selected_indices(
+        decltype(bytes_mask_to_bits_mask(nullptr)) mask, uint16_t base, 
uint16_t* output) {
+    uint16_t count = 0;
+#if defined(__x86_64__)
+    for (unsigned offset = 0; offset < 32; offset += 8) {
+        const auto part = static_cast<uint8_t>(mask >> offset);
+        uint16_t indices[8];
+        for (unsigned lane = 0; lane < 8; ++lane) {
+            indices[lane] = static_cast<uint16_t>(
+                    base + offset + 
detail::SELECTION_LOOKUP_TABLE.positions[part][lane]);
+        }
+        // count <= offset, so the fixed-width store stays within 32 output 
entries.
+        std::memcpy(output + count, indices, sizeof(indices));
+        count += detail::SELECTION_LOOKUP_TABLE.counts[part];
+    }
+#else
+    iterate_through_bits_mask(
+            [&](const int bit_pos) { output[count++] = base + 
static_cast<uint16_t>(bit_pos); },
+            mask);
+#endif
+    return count;
+}
+
+} // namespace doris::simd
diff --git a/be/test/util/simd/selection_test.cpp 
b/be/test/util/simd/selection_test.cpp
new file mode 100644
index 00000000000..7ed7d680db1
--- /dev/null
+++ b/be/test/util/simd/selection_test.cpp
@@ -0,0 +1,114 @@
+// 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/selection.h"
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+#include <random>
+#include <vector>
+
+namespace doris::simd {
+namespace {
+constexpr size_t MASK_ROWS = bits_mask_length();
+constexpr uint16_t CANARY = 0xa55a;
+
+void check_selection(const std::array<uint8_t, MASK_ROWS>& flags, uint16_t 
base) {
+    std::array<uint16_t, MASK_ROWS + 2> output;
+    output.fill(CANARY);
+    const auto count =
+            append_selected_indices(bytes_mask_to_bits_mask(flags.data()), 
base, output.data() + 1);
+    std::vector<uint16_t> expected;
+    for (size_t i = 0; i < flags.size(); ++i) {
+        if (flags[i]) {
+            expected.push_back(base + i);
+        }
+    }
+    ASSERT_EQ(count, expected.size());
+    EXPECT_TRUE(std::equal(expected.begin(), expected.end(), output.begin() + 
1));
+    EXPECT_EQ(output.front(), CANARY);
+    EXPECT_EQ(output.back(), CANARY);
+}
+} // namespace
+
+TEST(SelectionTest, EmptyFullAndSingleBit) {
+    std::array<uint8_t, MASK_ROWS> flags {};
+    check_selection(flags, 0);
+    flags.fill(1);
+    check_selection(flags, 0);
+    check_selection(flags, 65536 - MASK_ROWS);
+    for (size_t bit = 0; bit < flags.size(); ++bit) {
+        flags.fill(0);
+        flags[bit] = 1;
+        check_selection(flags, 17);
+        check_selection(flags, 65536 - MASK_ROWS);
+    }
+}
+
+TEST(SelectionTest, ExhaustiveByteMasks) {
+    for (size_t group = 0; group < MASK_ROWS; group += 8) {
+        for (unsigned mask = 0; mask < 256; ++mask) {
+            std::array<uint8_t, MASK_ROWS> flags {};
+            for (unsigned bit = 0; bit < 8; ++bit) {
+                flags[group + bit] = (mask >> bit) & 1;
+            }
+            check_selection(flags, 0);
+            check_selection(flags, 65536 - MASK_ROWS);
+        }
+    }
+}
+
+TEST(SelectionTest, ExhaustiveMixedMasks) {
+    for (unsigned mask = 0; mask < 65536; ++mask) {
+        std::array<uint8_t, MASK_ROWS> flags {};
+        for (size_t bit = 0; bit < MASK_ROWS; ++bit) {
+            const unsigned pattern = bit < 16 ? mask : ~mask;
+            flags[bit] = (pattern >> (bit % 16)) & 1;
+        }
+        check_selection(flags, 19);
+    }
+}
+
+TEST(SelectionTest, ConsecutiveMasks) {
+    constexpr size_t rows = 65536 - MASK_ROWS;
+    std::mt19937 random(20261029);
+    for (const unsigned rate : {0, 1, 5, 10, 50, 90, 99, 100}) {
+        std::vector<uint8_t> flags(rows);
+        std::vector<uint16_t> expected;
+        for (size_t i = 0; i < rows; ++i) {
+            flags[i] = random() % 100 < rate;
+            if (flags[i]) {
+                expected.push_back(i);
+            }
+        }
+        std::vector<uint16_t> output(rows + 2, CANARY);
+        size_t count = 0;
+        for (size_t base = 0; base < rows; base += MASK_ROWS) {
+            count += 
append_selected_indices(bytes_mask_to_bits_mask(flags.data() + base), base,
+                                             output.data() + 1 + count);
+        }
+        ASSERT_EQ(count, expected.size());
+        EXPECT_TRUE(std::equal(expected.begin(), expected.end(), 
output.begin() + 1));
+        EXPECT_EQ(output.front(), CANARY);
+        EXPECT_EQ(output.back(), CANARY);
+    }
+}
+
+} // namespace doris::simd


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

Reply via email to