GCC会在一条廉价指令上生成跳转到跳转,这有什么好的理由吗?

GCC会在一条廉价指令上生成跳转到跳转,这有什么好的理由吗?,gcc,assembly,compiler-optimization,Gcc,Assembly,Compiler Optimization,我在测试循环代码中的一些计数。 g++与-O2代码一起使用,我注意到当某些条件在50%的情况下为真时,它会出现一些性能问题。我认为这可能意味着代码会进行不必要的跳转(因为clang会生成更快的代码,所以这不是一些基本限制) 我在这个asm输出中发现有趣的是,代码跳过了一个简单的add => 0x42b46b <benchmark_many_ints()+1659>: movslq (%rdx),%rax 0x42b46e <benchmark_many_

我在测试循环代码中的一些计数。 g++与-O2代码一起使用,我注意到当某些条件在50%的情况下为真时,它会出现一些性能问题。我认为这可能意味着代码会进行不必要的跳转(因为clang会生成更快的代码,所以这不是一些基本限制)

我在这个asm输出中发现有趣的是,代码跳过了一个简单的add

=> 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>
=>0x42b46b:movslq(%rdx),%rax
0x42b46e:mov%rax,%rcx
0x42b471:imul%r9,%rax
0x42b475:shr$0xe,%rax
0x42b479:和$0x1ff,%eax
0x42b47e:cmp(%r10,%rax,4),%ecx
0x42b482:jne 0x42b488
0x42b484:添加$0x1,%rbx
0x42b488:添加$0x4,%rdx
0x42b48c:cmp%rdx,%r8
0x42b48f:jne 0x42b46b
请注意,我的问题不是如何修复我的代码,我只是想问为什么O2上的一个好编译器会生成jne指令来跳过1条便宜的指令。 我这样问是因为我们可以“简单地”得到比较结果,并使用它将计数器(在我的例子中是rbx)增加0或1

编辑:来源:

来源的相关部分(来自评论中的一个Godbolt链接,您应该将其编辑到您的问题中)是:

我没有检查libstdc++头以查看
count\u if
是否是用
if(){count++}
实现的,或者它是否使用三元来鼓励无分支代码。可能是有条件的。(编译器可以选择其中一种,但三元更可能编译为无分支的
cmovcc
setcc


gcc似乎高估了使用通用调优的代码的无分支成本
-mtune=skylake
(由
-march=skylake
)为我们提供了无分支代码,而不管
-O2
-O3
,或
-fno树向量化与
-ftree向量化
。(在上,我还将计数放在一个单独的函数中,该函数对一个
向量进行计数&
,因此我们不必费力地在
main
中完成计时和
cout
code gen)

  • 布兰奇代码:gcc8.2
    -O2
    -O3
    ,以及
    O2/3-march=haswell
    broadwell
  • 无分支代码:gcc8.2
    -O2/3-march=skylake
真奇怪。它发出的无分支代码与Broadwell和Skylake的成本相同。我想知道Skylake和Haswell是否因为更便宜的cmov而倾向于无分支。GCC的内部成本模型在中间端进行优化时并不总是以x86指令为依据(GIMPLE是一种架构中立的表示)。它还不知道在无分支序列中实际会使用哪些x86指令。因此,可能涉及一个条件选择操作,gcc将其建模为Haswell上的成本更高,
cmov
is 2 UOP?但是我测试了
-march=broadwell
,仍然得到了branchy代码。如果gcc的成本模型知道Broadwell(而不是Skylake)是第一个采用单uop
cmov
adc
sbb
(3输入整数运算)的Intel P6/SnB系列uarch,我们希望可以排除这种情况

我不知道gcc的Skylake调优选项还有什么其他优点,使它更喜欢这个循环的无分支代码。Gather在Skylake上是高效的,但gcc是自动矢量化的(使用
vpgatherqd xmm
),即使使用
-march=haswell
,在这种情况下,Gather看起来并不是一个赢家,因为Gather价格昂贵,并且要求每个输入向量使用2x
vpmuludq
进行32x64=>64位SIMD乘法。也许SKL值得,但我怀疑HSW。对于
vpgatherdd
,也可能遗漏了一个优化,即不打包回dword元素,以收集两倍数量的元素,且吞吐量几乎相同

我确实排除了优化程度较低的函数,因为它被称为
main
(并标记为
cold
)。通常建议不要将微基准放在
main
中:编译器至少用于以不同方式优化
main
(例如,用于代码大小而不仅仅是速度)


即使只有
-O2
,叮当声也会使它失去分支


当编译器必须在分支和分支之间做出决定时,他们会使用试探法来猜测哪个更好。如果他们认为这是高度可预测的(例如,可能大部分没有采取),那就倾向于布兰奇

在这种情况下,启发式算法可以决定在
int
的所有2^32个可能值中,很少能准确地找到您要查找的值。
=
可能愚弄了gcc,使其认为它是可预测的

Branchy有时会更好,这取决于循环,因为它可以打破数据依赖关系。有关非常可预测的情况,请参阅,
-O3
无分支代码生成速度较慢

-O3
至少在将条件句转换成无分支序列(如
cmp
)时更具攻击性<代码>lea 1(%rbx),%rcx
cmove%rcx,%rbx
,或者在这种情况下更可能是
xor
-zero/
cmp
/
sete
/
add
。(实际上,gcc
-march=skylake
使用的是
sete
/
movzx
,严格来说,情况更糟。)

如果没有任何运行时分析/检测数据,这些猜测很容易出错像这样的东西是配置文件引导优化的亮点。使用
-fprofile-generate
编译,运行它,然后使用
-fprofile-use
编译,您可能会得到无分支代码


顺便说一句,这些天通常推荐使用-O3。默认情况下,它不启用
-funroll循环
,因此仅
const auto cnt = std::count_if(lookups.begin(), lookups.end(),[](const auto& val){
    return buckets[hash_val(val)%16] == val;});