为什么icc无法以合理的方式处理编译时分支提示?

开发人员可以使用__builtin_expect 内置来帮助编译器了解分支可能走向哪个方向。

在将来,我们可能会为此目的获得一个标准属性 ,但截至今天,至少所有clangiccgcc支持非标准的__builtin_expect

但是,当你使用它时, icc似乎会生成奇怪的代码1 。 也就是说,无论使用哪个方向进行预测,使用内置函数的代码都严格地比没有内置代码的代码更糟糕。

以下面的玩具function为例:

 int foo(int a, int b) { do { a *= 77; } while (b-- > 0); return a * 77; } 

在三个编译器中, icc是唯一一个将其编译为3个指令的最佳标量循环的编译器:

 foo(int, int): ..B1.2: # Preds ..B1.2 ..B1.1 imul edi, edi, 77 #4.6 dec esi #5.12 jns ..B1.2 # Prob 82% #5.18 imul eax, edi, 77 #6.14 ret 

gcc和Clang都管理错过了简单的解决方案并使用5条指令。

另一方面,当你在循环条件中使用likelyunlikely宏时, icc完全是脑死亡:

 #define likely(x) __builtin_expect((x), 1) #define unlikely(x) __builtin_expect((x), 0) int foo(int a, int b) { do { a *= 77; } while (likely(b-- > 0)); return a * 77; } 

这个循环在function上等同于前一个循环(因为__builtin_expect只返回它的第一个参数),但icc产生了一些可怕的代码 :

 foo(int, int): mov eax, 1 #9.12 ..B1.2: # Preds ..B1.2 ..B1.1 xor edx, edx #9.12 test esi, esi #9.12 cmovg edx, eax #9.12 dec esi #9.12 imul edi, edi, 77 #8.6 test edx, edx #9.12 jne ..B1.2 # Prob 95% #9.12 imul eax, edi, 77 #11.15 ret #11.15 

该函数的大小增加了一倍,达到了10条指令,并且(更糟糕的是!)关键循环增加了一倍多,达到7条指令,其中一条长关键依赖链涉及cmov和其他奇怪的东西。

如果您使用unlikely提示,并且也使用Godbolt支持的所有icc版本(13,14,17),情况也是如此。 因此,无论提示如何,代码生成都严格地更糟,并且无论实际的运行时行为如何。

当使用提示时, gccclang都不会遭受任何降级。

那是怎么回事?


1至少在我试过的第一个和后续的例子中。

对我而言,这似乎是一个ICC错误。 此代码( 可在godbolt上获得 )

 int c; do { a *= 77; c = b--; } while (likely(c > 0)); 

只使用辅助局部变量c ,产生没有edx = !!(esi > 0)模式的输出

 foo(int, int): ..B1.2: mov eax, esi dec esi imul edi, edi, 77 test eax, eax jg ..B1.2 

但仍然不是最佳的(它可以没有eax )。

我不知道关于__builtin_expect的官方ICC政策是完全支持还是兼容支持 。


这个问题似乎更适合官方ICC论坛 。
我试过在那里发布这个话题,但我不确定我做得好(我被SO宠坏了)。
如果他们回答我,我会更新这个答案。

编辑
我在英特尔论坛得到了答案,他们在跟踪系统中记录了这个问题。
就像今天一样,这似乎是个错误。

不要让说明欺骗你。 重要的是表现。

考虑这个相当粗糙的测试:

 #include "stdafx.h" #include  #include  int foo(int a, int b) { do { a *= 7; } while (b-- > 0); return a * 7; } int fooA(int a, int b) { __asm { mov esi, b mov edi, a mov eax, a B1: imul edi, edi, 7 dec esi jns B1 imul eax, edi, 7 } } int fooB(int a, int b) { __asm { mov esi, b mov edi, a mov eax, 1 B1: xor edx, edx test esi, esi cmovg edx, eax dec esi imul edi, edi, 7 test edx, edx jne B1 imul eax, edi, 7 } } int main() { DWORD start = GetTickCount(); int j = 0; for (int aa = -10; aa < 10; aa++) { for (int bb = -500; bb < 15000; bb++) { j += foo(aa, bb); } } std::cout << "foo compiled (/Od)\n" << "j = " << j << "\n" << GetTickCount() - start << "ms\n\n"; start = GetTickCount(); j = 0; for (int aa = -10; aa < 10; aa++) { for (int bb = -500; bb < 15000; bb++) { j += fooA(aa, bb); } } std::cout << "optimal scalar\n" << "j = " << j << "\n" << GetTickCount() - start << "ms\n\n"; start = GetTickCount(); j = 0; for (int aa = -10; aa < 10; aa++) { for (int bb = -500; bb < 15000; bb++) { j += fooB(aa, bb); } } std::cout << "use likely \n" << "j = " << j << "\n" << GetTickCount() - start << "ms\n\n"; std::cin.get(); return 0; } 

产生输出:

foo编译(/ Od)
j = -961623752
4422ms

最佳标量
j = -961623752
1656ms

使用可能
j = -961623752
1641ms

这自然完全取决于CPU(在Haswell i7上进行测试),但是当在一系列输入上进行测试时,两个asm循环的性能通常非常相似。 其中很多与指令的选择和排序有关,这有助于利用CPU中的指令流水线(延迟),分支预测和其他硬件优化。

优化时的真正教训是,您需要进行分析 - 通过检查原始组件来执行此操作非常困难。

即使在likely(b-- >0)进行具有挑战性的测试,在三分之一的时间内也是如此:

 for (int aa = -10000000; aa < 10000000; aa++) { for (int bb = -3; bb < 9; bb++) { j += fooX(aa, bb); } } 

结果是 :

foo编译(/ Od):1844ms

最佳标量:906ms

使用可能:1187ms

哪个不错。 你必须记住的是,编译器通常会在没有干扰的情况下尽力而为。 使用__builtin_expect等应该真正限制在您拥有已分析的现有代码并且您已明确指定为热点以及具有管道或预测问题的情况下。 这个简单的例子是一个理想的情况,编译器几乎肯定会在没有你帮助的情况下做正确的事情。

通过包含__builtin_expect你要求编译器必须以不同的方式编译 - 一种更复杂的方式,就纯指令数而言,但更智能的方式是它以一种有助于CPU更好的方式构造程序集分支预测。 在这种纯寄存器播放的情况下(如本例所示)并没有太多的利害关系,但如果它在更复杂的循环中改进预测,可能会为您节省错误的错误预测,缓存未命中以及相关的附带损害,那么它可能值得使用。

我认为至少在这里很清楚,当分支实际可能时,我们几乎恢复了最佳循环的全部性能(我认为这是令人印象深刻的)。 在“最佳循环”相当复杂且不那么简单的情况下,我们可以预期codegen确实会提高分支预测率(这实际上就是这一点)。 我认为这是一个例子, 如果你不需要它,不要使用它。


关于likelyunlikely生成相同程序集的主题,这并不意味着编译器被破坏 - 它只是意味着相同的代码是有效的,无论分支是主要采用还是大部分不采用 - 只要它是大多数 情况下 ,它是好的( 在这种情况下 )。 codegen旨在优化指令流水线的使用并协助分支预测。 虽然我们看到上述混合情况下性能有所下降,但推动循环最unlikely恢复性能。

 for (int aa = -10000000; aa < 10000000; aa++) { for (int bb = -30; bb < 1; bb++) { j += fooX(aa, bb); } } 

foo编译(/ Od):2453ms

最佳标量:1968ms

使用可能:2094ms