为什么GCC会产生一个跳转来跳转到一条便宜的指令上,这有充分的理由吗?

时间:2018-08-31 01:34:03

标签: gcc assembly compiler-optimization

我正在对循环计数中的一些计数进行基准测试。 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

1 个答案:

答案 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++; }实现的,还是使用三元数来鼓励无分支代码的。可能是有条件的。 (编译器可以选择其中之一,但是三元数更可能编译为无分支的cmovccsetcc。)


通过通用调整,gcc似乎高估了此代码的无分支成本。 -mtune=skylake(由-march=skylake表示)为此提供了无分支代码,无论-O2-O3还是-fno-tree-vectorize-ftree-vectorize无关。 (在the Godbolt compiler explorer上,我也将计数放在单独的函数中,该函数对vector<int>&进行计数,因此我们不必费时地{{1}中的时间和cout代码生成}}。

  • 分支代码:gcc8.2 main-O2-O3O2/3 -march=haswell
  • 无分支代码:gcc8.2 broadwell

那很奇怪。它发出的无分支代码在Broadwell与Skylake上的成本相同。我想知道Skylake与Haswell是否因为便宜的-O2/3 -march=skylake而偏爱无分支。在中端进行优化时,GCC的内部成本模型并不总是以x86指令表示(在GIMPLE中,是一种与体系结构无关的表示)。它尚不知道什么x86指令实际用于无分支序列。因此,也许涉及到条件选择操作,并且gcc在Haswell上将其建模为更昂贵,其中cmov是2 oups?但是我测试了cmov,但仍然得到了分支代码。希望我们可以排除假设,假设gcc的成本模型知道Broadwell(不是Skylake)是第一个具有单uup -march=broadwellcmovadc的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-useIs optimisation level -O3 dangerous in g++?。默认情况下,它不会启用-O3,因此它仅在自动矢量化时才膨胀代码(尤其是围绕着 tiny SIMD循环会阻塞循环开销。/facepalm。)