我在循环代码中对一些计数进行基准测试。 g ++与-O2代码一起使用,我发现在50%的情况下,当某些条件为真时,它会出现一些性能问题。我假设这可能意味着代码执行不必要的跳转(因为clang产生更快的代码,因此它不是一些基本的限制)。
我在这个asm输出中发现的有趣的是代码跳过一个简单的添加。
=> 0x42b46b <benchmark_many_ints()+1659>: movslq (%rdx),%rax
0x42b46e <benchmark_many_ints()+1662>: mov %rax,%rcx
0x42b471 <benchmark_many_ints()+1665>: imul %r9,%rax
0x42b475 <benchmark_many_ints()+1669>: shr $0xe,%rax
0x42b479 <benchmark_many_ints()+1673>: and $0x1ff,%eax
0x42b47e <benchmark_many_ints()+1678>: cmp (%r10,%rax,4),%ecx
0x42b482 <benchmark_many_ints()+1682>: jne 0x42b488 <benchmark_many_ints()+1688>
0x42b484 <benchmark_many_ints()+1684>: add $0x1,%rbx
0x42b488 <benchmark_many_ints()+1688>: add $0x4,%rdx
0x42b48c <benchmark_many_ints()+1692>: cmp %rdx,%r8
0x42b48f <benchmark_many_ints()+1695>: jne 0x42b46b <benchmark_many_ints()+1659>
请注意,我的问题不是如何修复我的代码,我只是问是否有一个原因,为什么一个好的编译器在O2会生成jne指令跳过一个便宜的指令。我问,因为从what I understand可以“简单地”得到比较结果,并使用它来没有跳跃增加计数器(在我的例子中的rbx)0或1。
源代码的相关部分(来自评论中的Godbolt链接,您应该真正编辑到您的问题中)是:
const auto cnt = std::count_if(lookups.begin(), lookups.end(),[](const auto& val){
return buckets[hash_val(val)%16] == val;});
我没有检查libstdc ++标头,看看count_if
是用if() { count++; }
实现的,还是使用三元组来鼓励无分支代码。可能是有条件的。 (编译器可以选择其中之一,但三元组更有可能编译为无分支cmovcc
或setcc
。)
看起来gcc高估了使用泛型调优的代码无分支的成本。 -mtune=skylake
(由-march=skylake
隐含)为我们提供了无分支代码,无论-O2
与-O3
,还是-fno-tree-vectorize
与-ftree-vectorize
相关。 (在the Godbolt compiler explorer上,我还将计数放在一个单独的函数中,计算一个vector<int>&
,所以我们不必趟过cout
中的时间和main
代码。)
-O2
或-O3
,和O2/3 -march=haswell
或broadwell
-O2/3 -march=skylake
。那真是怪了。它发出的无分支代码与Broadwell vs. Skylake的成本相同。我想知道Skylake vs. Haswell是否支持无分支,因为更便宜的cmov
。 GCC的内部成本模型在中端优化(GIMPLE,体系结构中立表示)时并不总是与x86指令相关。它还不知道什么x86指令实际上将用于无分支序列。因此可能涉及条件选择操作,并且gcc将其设计为在Haswell上更昂贵,其中cmov
是2 uops?但我测试了-march=broadwell
并仍然得到了分支代码。希望我们可以排除假设gcc的成本模型知道Broadwell(而不是Skylake)是第一个拥有单uop cmov
,adc
和sbb
(3输入整数运算)的Intel P6 / SnB系列uarch。
我不知道gcc的Skylake调优选项还有什么能让它更喜欢这个循环的无分支代码。收集在Skylake上是有效的,但gcc是自动矢量化(使用vpgatherqd xmm
),即使使用-march=haswell
,它看起来不像是一个胜利,因为聚集是昂贵的,并且需要32x64 => 64位SIMD乘法,每个输入使用2x vpmuludq
向量。也许SKL很值得,但我怀疑HSW。也许可能是一个错过的优化,不会收回dword元素以收集两倍于vpgatherdd
的吞吐量相同的元素。
我确实排除了功能不太优化,因为它被称为main
(并标记为cold
)。通常建议不要将您的微基准标记放在main
中:至少用于优化main
的编译器(例如代码大小而不仅仅是速度)。
即使只使用-O2
,Clang确实让它无分支。
当编译器必须在分支和分支之间做出决定时,他们会使用启发式方法来猜测哪个更好。如果他们认为这是高度可预测的(例如可能大部分都没有采取),那就倾向于支持多枝。
在这种情况下,启发式可能已经确定了int
的所有2 ^ 32个可能的值,找到您正在寻找的值很少。 ==
可能已经愚弄了gcc,认为它是可以预测的。
有时,Branchy可能会更好,具体取决于循环,因为它可以破坏数据依赖性。请参阅gcc optimization flag -O3 makes code slower than -O2,了解一个非常可预测的情况,并且-O3
无分支代码生成速度较慢。
-O3
至少习惯于将条件转换为无分支序列(如cmp
)更具攻击性; lea 1(%rbx), %rcx
; cmove %rcx, %rbx
,或者在这种情况下更可能是xor
-zero / cmp
/ sete
/ add
。 (实际上gcc -march=skylake
使用的是sete
/ movzx
,这几乎是非常严重的。)
没有任何运行时分析/检测数据,这些猜测很容易出错。像这样的东西是Profile Guided Optimization闪耀的地方。用-fprofile-generate
编译,运行它,然后用-fprofile-use
编译,你可能会得到无分支代码。
BTW,这些天通常推荐-O3
。 Is optimisation level -O3 dangerous in g++?。它默认情况下不启用-funroll-loops
,所以它只会在自动矢量化时膨胀代码(特别是对于一个非常大的完全展开的标量序言/结尾,围绕一个微小的SIMD循环,这会导致循环开销出现瓶颈./ facepalm。)