GCC 的 __builtin_expect 在 if else 语句中有什么优势?
- 2024-10-12 09:08:00
- admin 原创
- 80
问题描述:
我碰到了#define
他们使用的__builtin_expect
。
文档说:
内置函数:
long __builtin_expect (long exp, long c)
您可以使用
__builtin_expect
为编译器提供分支预测信息。一般来说,您应该更喜欢为此使用实际配置文件反馈(-fprofile-arcs
),因为众所周知,程序员在预测程序实际执行情况方面表现不佳。但是,有些应用程序中很难收集这些数据。返回值为 的值
exp
,应为整数表达式。内置函数的语义是期望
exp == c
。例如:if (__builtin_expect (x, 0)) foo ();
表明我们不希望调用
foo
,因为我们预计x
为零。
那么为什么不直接使用:
if (x)
foo ();
而不是使用复杂的语法__builtin_expect
?
解决方案 1:
想象一下从下面生成的汇编代码:
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
我想应该是这样的:
cmp $x, 0
jne _foo
_bar:
call bar
...
jmp after_if
_foo:
call foo
...
after_if:
您可以看到,指令的排列顺序是bar
先执行案例,后执行foo
案例(与 C 代码相反)。这样可以更好地利用 CPU 流水线,因为跳转会破坏已经获取的指令。
在执行跳转指令之前,其下方的指令(bar
case)会被推送到流水线。由于foo
case 不太可能发生,因此跳转指令也不太可能发生,因此流水线不太可能发生混乱。
解决方案 2:
让我们反编译一下,看看 GCC 4.8 如何处理它
Blagovest 提到了分支反转来改善流水线,但是当前的编译器真的能做到这一点吗?让我们来一探究竟!
没有__builtin_expect
#include "stdio.h"
#include "time.h"
int main() {
/* Use time to prevent it from being optimized away. */
int i = !time(NULL);
if (i)
puts("a");
return 0;
}
使用GCC 4.8.2 x86_64 Linux进行编译和反编译:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
输出:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 75 0a jne 1a <main+0x1a>
10: bf 00 00 00 00 mov $0x0,%edi
11: R_X86_64_32 .rodata.str1.1
15: e8 00 00 00 00 callq 1a <main+0x1a>
16: R_X86_64_PC32 puts-0x4
1a: 31 c0 xor %eax,%eax
1c: 48 83 c4 08 add $0x8,%rsp
20: c3 retq
内存中的指令顺序保持不变:首先是puts
,然后retq
返回。
和__builtin_expect
现在替换if (i)
为:
if (__builtin_expect(i, 0))
我们得到:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 74 07 je 17 <main+0x17>
10: 31 c0 xor %eax,%eax
12: 48 83 c4 08 add $0x8,%rsp
16: c3 retq
17: bf 00 00 00 00 mov $0x0,%edi
18: R_X86_64_32 .rodata.str1.1
1c: e8 00 00 00 00 callq 21 <main+0x21>
1d: R_X86_64_PC32 puts-0x4
21: eb ed jmp 10 <main+0x10>
被puts
移到了函数的最末尾,retq
返回!
新代码基本相同:
int i = !time(NULL);
if (i)
goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
此优化未通过 完成-O0
。
但祝你好运,写出一个运行速度__builtin_expect
比没有运行速度更快的例子,现在的 CPU 真的很聪明。我的天真尝试如下。
C++20[[likely]]
和[[unlikely]]
C++20 已经标准化了这些 C++ 内置函数:如何在 if-else 语句中使用 C++20 的 likely/unlikely 属性它们很可能(双关语!)做同样的事情。
解决方案 3:
这个想法__builtin_expect
是告诉编译器你通常会发现表达式的计算结果为 c,以便编译器可以针对这种情况进行优化。
我猜有人认为他们很聪明,认为这样做可以加快进程。
不幸的是,除非情况得到很好的理解(他们很可能没有这样做),否则情况可能会变得更糟。文档甚至说:
一般来说,您应该更愿意为此使用实际的配置文件反馈(
-fprofile-arcs
),因为众所周知,程序员不善于预测他们的程序的实际性能。然而,有些应用程序中这些数据很难收集。
一般而言,您不应使用,__builtin_expect
除非:
你有一个非常现实的性能问题
您已经对系统中的算法进行了适当的优化
你有性能数据来支持你的断言,即某个特定情况最有可能
解决方案 4:
嗯,正如描述中所说,第一个版本在构造中添加了一个预测元素,告诉编译器该x == 0
分支是更有可能的分支 - 也就是说,它是您的程序更频繁采用的分支。
考虑到这一点,编译器可以优化条件,以便在预期条件成立时需要最少的工作,但在出现意外情况时可能需要做更多的工作。
查看在编译阶段以及在生成的程序集中条件是如何实现的,以了解一个分支的工作量为何可能比另一个分支少。
但是,我仅当所讨论的条件是被多次调用的紧密内循环的一部分时,才会期望这种优化产生明显的效果,因为结果代码的差异相对较小。如果你以错误的方式对其进行优化,则可能会降低性能。
解决方案 5:
我没有看到任何答案可以解决我认为你要问的问题,解释如下:
是否有一种更可移植的方法来向编译器提示分支预测。
你问题的标题让我想到这样做:
if ( !x ) {} else foo();
如果编译器认为“真”的可能性更大,那么它可以优化不调用foo()
。
这里的问题是,一般来说,你不知道编译器会假设什么——所以任何使用这种技术的代码都需要仔细测量(并且如果上下文发生变化,可能需要随着时间的推移进行监控)。
解决方案 6:
我按照@Blagovest Buyukliev 和@Ciro 的建议在 Mac 上进行了测试。汇编看起来很清晰,我添加了注释;
命令是 gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o
当我使用 -O3 时,无论 __builtin_expect(i, 0) 是否存在,它看起来都一样。
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp // open function stack
0000000000000004 xorl %edi, %edi // set time args 0 (NULL)
0000000000000006 callq _time // call time(NULL)
000000000000000b testq %rax, %rax // check time(NULL) result
000000000000000e je 0x14 // jump 0x14 if testq result = 0, namely jump to puts
0000000000000010 xorl %eax, %eax // return 0 , return appear first
0000000000000012 popq %rbp // return 0
0000000000000013 retq // return 0
0000000000000014 leaq 0x9(%rip), %rdi ## literal pool for: "a" // puts part, afterwards
000000000000001b callq _puts
0000000000000020 xorl %eax, %eax
0000000000000022 popq %rbp
0000000000000023 retq
使用 -O2 编译时,有和没有 __builtin_expect(i, 0) 看起来不同
首先没有
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 xorl %edi, %edi
0000000000000006 callq _time
000000000000000b testq %rax, %rax
000000000000000e jne 0x1c // jump to 0x1c if not zero, then return
0000000000000010 leaq 0x9(%rip), %rdi ## literal pool for: "a" // put part appear first , following jne 0x1c
0000000000000017 callq _puts
000000000000001c xorl %eax, %eax // return part appear afterwards
000000000000001e popq %rbp
000000000000001f retq
现在有了 __builtin_expect(i, 0)
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 xorl %edi, %edi
0000000000000006 callq _time
000000000000000b testq %rax, %rax
000000000000000e je 0x14 // jump to 0x14 if zero then put. otherwise return
0000000000000010 xorl %eax, %eax // return appear first
0000000000000012 popq %rbp
0000000000000013 retq
0000000000000014 leaq 0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b callq _puts
0000000000000020 jmp 0x10
总而言之,__builtin_expect 在最后一种情况下有效。
解决方案 7:
在大多数情况下,您应该保持分支预测原样,而不必担心它。
一个有益的情况是具有大量分支的 CPU 密集型算法。在某些情况下,跳转可能会导致超出当前 CPU 程序缓存,从而使 CPU 等待软件的下一部分运行。通过将不太可能的分支推到最后,您将保持内存接近并仅在不太可能的情况下跳转。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件