有没有一个很好的理由为什么GCC会跳过跳过一个便宜的指令?

问题描述 投票:0回答:1

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

编辑:来源:https://godbolt.org/z/v0Iiv4

gcc assembly compiler-optimization
1个回答
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>&,所以我们不必趟过cout中的时间和main代码。)

  • 分支代码:gcc8.2 -O2-O3,和O2/3 -march=haswellbroadwell
  • 无分支代码:gcc8.2 -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 cmovadcsbb(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,这些天通常推荐-O3Is optimisation level -O3 dangerous in g++?。它默认情况下不启用-funroll-loops,所以它只会在自动矢量化时膨胀代码(特别是对于一个非常大的完全展开的标量序言/结尾,围绕一个微小的SIMD循环,这会导致循环开销出现瓶颈./ facepalm。)

© www.soinside.com 2019 - 2024. All rights reserved.