我正在对循环计数中的一些计数进行基准测试。 g ++与-O2代码一起使用,我注意到当在50%的情况下某些条件成立时,它会出现一些性能问题。我认为这可能意味着代码会进行不必要的跳转(因为clang会生成更快的代码,所以这不是一些基本的限制)。
我在这个汇编输出中发现有趣的是,代码跳过了一个简单的添加。
=> 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指令来跳过1条便宜的指令。 我问是因为,从what I understand可以“简单地”获得比较结果,并将其用于没有跳转的计数器(在我的示例中为rbx)递增0或1。
编辑:来源: https://godbolt.org/z/v0Iiv4
答案 0 :(得分:2)
来源的相关部分(来自注释中的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>&
进行计数,因此我们不必费时地{{1}中的时间和cout
代码生成}}。
main
或-O2
和-O3
或O2/3 -march=haswell
broadwell
。那很奇怪。它发出的无分支代码在Broadwell与Skylake上的成本相同。我想知道Skylake与Haswell是否因为便宜的-O2/3 -march=skylake
而偏爱无分支。在中端进行优化时,GCC的内部成本模型并不总是以x86指令表示(在GIMPLE中,是一种与体系结构无关的表示)。它尚不知道什么x86指令实际用于无分支序列。因此,也许涉及到条件选择操作,并且gcc在Haswell上将其建模为更昂贵,其中cmov
是2 oups?但是我测试了cmov
,但仍然得到了分支代码。希望我们可以排除假设,假设gcc的成本模型知道Broadwell(不是Skylake)是第一个具有单uup -march=broadwell
,cmov
和adc
的Intel P6 / SnB系列uarch (3输入整数运算)。
我不知道gcc的Skylake调整选项还有什么让它喜欢此循环的无分支代码。 Gather在Skylake上非常有效,但是gcc甚至可以在sbb
上自动矢量化(使用vpgatherqd xmm
),在这种情况下看起来不算成功,因为收集昂贵,并且需要32x64 => 64-每个输入向量使用2x -march=haswell
乘以SIMD位。 SKL也许值得,但我怀疑HSW。可能还错过了一个优化方案,那就是不压缩回dword元素以收集vpmuludq
吞吐量几乎相同的两倍的元素。
我确实排除了该函数未优化的问题,因为它被称为vpgatherdd
(并标记为main
)。通常建议不要将微基准放在cold
中:至少用于优化main
的编译器要进行不同的优化(例如,代码大小而不只是速度)。
即使只有main
,Clang也会使其无分支。
当编译器必须在分支和分支之间做出决定时,他们会采用启发式方法猜测哪种方法会更好。如果他们认为这是高度可预测的(例如,可能大部分不被采用),则倾向于分支。
在这种情况下,启发式方法可能会决定,在-O2
的所有2 ^ 32个可能值中,很少会找到您要查找的值。 int
可能欺骗了gcc认为它是可预测的。
根据循环的不同,有时随机性可能会更好,因为它可以破坏数据依赖性。请参见gcc optimization flag -O3 makes code slower than -O2,了解是可预测的,并且==
无分支代码生成速度较慢。
-O3
至少在将条件转换为-O3
之类的无分支序列时更具攻击性; cmp
; lea 1(%rbx), %rcx
,或者在这种情况下更可能是cmove %rcx, %rbx
-零/ xor
/ cmp
/ sete
。 (实际上,gcc add
使用-march=skylake
/ sete
,严格来说要差得多。)
没有任何运行时分析/检测数据,这些猜测很容易是错误的。 像这样的东西是Profile Guided Optimization的亮点。使用movzx
进行编译,运行,然后使用-fprofile-generate
进行编译,您可能会获得无分支的代码。
顺便说一句,最近通常建议使用-fprofile-use
。 Is optimisation level -O3 dangerous in g++?。默认情况下,它不会启用-O3
,因此它仅在自动矢量化时才膨胀代码(尤其是围绕着 tiny SIMD循环会阻塞循环开销。/facepalm。)