本文算法完全来自于丹大师的博客,只是简单做一点内容上的扩展。

提前省流:具体算法就是减少分支;丹大师的测试结果在现在看来不适用。

基本的查找

常规的容器一般使用如下查找算法:

#include <cstddef>

bool find(int* arr, size_t size, int val) {
    #pragma clang loop unroll_count(4)
    for(size_t i = 0; i < size; ++i) {
        if(val == arr[i]) return true;
    }
    return false;
}

clang++-18 生成如下指令(-O3,简化了重复占版面的循环展开):

0000000000000000 <_Z4findPimi>:
   0: 48 85 f6              test   %rsi,%rsi             // 判断是否为空
   3: 74 23                 je     28 <_Z4findPimi+0x28> // 如果是,则返回 false
   5: b0 01                 mov    $0x1,%al              // 返回值预设 true,同时作为下标计数器(见 1c)
   7: 39 17                 cmp    %edx,(%rdi)           // 对比 val 和 arr[0]
   9: 74 1c                 je     27 <_Z4findPimi+0x27> // 如果相等,则返回预设的 true
   b: b9 01 00 00 00        mov    $0x1,%ecx             // %rcx/%ecx 和 %rax 用于下标计数器(见 18)
  10: 48 89 c8              mov    %rcx,%rax             // %rax 此时用于下标计数器,初始值为 1
  13: 48 39 ce              cmp    %rcx,%rsi             // 如果下标已经到达末尾……
  16: 74 09                 je     21 <_Z4findPimi+0x21> // 那就跳转
  18: 48 8d 48 01           lea    0x1(%rax),%rcx        // 否则后续下标间接增加 1(为啥不直接生成 inc %rcx?)
  1c: 39 14 87              cmp    %edx,(%rdi,%rax,4)    // 对比 arr[i] == val
  1f: 75 ef                 jne    10 <_Z4findPimi+0x10> // 不符合条件就进入下一次循环
  21: 48 39 f0              cmp    %rsi,%rax             // 对比 i < size
  24: 0f 92 c0              setb   %al                   // 未达末尾就返回 true
  27: c3                    ret
  28: 31 c0                 xor    %eax,%eax
  2a: c3                    ret

NOTE: 这是 libstdc++ 标准库实际使用的 std::find 实现,细微区别在于人家是坚持手作 unroll。

优化的查找

假设容器的 sentinel 具有值语义且可修改,可以优化如下:

#include <cstddef>

bool find_v2(int* arr, size_t size, int val) {
    // Hidden sentinel!
    arr[size] = val;

    #pragma clang loop unroll_count(4)
    for(size_t i = 0; ; ++i) {
        if(val == arr[i]) return i != size;
    }
    // Unreachable.
    return false;
}

clang++-18 生成如下指令(-O3,同理没有循环展开部分):

0000000000000000 <_Z7find_v2Pimi>:
   0: 89 14 b7              mov    %edx,(%rdi,%rsi,4)       // sentinel 赋值
   3: 31 c0                 xor    %eax,%eax                // 作为下标计数器,初始值为 0
   5: 66 66 2e 0f 1f 84 00  data16 cs nopw 0x0(%rax,%rax,1) // 仅用于对齐
   c: 00 00 00 00 
  10: 48 89 c1              mov    %rax,%rcx                // 作为下标计数器
  13: 48 ff c0              inc    %rax                     // 下一次循环的下标
  16: 39 14 8f              cmp    %edx,(%rdi,%rcx,4)       // 对比 arr[i] == val
  19: 75 f5                 jne    10 <_Z7find_v2Pimi+0x10> // 不满足则进入下一次循环
  1b: 48 ff c6              inc    %rsi                     // %rsi = size + 1
  1e: 48 39 c6              cmp    %rax,%rsi                // 如果 i+1 != size+1 的话……
  21: 0f 95 c0              setne  %al                      // 那就返回 true
  24: c3                    ret                             // (所以为啥不直接拿 %rcx 和 %rsi 比呢……)

简单来说就是每次循环都减少了一次 v1 版本中的 i < size 分支判断指令,因为我们的哨兵总能让这个隐含的条件自然结束。从 jcc 指令的数目也可以粗略猜到性能差异,但是我们需要实测。

NOTE: 完整的指令可以参考 godbolt

基准测试一

// clang++ -S -O3 find.cpp -o - | llvm-mca
Iterations:        100
Instructions:      4000
Total Cycles:      760
Total uOps:        4000

Dispatch Width:    6
uOps Per Cycle:    5.26
IPC:               5.26
Block RThroughput: 7.5 // 😞

// clang++ -S -O3 find_v2.cpp -o - | llvm-mca
Iterations:        100
Instructions:      3000
Total Cycles:      608
Total uOps:        3000

Dispatch Width:    6
uOps Per Cycle:    4.93
IPC:               4.93
Block RThroughput: 5.0 // 😄

先使用 LLVM-MCA 做静态性能预测,可以看出 v2 版本的代码生成质量更优。

基准测试二

// best case: 查找数组中的首个元素
// worst case: 查找数组中的末尾元素
// average case: 查找数组中的中间元素
CPU Caches:
  L1 Data 32 KiB (x8)
  L1 Instruction 32 KiB (x8)
  L2 Unified 1024 KiB (x8)
  L3 Unified 16384 KiB (x1)
Load Average: 0.24, 0.25, 0.11
---------------------------------------------------------------------------
Benchmark                                 Time             CPU   Iterations
---------------------------------------------------------------------------
BM_find_best_case/8                    1.01 ns         1.01 ns    658995011
BM_find_best_case/16                   1.01 ns         1.01 ns    659940864
BM_find_best_case/32                   1.01 ns         1.01 ns    663255342
BM_find_best_case/64                   1.01 ns         1.01 ns    659104479
BM_find_best_case/128                  1.01 ns         1.01 ns    667078063
BM_find_best_case/256                  1.00 ns         1.00 ns    665760535
BM_find_best_case/512                 0.998 ns        0.998 ns    672838919
BM_find_best_case/1024                 1.47 ns         1.47 ns    467624330
BM_find_best_case/2048                 1.01 ns         1.01 ns    662828304
BM_find_best_case/4096                 1.02 ns         1.02 ns    655883031
BM_find_best_case/8192                 1.01 ns         1.01 ns    661704357
BM_find_best_case/16384                1.02 ns         1.02 ns    657260684
BM_find_best_case/32768                1.02 ns         1.02 ns    658413702
BM_find_best_case/65536                1.01 ns         1.01 ns    661556933
BM_find_best_case/131072               1.02 ns         1.02 ns    670742089
BM_find_best_case/262144               1.01 ns         1.01 ns    661440824
BM_find_best_case/524288               1.01 ns         1.01 ns    664805149
BM_find_best_case/1048576              1.01 ns         1.01 ns    679291625
BM_find_best_case/2097152              1.02 ns         1.02 ns    671568522
BM_find_best_case/4194304              1.00 ns         1.00 ns    682087755
BM_find_best_case/8388608              1.00 ns         1.00 ns    682462635
BM_find_best_case/16777216             1.48 ns         1.48 ns    474557730
BM_find_best_case/33554432             1.48 ns         1.48 ns    473469374
BM_find_best_case/67108864             1.90 ns         1.90 ns    364997786
BM_find_v2_best_case/8                 1.05 ns         1.05 ns    634366581
BM_find_v2_best_case/16                1.04 ns         1.04 ns    583873592
BM_find_v2_best_case/32                1.05 ns         1.05 ns    645523533
BM_find_v2_best_case/64                1.04 ns         1.04 ns    642675493
BM_find_v2_best_case/128               1.06 ns         1.06 ns    642491168
BM_find_v2_best_case/256               1.04 ns         1.04 ns    638667286
BM_find_v2_best_case/512               1.05 ns         1.05 ns    639525158
BM_find_v2_best_case/1024              2.15 ns         2.15 ns    320029152
BM_find_v2_best_case/2048              1.08 ns         1.08 ns    615228415
BM_find_v2_best_case/4096              1.09 ns         1.09 ns    616060495
BM_find_v2_best_case/8192              1.08 ns         1.08 ns    619851387
BM_find_v2_best_case/16384             1.09 ns         1.09 ns    623755858
BM_find_v2_best_case/32768             1.10 ns         1.10 ns    591332645
BM_find_v2_best_case/65536             1.10 ns         1.10 ns    623551623
BM_find_v2_best_case/131072            1.10 ns         1.10 ns    616512218
BM_find_v2_best_case/262144            1.09 ns         1.09 ns    616320040
BM_find_v2_best_case/524288            1.07 ns         1.07 ns    617523472
BM_find_v2_best_case/1048576           1.08 ns         1.08 ns    620027567
BM_find_v2_best_case/2097152           1.10 ns         1.10 ns    610900178
BM_find_v2_best_case/4194304           1.10 ns         1.10 ns    624739188
BM_find_v2_best_case/8388608           1.09 ns         1.09 ns    649713977
BM_find_v2_best_case/16777216          2.14 ns         2.14 ns    330665800
BM_find_v2_best_case/33554432          2.15 ns         2.15 ns    329984987
BM_find_v2_best_case/67108864          3.43 ns         3.43 ns    230014000
BM_find_worst_case/8                   1.95 ns         1.95 ns    352421202
BM_find_worst_case/16                  3.56 ns         3.55 ns    195380271
BM_find_worst_case/32                  6.74 ns         6.74 ns     95370364
BM_find_worst_case/64                  13.1 ns         13.1 ns     50075888
BM_find_worst_case/128                 26.0 ns         26.0 ns     26253410
BM_find_worst_case/256                 52.0 ns         52.0 ns     12500177
BM_find_worst_case/512                  104 ns          104 ns      6585238
BM_find_worst_case/1024                 214 ns          214 ns      3239463
BM_find_worst_case/2048                 421 ns          421 ns      1675597
BM_find_worst_case/4096                 821 ns          821 ns       797520
BM_find_worst_case/8192                1657 ns         1657 ns       408924
BM_find_worst_case/16384               3280 ns         3280 ns       210586
BM_find_worst_case/32768               6670 ns         6670 ns       101040
BM_find_worst_case/65536              13158 ns        13158 ns        51230
BM_find_worst_case/131072             26715 ns        26715 ns        26424
BM_find_worst_case/262144             53396 ns        53395 ns        12372
BM_find_worst_case/524288            105524 ns       105524 ns         6150
BM_find_worst_case/1048576           210971 ns       210972 ns         3145
BM_find_worst_case/2097152           424554 ns       424557 ns         1655
BM_find_worst_case/4194304           871541 ns       871546 ns          770
BM_find_worst_case/8388608          1722983 ns      1722995 ns          395
BM_find_worst_case/16777216         3484659 ns      3484711 ns          199
BM_find_worst_case/33554432         6981308 ns      6981458 ns           97
BM_find_worst_case/67108864        14016949 ns     14005970 ns           50
BM_find_v2_worst_case/8                1.87 ns         1.87 ns    367027163
BM_find_v2_worst_case/16               2.65 ns         2.65 ns    258739829
BM_find_v2_worst_case/32               4.32 ns         4.32 ns    161791764
BM_find_v2_worst_case/64               7.64 ns         7.64 ns     85923262
BM_find_v2_worst_case/128              14.3 ns         14.3 ns     48003896
BM_find_v2_worst_case/256              26.9 ns         26.9 ns     24655725
BM_find_v2_worst_case/512              52.8 ns         52.9 ns     12588132
BM_find_v2_worst_case/1024              111 ns          111 ns      6055381
BM_find_v2_worst_case/2048              214 ns          215 ns      3206550
BM_find_v2_worst_case/4096              424 ns          424 ns      1650669
BM_find_v2_worst_case/8192              867 ns          868 ns       762432
BM_find_v2_worst_case/16384            1923 ns         1925 ns       366180
BM_find_v2_worst_case/32768            3774 ns         3778 ns       180173
BM_find_v2_worst_case/65536            7354 ns         7660 ns        83772
BM_find_v2_worst_case/131072          14210 ns        15502 ns        45643
BM_find_v2_worst_case/262144          28925 ns        31555 ns        21975
BM_find_v2_worst_case/524288          58576 ns        63901 ns        10677
BM_find_v2_worst_case/1048576        105916 ns       115545 ns         5809
BM_find_v2_worst_case/2097152        217553 ns       237331 ns         2926
BM_find_v2_worst_case/4194304        495386 ns       540422 ns         1205
BM_find_v2_worst_case/8388608        985209 ns      1060639 ns          661
BM_find_v2_worst_case/16777216      2259903 ns      2259874 ns          321
BM_find_v2_worst_case/33554432      4586589 ns      4516989 ns          159
BM_find_v2_worst_case/67108864      9597705 ns      9425770 ns           83
BM_find_average_case/8                 1.54 ns         1.51 ns    451508820
BM_find_average_case/16                2.30 ns         2.26 ns    305975643
BM_find_average_case/32                4.01 ns         3.94 ns    177846394
BM_find_average_case/64                7.17 ns         7.11 ns     92837892
BM_find_average_case/128               13.5 ns         13.5 ns     50191614
BM_find_average_case/256               26.3 ns         26.3 ns     25961168
BM_find_average_case/512               52.8 ns         51.9 ns     12803869
BM_find_average_case/1024               106 ns          104 ns      6555978
BM_find_average_case/2048               208 ns          208 ns      3375323
BM_find_average_case/4096               420 ns          420 ns      1674677
BM_find_average_case/8192               835 ns          835 ns       803151
BM_find_average_case/16384             1647 ns         1647 ns       414660
BM_find_average_case/32768             3307 ns         3307 ns       207517
BM_find_average_case/65536             6671 ns         6671 ns       100027
BM_find_average_case/131072           13404 ns        13404 ns        50081
BM_find_average_case/262144           26478 ns        26478 ns        26062
BM_find_average_case/524288           54283 ns        54283 ns        12388
BM_find_average_case/1048576         105745 ns       105745 ns         6332
BM_find_average_case/2097152         213429 ns       213427 ns         3243
BM_find_average_case/4194304         423439 ns       423438 ns         1580
BM_find_average_case/8388608         863090 ns       863078 ns          809
BM_find_average_case/16777216       1732264 ns      1732225 ns          389
BM_find_average_case/33554432       3611597 ns      3611527 ns          202
BM_find_average_case/67108864       6959930 ns      6960173 ns          100
BM_find_v2_average_case/8              1.45 ns         1.45 ns    478012856
BM_find_v2_average_case/16             1.84 ns         1.84 ns    370132859
BM_find_v2_average_case/32             2.71 ns         2.71 ns    260878805
BM_find_v2_average_case/64             4.25 ns         4.25 ns    163010343
BM_find_v2_average_case/128            7.84 ns         7.84 ns     84815178
BM_find_v2_average_case/256            14.3 ns         14.3 ns     47580872
BM_find_v2_average_case/512            27.0 ns         27.0 ns     24920250
BM_find_v2_average_case/1024           54.1 ns         54.4 ns     11824927
BM_find_v2_average_case/2048            111 ns          112 ns      5973142
BM_find_v2_average_case/4096            217 ns          219 ns      3233183
BM_find_v2_average_case/8192            417 ns          423 ns      1666755
BM_find_v2_average_case/16384           861 ns          872 ns       783335
BM_find_v2_average_case/32768          1881 ns         1906 ns       358115
BM_find_v2_average_case/65536          3761 ns         3810 ns       186855
BM_find_v2_average_case/131072         7429 ns         7526 ns        89232
BM_find_v2_average_case/262144        15052 ns        15248 ns        45060
BM_find_v2_average_case/524288        29185 ns        31492 ns        21300
BM_find_v2_average_case/1048576       56859 ns        62028 ns        10558
BM_find_v2_average_case/2097152      108465 ns       118326 ns         5626
BM_find_v2_average_case/4194304      217734 ns       237525 ns         2904
BM_find_v2_average_case/8388608      482902 ns       520782 ns         1344
BM_find_v2_average_case/16777216    1115999 ns      1115973 ns          637
BM_find_v2_average_case/33554432    2230035 ns      2229990 ns          321
BM_find_v2_average_case/67108864    4369605 ns      4369533 ns          161
测试代码
#include <benchmark/benchmark.h>
#include <vector>
#include <cstdlib>

auto make(auto &bm, auto size) {
    auto size_with_sentinel = size + 1;
    std::vector<int> vec(size_with_sentinel);
    for(auto i = 0; auto &v : vec) v = ++i;
    benchmark::DoNotOptimize(vec);
    return vec;
}

bool find(int* arr, size_t size, int val) {
    #pragma clang loop unroll_count(4)
    for(size_t i = 0; i < size; ++i) {
        if(val == arr[i]) return true;
    }
    return false;
}

bool find_v2(int* arr, size_t size, int val) {
    arr[size] = val;

    #pragma clang loop unroll_count(4)
    for(size_t i = 0; ; ++i) {
        if(val == arr[i]) return i != size;
    }
    return false;
}

template <auto F>
void bench_template(benchmark::State& state, int target_index) {
    auto size = state.range(0);
    auto vec = make(state, size);
    for(auto _ : state) {
        auto index = target_index;
        auto old = vec[size];
        benchmark::DoNotOptimize(index);
        auto target = vec[index];
        benchmark::DoNotOptimize(target);
        auto result = F(vec.data(), size, target);
        benchmark::DoNotOptimize(result);
        vec[size] = old;
    }
}

void BM_find_best_case(benchmark::State& state) {
    bench_template<find>(state, 0);
}

void BM_find_v2_best_case(benchmark::State& state) {
    bench_template<find_v2>(state, 0);
}

void BM_find_worst_case(benchmark::State& state) {
    bench_template<find>(state, state.range(0));
}

void BM_find_v2_worst_case(benchmark::State& state) {
    bench_template<find_v2>(state, state.range(0));
}

void BM_find_average_case(benchmark::State& state) {
    bench_template<find>(state, state.range(0) / 2);
}

void BM_find_v2_average_case(benchmark::State& state) {
    bench_template<find_v2>(state, state.range(0) / 2);
}

#define FIND_BM_(name) BENCHMARK(name)->RangeMultiplier(2)->Range(8, 1<<26)
FIND_BM_(BM_find_best_case);
FIND_BM_(BM_find_v2_best_case);
FIND_BM_(BM_find_worst_case);
FIND_BM_(BM_find_v2_worst_case);
FIND_BM_(BM_find_average_case);
FIND_BM_(BM_find_v2_average_case);
#undef FIND_BM_

BENCHMARK_MAIN();

然后使用 google benchmark 做运行时跑分。可以看出,任意数据规模,v2 算法均有明显优势。

NOTES:

  • 一个有趣的现象:丹大师在 2016 年测试得出的结论是,非循环展开的版本 v2 比 v1 快约 20%。但是 2025 年的现在,实测是非循环展开版本 v1 比 v2 还要快(当然了,全部展开的话就是上述表现,v2 把 v1 吊着打)。
  • 这么多年了,编译器生成的代码倒是差异不大。也就是说,很可能是体系结构的更新导致了「算法过期」这种现象,有空 perf 剖析一下。

通用扩展

如果像上面所说的,要求 sentinel 可修改,那基本上应用场景是非常窄的。比如考虑常见的迭代器模式 \([begin, end)\),对于一个子范围(range)来说,end 可能是有意义的数据,是不应修改的;其次,对于完整的范围来说,end 根本没有值语义,是无法执行写操作的。

// 简单起见,就不写 begin 和 end 迭代器了
template <typename T /*, typename Iter*/>
bool find_v2_subrange(T* arr, size_t size, T val) {
    auto old = std::move(arr[size]);
    arr[size] = std::move(val);

    #pragma clang loop unroll_count(4)
    for(size_t i = 0; ; ++i) {
        if(val == arr[i]) {
            arr[size] = std::move(old);
            return i != size;
        }
    }
    // 实际上这段代码不可达,不需继续处理
    return false;
}

但是只要你的元素是可移动的,那么不覆盖到哨兵的子范围可以轻松实现如上版本。不过「可移动」其实也意味着 const range 肯定用不了这种优化。

template <typename T /*, typename Iter*/>
bool find_v2_range(T* arr, size_t size, T val) {
    // 会多出一些分支
    if(size == 0) return false;
    if(arr[size - 1] == val) return true;

    auto old = std::move(arr[size - 1]);
    arr[size - 1] = std::move(val);

    #pragma clang loop unroll_count(4)
    for(size_t i = 0; ; ++i) {
        if(val == arr[i]) {
            arr[size - 1] = std::move(old);
            return i != size - 1;
        }
    }
    // 实际上这段代码不可达,不需继续处理
    return false;
}

对于任意范围该如何支持?不考虑改动数据结构本身的话,那就先判断实质 sentinel 前的一个元素,然后照着抄就好了。

总结

这篇文章也就只是搜索一些 branchless programming(不懂,别问)资料时顺手做的测试,实际上这种优化方法也是老黄历了,没想到跑得还挺快。

总之,不考虑那么远,如果特定场景合适(流式数据!),不妨试试 v2 算法。


后记:自动向量化

考虑进一步的编译器优化,自然是想到 -march 或者 vectorize(enable) 编译器选项来提供更加激进的向量化,比如指定 skylake-avx512。不过 Clang 拒绝了,上述两种实现都是生成此前相同的指令。

clang++ find.cpp -O3 -march=native -Rpass-analysis=loop-vectorize -c
find.cpp:4:5: remark: loop not vectorized: could not determine number of loop iterations [-Rpass-analysis=loop-vectorize]
    4 |     for(size_t i = 0; i < 512; ++i) {
      |     ^

NOTES:

  • 虽然 Clang 可以用 -Rpass-analysis=loop-vectorize 来分析拒绝向量化的原因……
  • 但是如上所示,事实上它也会胡说八道。
  • GCC trunk 版本可以做到向量化,还没仔细分析过指令(godbolt),TODO。
  • 再次测试编译期已知大小的未展开 v1 的代码生成(godbolt)。使用 std::array<int, N> 时,当 N > 74 就是 v1 算法,但是更小的规模就会在 v1 的基础上继续做向量化展开。有点超出预料,有空也可以测一下性能。

图文无关

cainiao