c++ - bool 数组上的原始循环比变换或 for_each 快 5 倍

标签 c++ performance c++17 clang++

根据我之前对 transform 和 for_each 进行基准测试的经验,它们的执行速度通常比原始循环稍快,当然它们也更安全,因此我尝试将所有原始循环替换为 transform、generate 和 for_each。今天,我比较了使用 for_each、transform 和 raw 循环翻转 bool 值的速度,我得到了非常令人惊讶的结果。 raw_loop 的执行速度比其他两个快 5 倍。我真的找不到一个很好的理由为什么我们会得到如此巨大的差异?

#include <array>
#include <algorithm>


static void ForEach(benchmark::State& state) {
  std::array<bool, sizeof(short) * 8> a;
  std::fill(a.begin(), a.end(), true);

  for (auto _ : state) {
    std::for_each(a.begin(), a.end(), [](auto & arg) { arg = !arg; });
    benchmark::DoNotOptimize(a);
  }
}
BENCHMARK(ForEach);

static void Transform(benchmark::State& state) {
  std::array<bool, sizeof(short) * 8> a;
  std::fill(a.begin(), a.end(), true);

  for (auto _ : state) {
    std::transform(a.begin(), a.end(), a.begin(), [](auto arg) { return !arg; });
    benchmark::DoNotOptimize(a);
  }
}
BENCHMARK(Transform);


static void RawLoop(benchmark::State& state) {
  std::array<bool, sizeof(short) * 8> a;
  std::fill(a.begin(), a.end(), true);

  for (auto _ : state) {
    for (int i = 0; i < a.size(); i++) {
      a[i] = !a[i];
    }
    benchmark::DoNotOptimize(a);
  }
}
BENCHMARK(RawLoop);

clang++ (7.0) -O3 -libc++ (LLVM)

enter image description here

最佳答案

在此示例中,clang 向量化索引,但(错误地)未能向量化迭代。

总结一下结果,使用原始循环和使用 std::transformstd::for_each 没有区别。然而,使用索引和使用迭代之间是有区别的,就这个特殊问题而言,clang 在优化索引方面比在优化迭代方面更好,因为索引是矢量化的。 std::transformstd::for_each 使用迭代,因此它们最终会变慢(在 clang 下编译时)。

索引和迭代有什么区别? - 索引是当你使用一个整数来索引一个数组 - 迭代是指将指针从 begin() 增加到 end()

让我们使用索引和使用迭代来编写原始循环,并将迭代(使用原始循环)与索引的性能进行比较。

// Indexing
for(int i = 0; i < a.size(); i++) {
    a[i] = !a[i];
}
// Iterating, used by std::for_each and std::transform
bool* begin = a.data();
bool* end   = begin + a.size(); 
for(; begin != end; ++begin) {
    *begin = !*begin; 
}

使用索引的示例得到了更好的优化,使用 clang 编译时运行速度提高了 4-5 倍。

为了证明这一点,让我们添加两个额外的测试,都使用原始循环。一种将使用迭代器,另一种将使用原始指针。


static void RawLoopIt(benchmark::State& state) {
  std::array<bool, 16> a;
  std::fill(a.begin(), a.end(), true); 

  for(auto _ : state) {
    auto scan = a.begin(); 
    auto end = a.end(); 
    for (; scan != end; ++scan) {
      *scan = !*scan; 
    }
    benchmark::DoNotOptimize(a); 
  }
 }

BENCHMARK(RawLoopIt); 

static void RawLoopPtr(benchmark::State& state) {
  std::array<bool, 16> a;
  std::fill(a.begin(), a.end(), true); 

  for(auto _ : state) {
    bool* scan = a.data(); 
    bool* end = scan + a.size(); 
    for (; scan != end; ++scan) {
      *scan = !*scan; 
    }
    benchmark::DoNotOptimize(a); 
  } 
}

BENCHMARK(RawLoopPtr); 

当使用从 beginend 的指针或迭代器时,这些函数性能相同 与使用 std::for_each std::transform.

Clang Quick-bench results:

enter image description here

这通过在本地运行 clang 基准得到确认:

me@K2SO:~/projects/scratch$ clang++ -O3 bench.cpp -lbenchmark -pthread -o clang-bench
me@K2SO:~/projects/scratch$ ./clang-bench
2019-07-05 16:13:27
Running ./clang-bench
Run on (8 X 4000 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 256K (x4)
  L3 Unified 8192K (x1)
Load Average: 0.44, 0.55, 0.59
-----------------------------------------------------
Benchmark           Time             CPU   Iterations
-----------------------------------------------------
ForEach          8.32 ns         8.32 ns     83327615
Transform        8.29 ns         8.28 ns     82536410
RawLoop          1.92 ns         1.92 ns    361745495
RawLoopIt        8.31 ns         8.31 ns     81848945
RawLoopPtr       8.28 ns         8.28 ns     82504276

GCC 没有这个问题。

就本示例而言,索引或迭代之间没有根本区别。它们都对数组应用相同的转换,并且编译器应该能够相同地编译它们。

确实,GCC 能够做到这一点,所有方法的运行速度比在 clang 下编译的相应版本快

GCC Quick-bench results: enter image description here

GCC 本地结果:

2019-07-05 16:13:35
Running ./gcc-bench
Run on (8 X 4000 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 256K (x4)
  L3 Unified 8192K (x1)
Load Average: 0.52, 0.57, 0.60
-----------------------------------------------------
Benchmark           Time             CPU   Iterations
-----------------------------------------------------
ForEach          1.43 ns         1.43 ns    484760981
Transform        1.44 ns         1.44 ns    485788409
RawLoop          1.43 ns         1.43 ns    484973417
RawLoopIt        1.44 ns         1.44 ns    482685685
RawLoopPtr       1.44 ns         1.44 ns    483736235

索引实际上比迭代更快吗?不会。索引速度更快,因为 clang 对其进行了矢量化。

在底层,既不迭代也不索引发生。相反,gcc 和 clang 通过将数组视为两个 64 位整数并对它们使用按位异或来矢量化操作。我们可以在用于翻转位的程序集中看到这一点:

       movabs $0x101010101010101,%rax
       nopw   %cs:0x0(%rax,%rax,1)
       xor    %rax,(%rsp)
       xor    %rax,0x8(%rsp)
       sub    $0x1,%rbx

在使用 clang 编译时,迭代速度较慢,因为由于某种原因,使用迭代时,clang 无法向量化操作。这是 clang 的一个缺陷,也是特别适用于这个问题。随着 clang 的改进,这种差异应该会消失,这不是我现在担心的事情。

不要进行微优化。让编译器处理这个问题,如有必要,测试 gcc 或 clang 是否为您的特定用例生成更快的代码。对于所有情况,两者都不是更好。例如,clang 更擅长向量化一些数学运算。

关于c++ - bool 数组上的原始循环比变换或 for_each 快 5 倍,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56873187/

相关文章:

c++ - 如果我的 C++ “new” 内存分配失败,如何找出返回值?

database - 你能推荐一个水平扩展的数据库吗?

javascript - Underscorejs 'defer'(或 setTimeout 1)无法持续工作

c++ - 模板相关参数的类模板参数推导

c++ - 返回不可复制常量值的函数的不直观 RVO?

c++ - Visual Studio 无法识别虚幻引擎

c++ - 如何获取卷 GUID

c++ - 关于类中 char* 函数成员函数的问题

jquery - $(selector).load ('page.html #id' ) 与 $.get 和过滤/查找 #id

c++ - 可变函数模板 : automating N inputs at run time based on N compile time value