C++ 求和 for 循环基准测试





5.00/5 (2投票s)
C++ 求和 For 循环基准测试产生了有趣的结果以及不同编译器下的汇编代码。
引言
最初的动机是找出 C++ 中不同类型的 for 循环的开销。
代码
将以下代码复制并粘贴到 Godbolt 在线 C++ 编译器 中,以查看生成的汇编代码。 注意: 数组或向量在基准测试中进行了初始化。以下简化后的代码供您复制并粘贴到 Godbolt 在线 C++ 编译器 中,以便您仅阅读相关的汇编代码,其他代码只会增加干扰,您需要仔细筛选才能找到汇编代码。
在使用 Godbolt 之前,我一直在研究 Visual C++ 生成的汇编代码,但由于每行 C++ 代码的优化汇编代码与其他行的汇编代码交错在一起,因此很难阅读。 我猜测这样做可能是为了利用 CPU 流水线,以便不依赖于先前操作结果的代码可以独立执行。 使用 Godbolt 是最好和最简单的方法。相信我。
#include <cstdint>
#include <algorithm>
#include <numeric>
#include <iterator>
const size_t LEN = 1000000;
// Increment For Loop
uint64_t func1()
{
uint64_t vec[LEN];
uint64_t sum = 0;
for (size_t i = 0; i < LEN; ++i)
{
sum += vec[i];
}
return sum;
}
// Range For Loop
uint64_t func2()
{
uint64_t vec[LEN];
uint64_t sum = 0;
for (auto n : vec)
{
sum += n;
}
return sum;
}
// Iterator For Loop
uint64_t func3()
{
uint64_t vec[LEN];
uint64_t sum = 0;
for (auto it = std::cbegin(vec); it != std::cend(vec); ++it)
{
sum += *it;
}
return sum;
}
// Accumulator
uint64_t func4()
{
uint64_t vec[LEN];
uint64_t sum = 0;
const uint64_t Zero = 0;
sum = std::accumulate(std::cbegin(vec), std::cend(vec), Zero);
return sum;
}
测试机器: Intel i7 6700,主频 3.4 GHz
Visual C++ 2017 (15.4 更新) 结果
请忽略求和结果。我显示结果和是为了防止编译器优化掉 for 循环。Visual C++ 使用 SSE2 对代码进行了向量化。
Increment For Loop: 599ms, sum:500000500000 Range For Loop: 446ms, sum:500000500000 Iterator For Loop: 558ms, sum:500000500000 Accumulator: 437ms, sum:500000500000
调查显示,数组索引下标乘以 8 可能是 递增 For 循环 速度变慢的原因。
sum += vec[i];
movdqu xmm0, XMMWORD PTR vec$[rsp+rax*8] <== multiplication by 8
对于 范围 For 循环,地址递增 16(= 8 + 8,因为进行了循环展开),未使用乘法来计算地址。累加器代码使用相同的策略。 在十年前,C 程序员对 std::accumulate
比 for 循环更快感到困惑。现在我们知道了原因。
对于 迭代器 For 循环 较差的结果,我的猜测是迭代器开销。
Cygwin clang++ 3.9.1 结果
clang++ 为所有 4 个循环生成了相似的代码,因此,计时也相似。clang++ 使用 SSE2 对循环进行了向量化。要使用 clang++ 编译代码,请使用以下命令。
# clang++ ForLoopBenchmark.cpp -O2 -std=c++14
Increment For Loop: 392ms, sum:500000500000 Range For Loop: 406ms, sum:500000500000 Iterator For Loop: 381ms, sum:500000500000 Accumulator: 391ms, sum:500000500000
Cygwin g++ 5.4 结果
与 clang++ 类似,g++ 也为所有 4 个循环生成了相似的代码,因此它们的计时也相似,但遗憾的是,在 O2 中循环没有被向量化。指定 O3 可以向量化所有循环,结果与 clang++ 的 O2 相当。要使用 g++ 编译代码,请使用以下命令。
g++ ForLoopBenchmark.cpp -O2 -std=c++14
Increment For Loop: 558ms, sum:500000500000 Range For Loop: 552ms, sum:500000500000 Iterator For Loop: 542ms, sum:500000500000 Accumulator: 544ms, sum:500000500000
“这些信息有价值吗?”
存在 FIX 协议(用于金融市场),它使用求和来计算消息的简单校验和。
如果您觉得 Godbolt 有用,请考虑成为 Matt Godbolt 的 patreon,以表达您的感谢并帮助他支付每月服务器费用。当然,捐赠不是强制性的。我从十二月起就一直是他的 patreon。
基准测试源代码托管 这里。感谢您的阅读!