开发人员可以使用__builtin_expect
内置函数来帮助编译器了解分支可能走向哪个方向.
将来,我们可能会为此目的获得一个标准属性,但截至今天至少全部clang
,icc
并gcc
支持非标准属性__builtin_expect
.
但是,icc
当你使用它时,似乎会生成奇怪的代码1.也就是说,无论使用哪个方向进行预测,使用内置函数的代码都严格地比没有内置代码的代码更糟糕.
以下面的玩具功能为例:
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条指令.
另一方面,当你在循环条件下使用likely
或unlikely
宏时,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; }
这个循环在功能上等同于前一个循环(因为__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),情况也是如此.因此,无论提示如何,代码生成都严格地更糟,并且无论实际的运行时行为如何.
使用提示时,既gcc
不会clang
降低也不会有任何降级.
那是怎么回事?
1至少在我试过的第一个和后续的例子中.