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 5b25bbc85b1 [improvement](be) Fuse runtime filter mask merging and 
counting with SSE (#69685)
5b25bbc85b1 is described below

commit 5b25bbc85b118ab55a692a9e1d391b35110ad6e8
Author: HappenLee <[email protected]>
AuthorDate: Thu Oct 29 14:27:30 2026 +0800

    [improvement](be) Fuse runtime filter mask merging and counting with SSE 
(#69685)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: N/A
    
    Problem Summary:
    
    `RuntimeFilterExpr::execute_filter` scans the result mask three times
    for every non-constant filter: count live input rows, AND the new filter
    into the mask, then count live output rows.
    
    Fuse these operations in a shared SSE helper used by both ordinary and
    nullable filters. It processes four 16-byte vectors per iteration,
    accumulates zero counts in byte lanes (at most four per lane), and
    reduces them with `_mm_sad_epu8`. The existing `sse2neon` dependency
    maps the same implementation to NEON on AArch64. A scalar tail/fallback
    handles remaining rows.
    
    The helper preserves the existing bitwise filtering and nullable
    semantics, counts nonzero bytes without assuming the inputs are
    normalized to 0/1, and retains the runtime-filter Profile/selectivity
    updates.
    
    #### Local microbenchmark
    
    Intel Xeon Platinum 8457C, Clang 21.1.8, `-O3 -DNDEBUG -msse4.2 -mavx2`,
    pinned to one CPU. Five repetitions with random interleaving; median CPU
    time per batch. The incoming result mask is all ones and the new filter
    has 50% passing rows.
    
    | Rows per batch | Before | SSE fusion | CPU time reduction |
    |---:|---:|---:|---:|
    | 4,096 | 239.57 ns | 188.42 ns | 21.35% |
    | 16,384 | 1,014.96 ns | 759.39 ns | 25.18% |
    
    Each iteration restores fresh masks outside the timed region so repeated
    AND operations do not gradually turn the inputs into zeros. Independent
    blocks use a cache-resident working set; the benchmark measures only
    mask merging and counting, not RF evaluation or SQL latency. AArch64
    cross-compilation confirms NEON code generation; ARM performance has not
    been measured.
    
    ### Release note
    
    Reduce CPU overhead when merging runtime-filter masks and counting live
    rows.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [ ] Unit Test
        - [x] Manual test
    - Added BE unit tests for ordinary/nullable filters, empty batches,
    unaligned buffers, SIMD boundaries/tails, and arbitrary byte values; the
    test file compiles independently.
    - Independent ASAN/UBSan oracle checks passed 11,776 cases for the SSE
    path and 11,776 cases for the forced scalar fallback, comparing both
    counts and the entire output buffer.
    - Ran the SSE microbenchmark matrix for 64/256/1,024/4,093/4,096/16,384
    rows and 0/50/100% filter pass rates.
    - Cross-compiled the isolated helper through the actual `sse2neon.h` for
    AArch64 and inspected NEON code generation. This is not a full ARM BE
    build.
    - Formatting, header/build hygiene, `git diff --check`, and clang-tidy
    on the added helper/test lines passed.
    - Attempted `./run-be-ut.sh -j 48 --run
    --filter='BitsTest.FilterAndCount*'`; existing local
    `ColumnVector`/`PODArray` constructor compilation errors blocked the
    full build, so the standard BE unit tests did not execute. Full-TU
    clang-tidy is also incomplete because of local compilation/environment
    errors.
    - Behavior changed:
        - [x] No.
        - [ ] Yes.
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 be/src/exprs/runtime_filter_expr.cpp | 50 ++++++--------------
 be/src/util/simd/bits.h              | 59 +++++++++++++++++++++++
 be/test/util/simd/bits_test.cpp      | 90 ++++++++++++++++++++++++++++++++++++
 3 files changed, 164 insertions(+), 35 deletions(-)

diff --git a/be/src/exprs/runtime_filter_expr.cpp 
b/be/src/exprs/runtime_filter_expr.cpp
index e491b4329f3..1fa0b1c3708 100644
--- a/be/src/exprs/runtime_filter_expr.cpp
+++ b/be/src/exprs/runtime_filter_expr.cpp
@@ -34,6 +34,7 @@
 #include "exprs/vslot_ref.h"
 #include "runtime/runtime_profile.h"
 #include "storage/index/zone_map/zonemap_eval_context.h"
+#include "util/simd/bits.h"
 
 namespace doris {
 
@@ -168,52 +169,31 @@ Status RuntimeFilterExpr::execute_filter(VExprContext* 
context, const Block* blo
             rf_selectivity.update_judge_selectivity(_filter_id, 0, rows, 
_ignore_thredhold);
             return Status::OK();
         }
-    } else if (const auto* nullable_column = 
check_and_get_column<ColumnNullable>(*filter_column)) {
+    }
+
+    simd::FilterCounts counts;
+    if (const auto* nullable_column = 
check_and_get_column<ColumnNullable>(*filter_column)) {
         // nullable(bool)
         const ColumnPtr& nested_column = 
nullable_column->get_nested_column_ptr();
         const IColumn::Filter& filter = assert_cast<const 
ColumnUInt8&>(*nested_column).get_data();
         const auto* __restrict filter_data = filter.data();
         const auto* __restrict null_map_data = 
nullable_column->get_null_map_data().data();
 
-        const size_t input_rows = rows - 
simd::count_zero_num((int8_t*)result_filter_data, rows);
-
-        for (size_t i = 0; i < rows; ++i) {
-            result_filter_data[i] &= (!null_map_data[i]) & filter_data[i];
-        }
-
-        const size_t output_rows = rows - 
simd::count_zero_num((int8_t*)result_filter_data, rows);
-
-        COUNTER_UPDATE(_rf_filter_rows, input_rows - output_rows);
-        COUNTER_UPDATE(_rf_input_rows, input_rows);
-        rf_selectivity.update_judge_selectivity(_filter_id, input_rows - 
output_rows, input_rows,
-                                                _ignore_thredhold);
-
-        if (output_rows == 0) {
-            *can_filter_all = true;
-            return Status::OK();
-        }
+        counts = simd::filter_and_count<true>(result_filter_data, filter_data, 
rows, null_map_data);
     } else {
         // bool
         const IColumn::Filter& filter = assert_cast<const 
ColumnUInt8&>(*filter_column).get_data();
         const auto* __restrict filter_data = filter.data();
 
-        const size_t input_rows = rows - 
simd::count_zero_num((int8_t*)result_filter_data, rows);
-
-        for (size_t i = 0; i < rows; ++i) {
-            result_filter_data[i] &= filter_data[i];
-        }
-
-        const size_t output_rows = rows - 
simd::count_zero_num((int8_t*)result_filter_data, rows);
-
-        COUNTER_UPDATE(_rf_filter_rows, input_rows - output_rows);
-        COUNTER_UPDATE(_rf_input_rows, input_rows);
-        rf_selectivity.update_judge_selectivity(_filter_id, input_rows - 
output_rows, input_rows,
-                                                _ignore_thredhold);
-
-        if (output_rows == 0) {
-            *can_filter_all = true;
-            return Status::OK();
-        }
+        counts = simd::filter_and_count(result_filter_data, filter_data, rows);
+    }
+    const auto [input_rows, output_rows] = counts;
+    COUNTER_UPDATE(_rf_filter_rows, input_rows - output_rows);
+    COUNTER_UPDATE(_rf_input_rows, input_rows);
+    rf_selectivity.update_judge_selectivity(_filter_id, input_rows - 
output_rows, input_rows,
+                                            _ignore_thredhold);
+    if (output_rows == 0) {
+        *can_filter_all = true;
     }
     return Status::OK();
 }
diff --git a/be/src/util/simd/bits.h b/be/src/util/simd/bits.h
index 41b999b0be1..b0bc7c1f654 100644
--- a/be/src/util/simd/bits.h
+++ b/be/src/util/simd/bits.h
@@ -124,6 +124,65 @@ void iterate_through_bits_mask(Func func, 
decltype(bytes_mask_to_bits_mask(nullp
 #endif
 }
 
+struct FilterCounts {
+    size_t input_rows = 0;
+    size_t output_rows = 0;
+};
+
+// Merge a filter in-place and count nonzero bytes before and after the merge.
+// Nullable filters preserve the scalar expression data[i] &= !null_map[i] & 
filter[i].
+template <bool nullable = false>
+inline FilterCounts filter_and_count(uint8_t* __restrict data, const uint8_t* 
__restrict filter,
+                                     size_t size, const uint8_t* __restrict 
null_map = nullptr) {
+    FilterCounts counts;
+    size_t i = 0;
+#if defined(__SSE2__) || defined(__aarch64__)
+    const __m128i zero = _mm_setzero_si128();
+    __m128i input_sum = zero;
+    __m128i output_sum = zero;
+    const size_t vector_size = size / 64 * 64;
+    for (; i < vector_size; i += 64) {
+        __m128i input_zeros = zero;
+        __m128i output_zeros = zero;
+        for (size_t offset = 0; offset < 64; offset += 16) {
+            const auto before =
+                    _mm_loadu_si128(reinterpret_cast<const __m128i*>(data + i 
+ offset));
+            const auto filter_values =
+                    _mm_loadu_si128(reinterpret_cast<const __m128i*>(filter + 
i + offset));
+            auto after = _mm_and_si128(before, filter_values);
+            if constexpr (nullable) {
+                const auto nulls =
+                        _mm_loadu_si128(reinterpret_cast<const 
__m128i*>(null_map + i + offset));
+                const auto valid = _mm_and_si128(_mm_cmpeq_epi8(nulls, zero), 
_mm_set1_epi8(1));
+                after = _mm_and_si128(after, valid);
+            }
+            // Each byte accumulates at most four zeros. Reduce once per 64 
rows
+            // without expanding bytes into size_t lanes or emulating movemask 
on ARM.
+            input_zeros = _mm_sub_epi8(input_zeros, _mm_cmpeq_epi8(before, 
zero));
+            output_zeros = _mm_sub_epi8(output_zeros, _mm_cmpeq_epi8(after, 
zero));
+            _mm_storeu_si128(reinterpret_cast<__m128i*>(data + i + offset), 
after);
+        }
+        input_sum = _mm_add_epi64(input_sum, _mm_sad_epu8(input_zeros, zero));
+        output_sum = _mm_add_epi64(output_sum, _mm_sad_epu8(output_zeros, 
zero));
+    }
+    counts.input_rows = vector_size - 
static_cast<size_t>(_mm_cvtsi128_si64(_mm_add_epi64(
+                                              input_sum, 
_mm_srli_si128(input_sum, 8))));
+    counts.output_rows = vector_size - 
static_cast<size_t>(_mm_cvtsi128_si64(_mm_add_epi64(
+                                               output_sum, 
_mm_srli_si128(output_sum, 8))));
+#endif
+    for (; i < size; ++i) {
+        const uint8_t before = data[i];
+        uint8_t after = before & filter[i];
+        if constexpr (nullable) {
+            after &= !null_map[i];
+        }
+        counts.input_rows += before != 0;
+        counts.output_rows += after != 0;
+        data[i] = after;
+    }
+    return counts;
+}
+
 template <typename T>
     requires requires { std::is_unsigned_v<T>; }
 inline T count_zero_num(const int8_t* __restrict data, T size) {
diff --git a/be/test/util/simd/bits_test.cpp b/be/test/util/simd/bits_test.cpp
index abb8e4e340b..4034980baa7 100644
--- a/be/test/util/simd/bits_test.cpp
+++ b/be/test/util/simd/bits_test.cpp
@@ -24,10 +24,100 @@
 #include <unistd.h>
 
 #include <algorithm>
+#include <random>
 
 #include "gtest/gtest_pred_impl.h"
 
 namespace doris::simd {
+namespace {
+void fill_filter_inputs(std::vector<uint8_t>& source, std::vector<uint8_t>& 
filter,
+                        std::vector<uint8_t>& null_map, size_t size, size_t 
offset, int pattern,
+                        std::mt19937_64& random) {
+    for (size_t i = offset; i < offset + size; ++i) {
+        switch (pattern) {
+        case 0:
+            source[i] = 0;
+            filter[i] = 1;
+            break;
+        case 1:
+            source[i] = 1;
+            filter[i] = 0;
+            break;
+        case 2:
+            source[i] = filter[i] = 1;
+            break;
+        case 3:
+            source[i] = filter[i] = 0xff;
+            break;
+        case 4:
+            source[i] = 0x80;
+            filter[i] = 1;
+            break;
+        case 5:
+            source[i] = i % 2;
+            filter[i] = (i + 1) % 2;
+            break;
+        case 6:
+            source[i] = random() % 2;
+            filter[i] = random() % 2;
+            break;
+        default:
+            source[i] = static_cast<uint8_t>(random());
+            filter[i] = static_cast<uint8_t>(random());
+            break;
+        }
+        null_map[i] = pattern < 3 ? 0 : static_cast<uint8_t>(random() % 3);
+    }
+}
+
+template <bool nullable>
+void check_filter_counts(const std::vector<uint8_t>& source, const 
std::vector<uint8_t>& filter,
+                         const std::vector<uint8_t>& null_map, size_t size, 
size_t offset) {
+    auto expected = source;
+    FilterCounts expected_counts;
+    for (size_t i = offset; i < offset + size; ++i) {
+        expected_counts.input_rows += expected[i] != 0;
+        expected[i] &= filter[i];
+        if constexpr (nullable) {
+            expected[i] &= !null_map[i];
+        }
+        expected_counts.output_rows += expected[i] != 0;
+    }
+    auto actual = source;
+    const auto counts = filter_and_count<nullable>(actual.data() + offset, 
filter.data() + offset,
+                                                   size, null_map.data() + 
offset);
+    EXPECT_EQ(counts.input_rows, expected_counts.input_rows);
+    EXPECT_EQ(counts.output_rows, expected_counts.output_rows);
+    EXPECT_EQ(actual, expected);
+}
+} // namespace
+
+TEST(BitsTest, FilterAndCount) {
+    std::mt19937_64 random(20261027);
+    for (const size_t size : {0,  1,   7,   15,  16,  17,  31,  32,   33,   
63,   64,
+                              65, 127, 128, 129, 255, 256, 257, 1023, 1024, 
4093, 4096}) {
+        for (size_t offset = 0; offset < 16; ++offset) {
+            for (int pattern = 0; pattern < 8; ++pattern) {
+                std::vector<uint8_t> source(size + 32, 0xa5);
+                std::vector<uint8_t> filter(size + 32);
+                std::vector<uint8_t> null_map(size + 32);
+                fill_filter_inputs(source, filter, null_map, size, offset, 
pattern, random);
+                check_filter_counts<false>(source, filter, null_map, size, 
offset);
+                check_filter_counts<true>(source, filter, null_map, size, 
offset);
+            }
+        }
+    }
+}
+
+TEST(BitsTest, FilterAndCountEmpty) {
+    const auto counts = filter_and_count(nullptr, nullptr, 0);
+    EXPECT_EQ(counts.input_rows, 0U);
+    EXPECT_EQ(counts.output_rows, 0U);
+    const auto nullable_counts = filter_and_count<true>(nullptr, nullptr, 0, 
nullptr);
+    EXPECT_EQ(nullable_counts.input_rows, 0U);
+    EXPECT_EQ(nullable_counts.output_rows, 0U);
+}
+
 TEST(BitsTest, ContainsBytesGuardPages) {
     const size_t page_size = sysconf(_SC_PAGESIZE);
     void* mapping = mmap(nullptr, page_size * 3, PROT_NONE, MAP_PRIVATE | 
MAP_ANONYMOUS, -1, 0);


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

Reply via email to