定义
1 2 #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
__builtin_expect
是编译器内建函数,原型为
long __builtin_expect (long exp, long c)
。该函数并不会改变
exp
的值,但是可以对 if-else
分支或者
if
分支结构进行优化。 likely
表示
if
分支大概率会发生,unlikely
代表
if
分支大概率不会发生。
!!
是 C 语言中处理逻辑表达式的一个技巧。因为 C
语言中没有布尔变量,所以布尔值是用整型来代替的,0 为假,非 0 为真。 当
x
为 0 时,!(x)
为 1,!!(x)
为
0,!!
的运算没有什么意义;但当 x 为非 0 时(比如
100),!(x)
为 0,!!(x)
为 1,
这样就达到了将非 0 值全部映射为 1 的效果。
应用场景
总的来说,对代码运行效率有要求的 if-else
或
if
分支就应该使用 likely
或
unlikely
优化选项。
注意事项
likely
和 unlikely
的概率判断务必准确,不要写反了,否则非但不能提升运行效率,反而会起到反作用。
选择表达式时要选择编译阶段编译器无法推测出真假的表达式,否则优化不起作用。
编译时需要至少使用 -O2
选项,否则优化不起作用。
作用原理
理论
使用 likely
或 unlikely
为什么会起到提升代码运行效率的优化效果呢?
主要的作用机理有以下 2 点:
gcc 编译器在编译生成汇编代码时会在编译选项的引导下调整
if
分支内代码的位置,如果是 likely
修饰过的就调整到前面,如果是 unlikely
修饰过的就调整到后面。
放到前面的代码可以节省跳转指令带来的时间开销,从而达到提升效率的目的。
当代 CPU 都有 ICache 和流水线机制,在运行当前这条指令时,ICache
会预读取后面的指令,以提升运行效率。但是如果条件分支的结果是跳转到了其他指令,那取的下一条指令(有的
CPU 设置的是 4 级流水,也就是 4 条指令)就没用了,
这样就降低了流水线的效率。如果使用 likely
和
unlikely
来指导编译器总是将大概率执行的代码放在靠前的位置,就可以大大提高预取值的命中率,从而达到提升效率的目的。
实践
1 2 3 4 # 编译生成a.out,注意使用-O2选项,否则不生效 gcc -O2 test.c # 根据生成的a.out生成反汇编代码 objdump -CS a.out > objdump.txt
objdump
命令是用来查看目标文件或者可执行的目标文件的构成的 gcc 工具。
-d
反汇编目标文件中包含的可执行指令。
-S
混合显示源码和汇编代码,前提是在编译目标文件时加上
-g,否则相当于 -d
-C
一般针对 C++ 语言,用来更友好地显示符号名。
不使用 likely
和
unlikely
选项
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdlib.h> #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) int main (int argc, char *argv[]) { int i = atoi(argv[1 ]); if (i > 0 ) i--; else i++; return i; }
objdump 里面的 main
:
没有跳转指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 0000000000400440 <main>: 400440 : 48 83 ec 08 sub $0 x8,%rsp 400444 : 48 8b 7e 08 mov 0x8 (%rsi),%rdi 400448 : ba 0a 00 00 00 mov $0 xa,%edx 40044d : 31 f6 xor %esi,%esi 40044f: e8 dc ff ff ff callq 400430 <strtol@plt> 400454 : 8d 50 ff lea -0x1 (%rax),%edx // %rax 是返回值寄存器,i-1 400457 : 8d 48 01 lea 0x1 (%rax),%ecx // i+1 40045a: 85 c0 test %eax,%eax // 判断 %eax 是否为 0 40045c: 0f 4e d1 cmovle %ecx,%edx // if <=, edx = ecx 40045f: 48 83 c4 08 add $0 x8,%rsp 400463 : 89 d0 mov %edx,%eax // %edx 是最终返回值 400465 : c3 retq
-0x1(%rax)
表示 %rax
的值减
1,lea -0x1(%rax),%edx
则表示 %edx
保存的是
i-1
0x1(%rax)
表示 %rax
的值加
1,lea 0x1(%rax),%ecx
则表示 %ecx
保存的是
i+1
x86 寄存器参考文档:https://www.cs.uaf.edu/2017/fall/cs301/lecture/09_11_registers.html
其他参考文档 https://web.stanford.edu/class/archive/cs/cs107/cs107.1186/lectures/14-slides.pdf
使用 likely
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdlib.h> #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) int main (int argc, char *argv[]) { int i = atoi(argv[1 ]); if (likely(i > 0 )) i--; else i++; return i; }
objdump 里面的 main
:
1 2 3 4 5 6 7 8 9 10 11 12 13 0000000000400440 <main>: 400440 : 48 83 ec 08 sub $0 x8,%rsp 400444 : 48 8b 7e 08 mov 0x8 (%rsi),%rdi 400448 : ba 0a 00 00 00 mov $0 xa,%edx 40044d : 31 f6 xor %esi,%esi 40044f: e8 dc ff ff ff callq 400430 <strtol@plt> 400454 : 85 c0 test %eax,%eax 400456 : 7e 08 jle 400460 <main+0x20 > // 小于等于的时候跳转,if 语句块在前面 400458 : 83 e8 01 sub $0 x1,%eax 40045b: 48 83 c4 08 add $0 x8,%rsp 40045f: c3 retq 400460 : 83 c0 01 add $0 x1,%eax // else 语句块在后面 400463 : eb f6 jmp 40045b <main+0x1b >
使用 likely
的时候,编译器就知道,大部分情况下,if
判断的结果会是
true
,所以只有 i <= 0
的时候才会去跳转,这样一来大部分的情况下都不用跳转。
使用 unlikely
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdlib.h> #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) int main (int argc, char *argv[]) { int i = atoi(argv[1 ]); if (unlikely(i > 0 )) i--; else i++; return i; }
objdump 里面的 main
:
1 2 3 4 5 6 7 8 9 10 11 12 13 0000000000400440 <main>: 400440 : 48 83 ec 08 sub $0 x8,%rsp 400444 : 48 8b 7e 08 mov 0x8 (%rsi),%rdi 400448 : ba 0a 00 00 00 mov $0 xa,%edx 40044d : 31 f6 xor %esi,%esi 40044f: e8 dc ff ff ff callq 400430 <strtol@plt> 400454 : 85 c0 test %eax,%eax 400456 : 7f 08 jg 400460 <main+0x20 > // 大于的时候跳转,else 语句块在前面 400458 : 83 c0 01 add $0 x1,%eax 40045b: 48 83 c4 08 add $0 x8,%rsp 40045f: c3 retq 400460 : 83 e8 01 sub $0 x1,%eax // if 语句块在后面 400463 : eb f6 jmp 40045b <main+0x1b >
使用 unlikely
的时候,编译器就知道,大部分情况下,if
判断的结果会是
false
,所以只有 i > 0
的时候才会去跳转,这样一来大部分的情况下都不用跳转。