1. 源码
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
实际上就是通过GCC 的内建函数 __builtin_expect() 进行编译优化:
long __builtin_expect (long exp, long c)
该函数是告诉编译器:参数exp 为c 的可能更大,编译器可能就会根据这个提示信息,做一些分支预测上的代码优化。
参数 c,与函数的返回值无关,无论 c 为何值,函数的返回值都是 exp
例如:
if (__builtin_expect(x, 0))
foo();
这里更希望不执行 foo函数,因为我们期盼x 表达式的值为 0。
1.2 __builtin_expect注意
- 参数c 与函数的返回值无关,无论 c 为何值,函数的返回值都为 exp;
- exp 是一个完整的表达式,返回值是该表达式的值;
- 编译时需要使用编译选项 -fprofile-arcs;
1.3 likely 和unlikely 中的!!(x)
从源码中我们看到 likyly 和unlikely 就是使用了GCC 的内建函数 __builtin_expect,但exp 是!!(x),通过两次取反实现将表达式 x 变成bool,然后与 1 和 0 作比较,告诉编译器 x 为真或为假的可能性很高。
例如:
!!(100)
第一次取反值为0,第二次取反后为 1。实际就是为了返回100 的bool 值为 true
同样:
!!(-100)
第一次取反值为0,第二次取反后为 1。实际就是为了返回-100 的bool 值为 true
2. 验证 likeyly()
#include <stdio.h>
#include <stdlib.h>
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
int main(int argc, char** argv)
{
int a = atoi(argv[1]);
if (likely(a > 0)) {
a = 0x123;
} else {
a = 0x456;
}
printf("a= 0x%x\n", a);
return 0;
}
该 demo 告诉编译器 a > 0 的可能性更大。
下面通过汇编的代码来看下编译器的分析预测:
$ gcc -fprofile-arcs -O2 -c test.c
$ objdump -S test.o
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 48 83 ec 08 sub $0x8,%rsp
8: 48 8b 7e 08 mov 0x8(%rsi),%rdi
c: 31 f6 xor %esi,%esi
e: ba 0a 00 00 00 mov $0xa,%edx
13: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 1b <main+0x1b>
1a: 01
1b: e8 00 00 00 00 call 20 <main+0x20>
20: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 28 <main+0x28>
27: 01
28: ba 23 01 00 00 mov $0x123,%edx //编译器直接下给a赋值0x123
2d: 85 c0 test %eax,%eax //先赋值,再来判断 a>0
2f: 7e 22 jle 53 <main+0x53> //使用jle指令判断当a<=0,跳转
31: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 38 <main+0x38>
38: bf 02 00 00 00 mov $0x2,%edi
3d: 31 c0 xor %eax,%eax
3f: e8 00 00 00 00 call 44 <main+0x44>
44: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 4c <main+0x4c>
4b: 01
4c: 31 c0 xor %eax,%eax
4e: 48 83 c4 08 add $0x8,%rsp
52: c3 ret
53: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 5b <main+0x5b>
5a: 01
5b: ba 56 04 00 00 mov $0x456,%edx //赋值0x456的分支在最后
60: eb cf jmp 31 <main+0x31>
62: 66 66 2e 0f 1f 84 00 data16 cs nopw 0x0(%rax,%rax,1)
69: 00 00 00 00
6d: 0f 1f 00 nopl (%rax)
编译器通过likely() 得知 a>0 的概率很大,直接先给 a 赋值0x123 了,如果不满足条件会进行跳转,将 a = 0x456 的代码分支放到了最后。
3. 验证 unlikely()
#include <stdio.h>
#include <stdlib.h>
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
int main(int argc, char** argv)
{
int a = atoi(argv[1]);
if (unlikely(a > 0)) {
a = 0x123;
} else {
a = 0x456;
}
printf("a= 0x%x\n", a);
return 0;
}
该 demo 告诉编译器 a > 0 的可能性很小,或不希望 a>0.
下面通过汇编的代码来看下编译器的分析预测:
$ gcc -fprofile-arcs -O2 -c test.c
$ objdump -S test.o
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 48 83 ec 08 sub $0x8,%rsp
8: 48 8b 7e 08 mov 0x8(%rsi),%rdi
c: 31 f6 xor %esi,%esi
e: ba 0a 00 00 00 mov $0xa,%edx
13: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 1b <main+0x1b>
1a: 01
1b: e8 00 00 00 00 call 20 <main+0x20>
20: 85 c0 test %eax,%eax //面对概率小,编译器选择先判断a>0
22: 7f 2f jg 53 <main+0x53> //使用jg指令,如果a>0,跳转
24: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 2c <main+0x2c>
2b: 01
2c: ba 56 04 00 00 mov $0x456,%edx
31: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 38 <main+0x38>
38: bf 02 00 00 00 mov $0x2,%edi
3d: 31 c0 xor %eax,%eax
3f: e8 00 00 00 00 call 44 <main+0x44>
44: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 4c <main+0x4c>
4b: 01
4c: 31 c0 xor %eax,%eax
4e: 48 83 c4 08 add $0x8,%rsp
52: c3 ret
53: 48 83 05 00 00 00 00 addq $0x1,0x0(%rip) # 5b <main+0x5b>
5a: 01
5b: ba 23 01 00 00 mov $0x123,%edx
60: eb cf jmp 31 <main+0x31>
62: 66 66 2e 0f 1f 84 00 data16 cs nopw 0x0(%rax,%rax,1)
69: 00 00 00 00
6d: 0f 1f 00 nopl (%rax)
4. 总结
- likely()、unlikely() 原理是利用了GCC 的内建函数 __builtin_expect(),通过编译器预测代码分支;
- GCC 编译器会将不希望的代码分支放最后;
- GCC 可能优先执行期盼的执行,再进行判断,不同的编译器版本实现方式不同;
- likely() 表示该表达式为 “True” 的概率大一些,unlikely() 表示改表达式为 “True” 的概率小一些;
- likely()、unlikely() 通过分支预测指令的预取能提高代码的执行效率。 但是前提在使用的过程当中程序的开发者必须对自己的代码逻辑有清晰的认识,知道什么样的逻辑会大概率执行,什么样的逻辑大概率不会执行,只有这样才能通likely,unlikely 的判断做精准的分支预测,提高程序的运行性能。
更多的GCC 内建函数可以查看:GCC 内建函数