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 45d1598da2f [improvement](be) Accelerate phmap control scans with ARM 
NEON (#69688)
45d1598da2f is described below

commit 45d1598da2f85d90ded9c8bcd19982f5b2d93e17
Author: HappenLee <[email protected]>
AuthorDate: Fri Oct 30 16:56:52 2026 +0800

    [improvement](be) Accelerate phmap control scans with ARM NEON (#69688)
    
    AArch64 currently uses portable scalar phmap control-byte scans. The
    subtraction-based H2 mask can include adjacent false-positive
    candidates: control bytes `0x12, 0x13` can both be returned when looking
    for `0x12`. Key comparison preserves correctness, but the extra
    candidates cost work.
    
    Add exact NEON control comparisons on little-endian AArch64 while
    keeping eight-byte groups, the portable fallback and the existing ARM
    dump/load layout. x86 keeps its existing SSE implementation.
    `PHMAP_HAVE_ARM_NEON=0` disables the new implementation at compile time.
    
    The normal third-party downloader applies this patch alongside the
    existing AArch64 int128 hash-mixer patch, with separate markers and a
    subshell that restores the caller's working directory. The NEON patch
    now includes context so a replay cannot silently insert the same code
    again. The downloader checks the actual source with a direction-locked
    reverse dry-run, recovers a missing marker when all hunks are already
    applied, and preflights every hunk before a fresh application. Partial
    or duplicated sources fail without further edits and do not retain a
    success marker. The standalone tests cover control masks, unaligned
    reads, special slots, conversion bounds, map operations, rehash and
    dump/load. The standalone C++ benchmark can be compiled directly. For an
    incremental comparison, retain the existing int128 patch in both the
    baseline and optimized dependency sources, adding NEON only to the
    optimized copy.
    
    ### Supplied ARM measurements
    
    The following measurements come from the supplied report on original
    Doris baseline `70f7dcbe11c8e142b81536c1dfaaf505ec05437b`, not a new ARM
    run of this rebased PR. That baseline predates master's AArch64 int128
    patch. These numbers therefore do **not** establish the incremental
    gains of the final combined patch stack; a new ARM run with the combined
    patch stack is still needed.
    
    Environment: Neoverse-N2, 16 cores, nominal 3 GHz; Clang 20.1.8; C++17,
    `-O3 -DNDEBUG -march=armv8-a+crc -pthread`; one process pinned to CPU
    15. Nine alternating baseline/NEON rounds, each measurement at least 0.2
    seconds of thread CPU time. Values are median thread CPU ns/op. Reported
    intervals use 10,000 paired bootstrap resamples.
    
    Positive time reduction means faster; negative means slower.
    
    | Scenario | Baseline ns/op | NEON ns/op | Time reduction | Reported 95%
    interval |
    |---|---:|---:|---:|---:|
    | Hit lookup, 4,096 keys | 4.0773 | 3.8350 | +5.94% | [+5.84%, +6.11%] |
    | Hit lookup, 100,000 keys | 10.6120 | 9.6412 | +9.15% | [+8.99%,
    +12.44%] |
    | Hit lookup, 114,000 keys | 11.4606 | 10.5508 | +7.94% | [+7.73%,
    +11.42%] |
    | Hit lookup, 1,000,000 keys | 16.3554 | 14.4228 | +11.82% | [+10.20%,
    +12.74%] |
    | Miss lookup, 4,096 keys | 3.4818 | 3.3069 | +5.02% | [+4.81%, +5.08%]
    |
    | Miss lookup, 100,000 keys | 9.7027 | 9.9144 | -2.18% | [-2.91%,
    -1.27%] |
    | Miss lookup, 114,000 keys | 16.8860 | 17.5147 | -3.72% | [-3.93%,
    -2.89%] |
    | Miss lookup, 1,000,000 keys | 8.6927 | 8.1035 | +6.78% | [+4.93%,
    +9.01%] |
    | Insert, 100,000 keys, including allocation/destruction | 9.8116 |
    9.2758 | +5.46% | [+3.94%, +6.25%] |
    | Erase/reinsert, 20,000 operations per iteration | 16.3032 | 16.3810 |
    -0.48% | [-1.19%, +0.97%] |
    | Synthetic adjacent-H2 scan, 32,768 groups | 2.7578 | 1.7641 | +36.03%
    | [+35.75%, +36.09%] |
    
    Load factors differ: approximately 50.0%, 76.3%, 87.0% and 47.7% at the
    four lookup sizes. Two miss-lookup workloads regress reproducibly. The
    erase/reinsert interval crosses zero. The adjacent-H2 case measures
    group scans and candidate-key comparisons, not complete table lookups.
    These are library microbenchmarks, not end-to-end SQL gains.
    
    ### Validation
    
    **Reported in the supplied ARM bundle:** randomized scalar-oracle
    checks, baseline/NEON/forced-portable paths, bidirectional dump/load and
    ASAN/UBSAN passed. The archive contains the patch and report; these ARM
    executions were not repeated during this submission.
    
    **Executed locally after rebasing onto master
    `3d2d9aac54148056529c13ef4b9fbdbcb65fab77` (x86_64, Clang 16.0.6):**
    
    - Standalone correctness tests passed for baseline, patched and
    explicitly NEON-disabled builds; x86 selected the existing 16-byte SSE
    groups in all three. Tests also exercise the portable group
    implementation directly.
    - Baseline-to-patched and patched-to-baseline dump/load passed; patched
    ASAN/UBSAN passed.
    - Benchmark source compiled with `-Wall -Wextra -Werror`; all 11
    scenarios passed a short checksum smoke run. No x86 performance
    conclusion is drawn from it.
    - Standard third-party download/patch script passed twice with `OLDPWD`
    unset. The standard downloader produces byte-identical headers to those
    used by the standalone tests.
    - clang-format 16, repository clang-tidy entry point for both standalone
    C++ files, shell syntax and `git diff --check` passed.
    
    **Downloader recovery follow-up:** `bash
    thirdparty/test/parallel-hashmap-patch-test.sh` passed using the pinned
    cached archive and the real downloader; no dependency downloads were
    needed. It covers fresh extraction, normal reruns, an applied patch with
    a missing marker, a stale marker over pristine sources, partially
    applied sources with/without a marker, and duplicate NEON definitions.
    Successful reruns compare both complete headers; failing cases verify
    unchanged source and absence of the success marker. The same test
    detects the original replay bug. The regenerated patch inserts exactly
    the same 67 C++ lines, and its applied header bytes match the original
    optimized source. Bash syntax and shell whitespace checks passed;
    generated patch context preserves the pinned dependency's existing
    whitespace.
    
    Full Doris BE compilation, SQL regression/performance tests and new ARM
    measurements against the combined int128+NEON patch stack have not been
    run.
    
    The C++ test and benchmark have no Python dependency and can be compiled
    directly:
    
    ```bash
    bash thirdparty/download-thirdparty.sh parallel_hashmap
    clang++ -std=c++17 -O2 -pthread -Wall -Wextra -Werror \
      -isystem thirdparty/src/parallel-hashmap-1.3.8 \
      thirdparty/tests/parallel_hashmap_neon_test.cpp -o /tmp/phmap-neon-test
    /tmp/phmap-neon-test
    clang++ -std=c++17 -O3 -DNDEBUG -pthread -Wall -Wextra -Werror \
      -isystem thirdparty/src/parallel-hashmap-1.3.8 \
      thirdparty/benchmarks/parallel_hashmap_bench.cpp -o /tmp/phmap-neon-bench
    taskset -c 15 /tmp/phmap-neon-bench 0.2
    ```
    
    Select an available isolated CPU instead of CPU 15 when necessary. For
    the reported ARM compiler settings, add `-march=armv8-a+crc`. The
    benchmark emits one JSON line per scenario with CPU/wall timings and
    checksums. To compare against baseline on current master, extract the
    verified source archive into another directory, apply only
    `parallel-hashmap-1.3.8-aarch64-int128.patch`, compile the same C++
    benchmark against that directory with identical flags, and alternate
    baseline/optimized execution order.
    
    ### Release note
    
    Accelerate phmap control-byte scanning on little-endian AArch64 while
    preserving eight-byte ARM groups and the portable fallback.
---
 thirdparty/benchmarks/parallel_hashmap_bench.cpp   | 200 +++++++++++++++
 thirdparty/download-thirdparty.sh                  |  33 ++-
 .../patches/parallel-hashmap-1.3.8-arm-neon.patch  | 104 ++++++++
 thirdparty/test/parallel-hashmap-patch-test.sh     | 143 +++++++++++
 thirdparty/tests/parallel_hashmap_neon_test.cpp    | 272 +++++++++++++++++++++
 5 files changed, 743 insertions(+), 9 deletions(-)

diff --git a/thirdparty/benchmarks/parallel_hashmap_bench.cpp 
b/thirdparty/benchmarks/parallel_hashmap_bench.cpp
new file mode 100644
index 00000000000..fe10d044740
--- /dev/null
+++ b/thirdparty/benchmarks/parallel_hashmap_bench.cpp
@@ -0,0 +1,200 @@
+// 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 <parallel_hashmap/phmap.h>
+
+#include <algorithm>
+#include <array>
+#include <chrono>
+#include <cstdint>
+#include <ctime>
+#include <iomanip>
+#include <iostream>
+#include <random>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+namespace {
+
+using Map = phmap::flat_hash_map<uint64_t, uint64_t>;
+using Group = phmap::priv::Group;
+
+uint64_t mix(uint64_t value) {
+    value += 0x9e3779b97f4a7c15ULL;
+    value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ULL;
+    value = (value ^ (value >> 27)) * 0x94d049bb133111ebULL;
+    return value ^ (value >> 31);
+}
+
+double cpu_seconds() {
+    timespec value {};
+    if (::clock_gettime(CLOCK_THREAD_CPUTIME_ID, &value) != 0) {
+        throw std::runtime_error("clock_gettime failed");
+    }
+    return value.tv_sec + value.tv_nsec * 1e-9;
+}
+
+template <typename Work>
+void measure(const std::string& name, uint64_t operations, size_t size, size_t 
capacity,
+             double seconds, Work work) {
+    uint64_t expected = work();
+    uint64_t checksum = 0;
+    uint64_t iterations = 1;
+    double cpu_elapsed = 0;
+    double wall_elapsed = 0;
+    for (;;) {
+        checksum = 0;
+        auto wall_start = std::chrono::steady_clock::now();
+        double cpu_start = cpu_seconds();
+        for (uint64_t iteration = 0; iteration < iterations; ++iteration) {
+            // Force each iteration to read the table again, including under 
LTO.
+            asm volatile("" ::: "memory");
+            checksum += work();
+            asm volatile("" : "+r"(checksum) : : "memory");
+        }
+        cpu_elapsed = cpu_seconds() - cpu_start;
+        wall_elapsed = 
std::chrono::duration<double>(std::chrono::steady_clock::now() - wall_start)
+                               .count();
+        if (cpu_elapsed >= seconds) {
+            break;
+        }
+        iterations *= 2;
+    }
+    if (checksum != expected * iterations) {
+        throw std::runtime_error("benchmark result changed between 
iterations");
+    }
+    auto count = static_cast<double>(operations) * iterations;
+    std::cout << std::setprecision(12) << R"({"name":")" << name << 
R"(","size":)" << size
+              << R"(,"capacity":)" << capacity << R"(,"group_width":)" << 
Group::kWidth
+              << R"(,"portable":)"
+              << std::is_same_v<Group, phmap::priv::GroupPortableImpl> << 
R"(,"iterations":)"
+              << iterations << R"(,"operations":)" << operations << 
R"(,"cpu_ns_per_op":)"
+              << cpu_elapsed * 1e9 / count << R"(,"wall_ns_per_op":)" << 
wall_elapsed * 1e9 / count
+              << R"(,"checksum_per_iteration":)" << expected << 
R"(,"checksum":)" << checksum << "}"
+              << std::endl;
+}
+
+std::vector<uint64_t> make_keys(size_t size, size_t start = 0) {
+    std::vector<uint64_t> keys;
+    keys.reserve(size);
+    for (size_t i = 0; i < size; ++i) {
+        keys.push_back(mix(i + start));
+    }
+    std::mt19937_64 random(81384);
+    std::shuffle(keys.begin(), keys.end(), random);
+    return keys;
+}
+
+void lookup_benchmarks(size_t size, double seconds) {
+    auto keys = make_keys(size);
+    auto misses = make_keys(size, size);
+    Map map;
+    map.reserve(size);
+    for (auto key : keys) {
+        map.emplace(key, key + 1);
+    }
+    measure("find_hit/" + std::to_string(size), size, size, map.capacity(), 
seconds, [&] {
+        uint64_t checksum = 0;
+        for (auto key : keys) {
+            auto found = map.find(key);
+            checksum += found->second;
+        }
+        return checksum;
+    });
+    measure("find_miss/" + std::to_string(size), size, size, map.capacity(), 
seconds, [&] {
+        uint64_t checksum = 0;
+        for (auto key : misses) {
+            checksum += map.find(key) != map.end();
+        }
+        return checksum;
+    });
+}
+
+void mutation_benchmarks(double seconds) {
+    constexpr size_t size = 100000;
+    auto keys = make_keys(size);
+    measure("insert_reserved/100000", size, size, 0, seconds, [&] {
+        Map map;
+        map.reserve(size);
+        for (auto key : keys) {
+            map.emplace(key, key + 1);
+        }
+        asm volatile("" : : "g"(&map) : "memory");
+        return map.size();
+    });
+    Map map;
+    map.reserve(size);
+    for (auto key : keys) {
+        map.emplace(key, key + 1);
+    }
+    constexpr size_t changes = 10000;
+    measure("erase_insert/10000", changes * 2, size, map.capacity(), seconds, 
[&] {
+        for (size_t i = 0; i < changes; ++i) {
+            map.erase(keys[i]);
+        }
+        for (size_t i = 0; i < changes; ++i) {
+            map.emplace(keys[i], keys[i] + 1);
+        }
+        return map.size();
+    });
+}
+
+void adjacent_h2_benchmark(double seconds) {
+    struct Probe {
+        std::array<phmap::priv::ctrl_t, Group::kWidth> ctrl;
+        std::array<uint64_t, Group::kWidth> keys;
+        uint64_t needle;
+    };
+    std::vector<Probe> probes(32768);
+    for (size_t i = 0; i < probes.size(); ++i) {
+        auto& probe = probes[i];
+        probe.ctrl.fill(phmap::priv::kEmpty);
+        probe.ctrl[2] = 0x12;
+        probe.ctrl[3] = 0x13;
+        for (size_t j = 0; j < probe.keys.size(); ++j) {
+            probe.keys[j] = mix(i * Group::kWidth + j);
+        }
+        probe.needle = probe.keys[2];
+    }
+    measure("group_adjacent_h2/32768", probes.size(), probes.size(), 0, 
seconds, [&] {
+        uint64_t checksum = 0;
+        for (const auto& probe : probes) {
+            for (auto position : Group(probe.ctrl.data()).Match(0x12)) {
+                checksum += probe.keys[position] == probe.needle;
+            }
+        }
+        return checksum;
+    });
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+    double seconds = argc == 2 ? std::stod(argv[1]) : 0.2;
+    if (seconds <= 0 || seconds > 10) {
+        std::cerr << "minimum CPU seconds must be in (0, 10]\n";
+        return 1;
+    }
+    for (size_t size : {size_t {4096}, size_t {100000}, size_t {114000}, 
size_t {1000000}}) {
+        lookup_benchmarks(size, seconds);
+    }
+    mutation_benchmarks(seconds);
+    adjacent_h2_benchmark(seconds);
+    return 0;
+}
diff --git a/thirdparty/download-thirdparty.sh 
b/thirdparty/download-thirdparty.sh
index 5af6d4a96fa..c3183010b12 100755
--- a/thirdparty/download-thirdparty.sh
+++ b/thirdparty/download-thirdparty.sh
@@ -800,16 +800,31 @@ if [[ " ${TP_ARCHIVES[*]} " =~ " BOOST " ]]; then
     echo "Finished patching ${BOOST_SOURCE}"
 fi
 
-# Clang on AArch64 supports __int128. Enable phmap's multiply-based hash mixer
-# instead of falling back to the longer shift/xor implementation.
+# Clang on AArch64 supports __int128. Keep the existing hash mixer patch and
+# accelerate ARM control scans without changing the eight-byte group layout.
 if [[ " ${TP_ARCHIVES[*]} " =~ " PARALLEL_HASHMAP " ]]; then
-    cd "${TP_SOURCE_DIR}/${PARALLEL_HASHMAP_SOURCE}"
-    if [[ ! -f "${PATCHED_MARK}_aarch64_int128" ]]; then
-        patch -p1 
<"${TP_PATCH_DIR}/parallel-hashmap-1.3.8-aarch64-int128.patch"
-        touch "${PATCHED_MARK}_aarch64_int128"
-    fi
-    cd -
-    echo "Finished patching ${PARALLEL_HASHMAP_SOURCE} for AArch64 int128"
+    (
+        cd "${TP_SOURCE_DIR}/${PARALLEL_HASHMAP_SOURCE}"
+        if [[ ! -f "${PATCHED_MARK}_aarch64_int128" ]]; then
+            patch -p1 
<"${TP_PATCH_DIR}/parallel-hashmap-1.3.8-aarch64-int128.patch"
+            touch "${PATCHED_MARK}_aarch64_int128"
+        fi
+        neon_patch="${TP_PATCH_DIR}/parallel-hashmap-1.3.8-arm-neon.patch"
+        # A run can stop after patching but before writing the marker. Check 
the
+        # source even when the marker exists: the old insertion-only patch 
could
+        # be applied twice and still leave a marker claiming success.
+        # --forward prevents either dry-run probe from automatically changing 
direction.
+        if ! patch --dry-run --batch --forward --reverse --fuzz=0 -p1 \
+            <"${neon_patch}" >/dev/null 2>&1; then
+            rm -f "${PATCHED_MARK}_arm_neon"
+            # Check every hunk before modifying any file; partial or 
incompatible
+            # sources must fail without accepting or further applying the 
patch.
+            patch --dry-run --batch --forward --fuzz=0 -p1 <"${neon_patch}"
+            patch --batch --forward --fuzz=0 -p1 <"${neon_patch}"
+        fi
+        touch "${PATCHED_MARK}_arm_neon"
+    )
+    echo "Finished patching ${PARALLEL_HASHMAP_SOURCE} (AArch64 int128 and ARM 
NEON)"
 fi
 
 # vim: ts=4 sw=4 ts=4 tw=100:
diff --git a/thirdparty/patches/parallel-hashmap-1.3.8-arm-neon.patch 
b/thirdparty/patches/parallel-hashmap-1.3.8-arm-neon.patch
new file mode 100644
index 00000000000..53eac662c72
--- /dev/null
+++ b/thirdparty/patches/parallel-hashmap-1.3.8-arm-neon.patch
@@ -0,0 +1,104 @@
+--- a/parallel_hashmap/phmap.h
++++ b/parallel_hashmap/phmap.h
+@@ -462,10 +462,59 @@
+      #pragma warning(pop)  
+ #endif
+ 
+ #endif  // PHMAP_HAVE_SSE2
+ 
++#if PHMAP_HAVE_ARM_NEON && defined(PHMAP_IS_LITTLE_ENDIAN)
++
++struct GroupAArch64Impl {
++    enum { kWidth = 8 };
++
++    explicit GroupAArch64Impl(const ctrl_t* pos) { ctrl = 
vld1_u8(reinterpret_cast<const uint8_t*>(pos)); }
++
++    BitMask<uint64_t, kWidth, 3> Match(h2_t hash) const {
++        uint8x8_t dup = vdup_n_u8(hash);
++        auto mask = vceq_u8(ctrl, dup);
++        constexpr uint64_t msbs = 0x8080808080808080ULL;
++        return BitMask<uint64_t, kWidth, 
3>(vget_lane_u64(vreinterpret_u64_u8(mask), 0) & msbs);
++    }
++
++    BitMask<uint64_t, kWidth, 3> MatchEmpty() const {
++        auto mask = vceq_s8(vdup_n_s8(static_cast<ctrl_t>(kEmpty)), 
vreinterpret_s8_u8(ctrl));
++        constexpr uint64_t msbs = 0x8080808080808080ULL;
++        return BitMask<uint64_t, kWidth, 
3>(vget_lane_u64(vreinterpret_u64_u8(mask), 0) & msbs);
++    }
++
++    BitMask<uint64_t, kWidth, 3> MatchEmptyOrDeleted() const {
++        auto mask = vcgt_s8(vdup_n_s8(static_cast<ctrl_t>(kSentinel)), 
vreinterpret_s8_u8(ctrl));
++        constexpr uint64_t msbs = 0x8080808080808080ULL;
++        return BitMask<uint64_t, kWidth, 
3>(vget_lane_u64(vreinterpret_u64_u8(mask), 0) & msbs);
++    }
++
++    uint32_t CountLeadingEmptyOrDeleted() const {
++        uint64_t mask = vget_lane_u64(vreinterpret_u64_u8(ctrl), 0);
++        // ctrl | ~(ctrl >> 7) has the lowest bit of each byte set to 0 for 
kEmpty (-128)
++        // and kDeleted (-2), and 1 for kSentinel (-1) and full slots 
(0..127).
++        constexpr uint64_t bits = 0x0101010101010101ULL;
++        uint64_t x = (mask | ~(mask >> 7)) & bits;
++        return static_cast<uint32_t>((x == 0 ? 64 : TrailingZeros(x)) >> 3);
++    }
++
++    void ConvertSpecialToEmptyAndFullToDeleted(ctrl_t* dst) const {
++        uint64_t mask = vget_lane_u64(vreinterpret_u64_u8(ctrl), 0);
++        constexpr uint64_t msbs = 0x8080808080808080ULL;
++        constexpr uint64_t lsbs = 0x0101010101010101ULL;
++        auto x = mask & msbs;
++        auto res = (~x + (x >> 7)) & ~lsbs;
++        little_endian::Store64(dst, res);
++    }
++
++    uint8x8_t ctrl;
++};
++
++#endif  // PHMAP_HAVE_ARM_NEON && defined(PHMAP_IS_LITTLE_ENDIAN)
++
+ // --------------------------------------------------------------------------
+ // --------------------------------------------------------------------------
+ struct GroupPortableImpl 
+ {
+     enum { kWidth = 8 };
+@@ -519,10 +568,15 @@
+     uint64_t ctrl;
+ };
+ 
+ #if PHMAP_HAVE_SSE2  
+     using Group = GroupSse2Impl;
++#elif PHMAP_HAVE_ARM_NEON && defined(PHMAP_IS_LITTLE_ENDIAN)
++    using Group = GroupAArch64Impl;
++    static_assert(static_cast<size_t>(Group::kWidth) ==
++                  static_cast<size_t>(GroupPortableImpl::kWidth),
++                  "ARM NEON must preserve the portable phmap dump/load 
layout");
+ #else
+     using Group = GroupPortableImpl;
+ #endif
+ 
+ // The number of cloned control bytes that we copy from the beginning to the
+--- a/parallel_hashmap/phmap_config.h
++++ b/parallel_hashmap/phmap_config.h
+@@ -660,10 +660,23 @@
+ 
+ #if PHMAP_HAVE_SSE2
+     #include <emmintrin.h>
+ #endif
+ 
++// ARM NEON comparisons use the same eight control bytes as the portable 
group.
++#ifndef PHMAP_HAVE_ARM_NEON
++    #if defined(__ARM_NEON) && defined(__aarch64__) && 
defined(PHMAP_IS_LITTLE_ENDIAN)
++        #define PHMAP_HAVE_ARM_NEON 1
++    #else
++        #define PHMAP_HAVE_ARM_NEON 0
++    #endif
++#endif
++
++#if PHMAP_HAVE_ARM_NEON
++    #include <arm_neon.h>
++#endif
++
+ #if PHMAP_HAVE_SSSE3
+     #include <tmmintrin.h>
+ #endif
+ 
+ 
diff --git a/thirdparty/test/parallel-hashmap-patch-test.sh 
b/thirdparty/test/parallel-hashmap-patch-test.sh
new file mode 100755
index 00000000000..893a586cd09
--- /dev/null
+++ b/thirdparty/test/parallel-hashmap-patch-test.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+# 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.
+
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)"
+ARCHIVE_DIR="${1:-${ROOT}/src}"
+ARCHIVE_DIR="$(cd "${ARCHIVE_DIR}" && pwd)"
+TP_DIR="${ROOT}"
+source "${ROOT}/vars.sh"
+[[ -f "${ARCHIVE_DIR}/${PARALLEL_HASHMAP_NAME}" ]] || {
+    echo "FAIL: provide the directory containing ${PARALLEL_HASHMAP_NAME}" >&2
+    exit 1
+}
+
+mkdir -p "${ROOT}/src"
+tmpdir="$(mktemp -d "${ROOT}/src/phmap-patch-test.XXXXXX")"
+trap 'rm -rf "${tmpdir}"' EXIT
+
+fail() {
+    echo "FAIL: $*" >&2
+    exit 1
+}
+
+prepare() {
+    local dest="$1"
+    mkdir -p "${dest}/src"
+    cp "${ROOT}/vars.sh" "${dest}/vars.sh"
+    ln -s "${ROOT}/patches" "${dest}/patches"
+    cp "${ARCHIVE_DIR}/${PARALLEL_HASHMAP_NAME}" "${dest}/src/"
+}
+
+run_download() {
+    TP_DIR="$1" DORIS_HOME="${tmpdir}" bash "${ROOT}/download-thirdparty.sh" 
parallel_hashmap
+}
+
+check_sources() {
+    local source="$1/src/${PARALLEL_HASHMAP_SOURCE}"
+    [[ -f "${source}/patched_mark_aarch64_int128" ]] || fail "missing int128 
marker"
+    [[ -f "${source}/patched_mark_arm_neon" ]] || fail "missing NEON marker"
+    [[ "$(grep -c '^struct GroupAArch64Impl {' 
"${source}/parallel_hashmap/phmap.h")" == 1 ]] \
+        || fail "NEON group must appear exactly once"
+    cmp "${tmpdir}/expected/phmap.h" "${source}/parallel_hashmap/phmap.h"
+    cmp "${tmpdir}/expected/phmap_config.h" 
"${source}/parallel_hashmap/phmap_config.h"
+}
+
+snapshot() {
+    local source="$1/src/${PARALLEL_HASHMAP_SOURCE}/parallel_hashmap"
+    cp "${source}/phmap.h" "${tmpdir}/before-phmap.h"
+    cp "${source}/phmap_config.h" "${tmpdir}/before-phmap_config.h"
+}
+
+expect_failure() {
+    local dest="$1"
+    if run_download "${dest}" >"${dest}/failure.log" 2>&1; then
+        fail "incompatible or partial patch unexpectedly succeeded"
+    fi
+    local source="${dest}/src/${PARALLEL_HASHMAP_SOURCE}"
+    [[ ! -f "${source}/patched_mark_arm_neon" ]] || fail "failed patch 
retained its marker"
+    cmp "${tmpdir}/before-phmap.h" "${source}/parallel_hashmap/phmap.h"
+    cmp "${tmpdir}/before-phmap_config.h" 
"${source}/parallel_hashmap/phmap_config.h"
+}
+
+prepare "${tmpdir}/fresh"
+run_download "${tmpdir}/fresh"
+source_dir="${tmpdir}/fresh/src/${PARALLEL_HASHMAP_SOURCE}"
+mkdir "${tmpdir}/expected"
+cp "${source_dir}/parallel_hashmap/phmap.h" "${tmpdir}/expected/"
+cp "${source_dir}/parallel_hashmap/phmap_config.h" "${tmpdir}/expected/"
+check_sources "${tmpdir}/fresh"
+touch "${source_dir}/cache_reuse_sentinel"
+run_download "${tmpdir}/fresh"
+check_sources "${tmpdir}/fresh"
+[[ -f "${source_dir}/cache_reuse_sentinel" ]] || fail "rerun replaced cached 
sources"
+echo "PASS: fresh sources and normal rerun"
+
+# Model a process stopping between the completed patch and the marker write.
+rm "${source_dir}/patched_mark_arm_neon"
+run_download "${tmpdir}/fresh"
+check_sources "${tmpdir}/fresh"
+run_download "${tmpdir}/fresh"
+check_sources "${tmpdir}/fresh"
+echo "PASS: completed patch without a marker recovers without reapplying"
+
+# A stale marker must not suppress applying the patch to pristine sources.
+prepare "${tmpdir}/stale"
+tar xzf "${ARCHIVE_DIR}/${PARALLEL_HASHMAP_NAME}" -C "${tmpdir}/stale/src"
+touch "${tmpdir}/stale/src/${PARALLEL_HASHMAP_SOURCE}/patched_mark_arm_neon"
+run_download "${tmpdir}/stale"
+check_sources "${tmpdir}/stale"
+echo "PASS: stale marker on pristine sources"
+
+# One patched file and one unpatched file must be rejected before either 
changes.
+# Keep the real int128 patch applied, as it precedes NEON in the downloader.
+for marker in missing present; do
+    prepare "${tmpdir}/partial-${marker}"
+    tar xzf "${ARCHIVE_DIR}/${PARALLEL_HASHMAP_NAME}" -C 
"${tmpdir}/partial-${marker}/src"
+    partial_source="${tmpdir}/partial-${marker}/src/${PARALLEL_HASHMAP_SOURCE}"
+    (
+        cd "${partial_source}"
+        patch --batch --forward --fuzz=0 -p1 \
+            <"${ROOT}/patches/parallel-hashmap-1.3.8-aarch64-int128.patch"
+        touch patched_mark_aarch64_int128
+    )
+    cp "${tmpdir}/expected/phmap.h" 
"${partial_source}/parallel_hashmap/phmap.h"
+    if [[ "${marker}" == present ]]; then
+        touch "${partial_source}/patched_mark_arm_neon"
+    fi
+    snapshot "${tmpdir}/partial-${marker}"
+    expect_failure "${tmpdir}/partial-${marker}"
+done
+echo "PASS: partial sources fail without further edits or a success marker"
+
+# Duplicate definitions reproduce the old insertion-only patch's replay result.
+prepare "${tmpdir}/duplicate"
+run_download "${tmpdir}/duplicate"
+duplicate_source="${tmpdir}/duplicate/src/${PARALLEL_HASHMAP_SOURCE}"
+awk '
+    /^#if PHMAP_HAVE_ARM_NEON && defined\(PHMAP_IS_LITTLE_ENDIAN\)/ && !found 
{ copying = 1 }
+    copying { block = block $0 "\n" }
+    copying && /^#endif/ { copying = 0; found = 1 }
+    { print }
+    found && !inserted { printf "%s", block; inserted = 1 }
+' "${duplicate_source}/parallel_hashmap/phmap.h" >"${tmpdir}/duplicate-phmap.h"
+mv "${tmpdir}/duplicate-phmap.h" "${duplicate_source}/parallel_hashmap/phmap.h"
+snapshot "${tmpdir}/duplicate"
+expect_failure "${tmpdir}/duplicate"
+echo "PASS: duplicate NEON definitions are rejected despite an existing marker"
diff --git a/thirdparty/tests/parallel_hashmap_neon_test.cpp 
b/thirdparty/tests/parallel_hashmap_neon_test.cpp
new file mode 100644
index 00000000000..535819c54c9
--- /dev/null
+++ b/thirdparty/tests/parallel_hashmap_neon_test.cpp
@@ -0,0 +1,272 @@
+// 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 <parallel_hashmap/phmap.h>
+#include <parallel_hashmap/phmap_dump.h>
+
+#include <algorithm>
+#include <array>
+#include <cstdint>
+#include <fstream>
+#include <iostream>
+#include <random>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <unordered_map>
+#include <vector>
+
+namespace {
+
+using phmap::priv::ctrl_t;
+using phmap::priv::Group;
+using phmap::priv::GroupPortableImpl;
+using phmap::priv::h2_t;
+using phmap::priv::kDeleted;
+using phmap::priv::kEmpty;
+using phmap::priv::kSentinel;
+
+void require(bool condition, const char* message) {
+    if (!condition) {
+        throw std::runtime_error(message);
+    }
+}
+
+template <typename Mask>
+std::vector<uint32_t> positions(Mask mask) {
+    std::vector<uint32_t> result;
+    for (uint32_t position : mask) {
+        result.push_back(position);
+    }
+    require(static_cast<bool>(mask) == !result.empty(), "mask bool");
+    if (!result.empty()) {
+        require(mask.TrailingZeros() == result.front(), "trailing zeros");
+    }
+    return result;
+}
+
+template <typename Impl>
+void check_group(const ctrl_t* bytes, h2_t hash) {
+    constexpr int width = Impl::kWidth;
+    Impl group(bytes);
+    std::vector<uint32_t> matches;
+    std::vector<uint32_t> empty;
+    std::vector<uint32_t> available;
+    for (int i = 0; i < width; ++i) {
+        if (bytes[i] == static_cast<ctrl_t>(hash)) {
+            matches.push_back(i);
+        }
+        if (bytes[i] == kEmpty) {
+            empty.push_back(i);
+        }
+        if (bytes[i] == kEmpty || bytes[i] == kDeleted) {
+            available.push_back(i);
+        }
+    }
+    auto actual = positions(group.Match(hash));
+    if constexpr (std::is_same_v<Impl, GroupPortableImpl>) {
+        // The scalar subtraction can borrow into an adjacent byte: 0x12,0x13
+        // can both match 0x12. Only full slots may be false positives.
+        require(matches.empty() == actual.empty(), "portable match emptiness");
+        for (auto expected : matches) {
+            require(std::find(actual.begin(), actual.end(), expected) != 
actual.end(),
+                    "portable missed an exact match");
+        }
+        for (auto position : actual) {
+            require(position < width && bytes[position] >= 0, "portable 
matched special slot");
+        }
+    } else {
+        require(actual == matches, "SIMD match differs from scalar oracle");
+    }
+    require(positions(group.MatchEmpty()) == empty, "empty mask");
+    require(positions(group.MatchEmptyOrDeleted()) == available, 
"empty/deleted mask");
+    if (!actual.empty()) {
+        require(group.Match(hash).LeadingZeros() == width - 1 - actual.back(), 
"leading zeros");
+    }
+    uint32_t leading = 0;
+    while (leading < width && (bytes[leading] == kEmpty || bytes[leading] == 
kDeleted)) {
+        ++leading;
+    }
+    require(group.CountLeadingEmptyOrDeleted() == leading, "leading 
empty/deleted count");
+
+    std::array<ctrl_t, width + 16> converted;
+    converted.fill(42);
+    group.ConvertSpecialToEmptyAndFullToDeleted(converted.data() + 7);
+    for (int i = 0; i < width; ++i) {
+        require(converted[i + 7] == (bytes[i] < 0 ? kEmpty : kDeleted), 
"special conversion");
+    }
+    for (int i = 0; i < 7; ++i) {
+        require(converted[i] == 42, "conversion underrun");
+    }
+    for (size_t i = width + 7; i < converted.size(); ++i) {
+        require(converted[i] == 42, "conversion overrun");
+    }
+}
+
+void check_groups() {
+    std::mt19937 random(81384);
+    std::array<ctrl_t, 32> bytes;
+    constexpr std::array<ctrl_t, 3> special_values {kEmpty, kDeleted, 
kSentinel};
+    for (int iteration = 0; iteration < 100000; ++iteration) {
+        bytes.fill(kEmpty);
+        auto* data = bytes.data() + (iteration & 7);
+        for (int i = 0; i < Group::kWidth; ++i) {
+            uint32_t value = random() % 131;
+            data[i] = value < 128 ? static_cast<ctrl_t>(value) : 
special_values[value - 128];
+        }
+        h2_t hash = random() % 128;
+        check_group<Group>(data, hash);
+        check_group<GroupPortableImpl>(data, hash);
+    }
+    for (int offset = 0; offset < 8; ++offset) {
+        for (auto special : {kEmpty, kDeleted, kSentinel}) {
+            bytes.fill(special);
+            check_group<Group>(bytes.data() + offset, 0);
+            check_group<GroupPortableImpl>(bytes.data() + offset, 0);
+        }
+        for (int lane = 0; lane < Group::kWidth; ++lane) {
+            for (int value = 0; value < 128; ++value) {
+                bytes.fill(kEmpty);
+                bytes[offset + lane] = static_cast<ctrl_t>(value);
+                check_group<Group>(bytes.data() + offset, value);
+            }
+        }
+    }
+    bytes.fill(kEmpty);
+    bytes[2] = 0x12;
+    bytes[3] = 0x13;
+    check_group<Group>(bytes.data(), 0x12);
+    require(positions(GroupPortableImpl(bytes.data()).Match(0x12)) == 
std::vector<uint32_t>({2, 3}),
+            "adjacent-H2 scalar baseline");
+}
+
+template <typename Map>
+void check_map() {
+    Map map;
+    std::unordered_map<uint64_t, uint64_t> reference;
+    std::mt19937_64 random(81384);
+    map.reserve(65536);
+    for (int i = 0; i < 200000; ++i) {
+        uint64_t key = random() % 60000;
+        uint64_t value = random();
+        switch (i % 4) {
+        case 0:
+        case 1:
+            map[key] = value;
+            reference[key] = value;
+            break;
+        case 2:
+            require(map.erase(key) == reference.erase(key), "erase result");
+            break;
+        default: {
+            auto found = map.find(key);
+            auto expected = reference.find(key);
+            require((found == map.end()) == (expected == reference.end()), 
"find result");
+            if (found != map.end()) {
+                require(found->second == expected->second, "find value");
+            }
+        }
+        }
+        if (i % 16384 == 0) {
+            map.rehash(map.size() * 2);
+        }
+    }
+    require(map.size() == reference.size(), "map size");
+    for (const auto& [key, value] : reference) {
+        auto found = map.find(key);
+        require(found != map.end() && found->second == value, "map final 
content");
+    }
+}
+
+struct Archive {
+    std::iostream& stream;
+    void saveBinary(const void* data, size_t bytes) {
+        stream.write(static_cast<const char*>(data), bytes);
+        require(static_cast<bool>(stream), "dump write");
+    }
+    void loadBinary(void* data, size_t bytes) {
+        stream.read(static_cast<char*>(data), bytes);
+        require(static_cast<bool>(stream), "dump read");
+    }
+};
+
+using DumpMap = phmap::flat_hash_map<uint64_t, uint64_t>;
+
+DumpMap dump_fixture() {
+    DumpMap map;
+    for (uint64_t i = 0; i < 10000; ++i) {
+        map[i * 7 + 3] = i * 11 + 5;
+    }
+    for (uint64_t i = 0; i < 10000; i += 3) {
+        map.erase(i * 7 + 3);
+    }
+    return map;
+}
+
+void check_loaded(const DumpMap& map) {
+    auto expected = dump_fixture();
+    require(map.size() == expected.size(), "dump/load size");
+    for (const auto& [key, value] : expected) {
+        auto found = map.find(key);
+        require(found != map.end() && found->second == value, "dump/load 
content");
+    }
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+    try {
+        if (argc == 3) {
+            const bool dumping = std::string(argv[1]) == "--dump";
+            auto mode = std::ios::in | std::ios::out | std::ios::binary;
+            if (dumping) {
+                mode |= std::ios::trunc;
+            }
+            std::fstream stream(argv[2], mode);
+            require(static_cast<bool>(stream), "open archive");
+            Archive archive {stream};
+            if (dumping) {
+                auto map = dump_fixture();
+                require(map.phmap_dump(archive), "dump");
+            } else {
+                require(std::string(argv[1]) == "--load", "unknown archive 
operation");
+                DumpMap map;
+                require(map.phmap_load(archive), "load");
+                check_loaded(map);
+            }
+        } else {
+            require(argc == 1, "unexpected arguments");
+            check_groups();
+            check_map<phmap::flat_hash_map<uint64_t, uint64_t>>();
+            check_map<phmap::parallel_flat_hash_map<uint64_t, uint64_t>>();
+            std::stringstream stream;
+            Archive archive {stream};
+            auto source = dump_fixture();
+            require(source.phmap_dump(archive), "roundtrip dump");
+            DumpMap loaded;
+            require(loaded.phmap_load(archive), "roundtrip load");
+            check_loaded(loaded);
+        }
+        std::cout << "PASS group_width=" << Group::kWidth
+                  << " portable=" << std::is_same_v<Group, GroupPortableImpl> 
<< '\n';
+        return 0;
+    } catch (const std::exception& error) {
+        std::cerr << "FAIL: " << error.what() << '\n';
+        return 1;
+    }
+}


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

Reply via email to