0%

C 的 likely 和 unlikely 的作用

定义

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-elseif 分支就应该使用 likelyunlikely 优化选项。

注意事项

  • likelyunlikely 的概率判断务必准确,不要写反了,否则非但不能提升运行效率,反而会起到反作用。
  • 选择表达式时要选择编译阶段编译器无法推测出真假的表达式,否则优化不起作用。
  • 编译时需要至少使用 -O2 选项,否则优化不起作用。

作用原理

理论

使用 likelyunlikely 为什么会起到提升代码运行效率的优化效果呢?

主要的作用机理有以下 2 点:

  • gcc 编译器在编译生成汇编代码时会在编译选项的引导下调整 if 分支内代码的位置,如果是 likely 修饰过的就调整到前面,如果是 unlikely 修饰过的就调整到后面。 放到前面的代码可以节省跳转指令带来的时间开销,从而达到提升效率的目的。
  • 当代 CPU 都有 ICache 和流水线机制,在运行当前这条指令时,ICache 会预读取后面的指令,以提升运行效率。但是如果条件分支的结果是跳转到了其他指令,那取的下一条指令(有的 CPU 设置的是 4 级流水,也就是 4 条指令)就没用了, 这样就降低了流水线的效率。如果使用 likelyunlikely 来指导编译器总是将大概率执行的代码放在靠前的位置,就可以大大提高预取值的命中率,从而达到提升效率的目的。

实践

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++ 语言,用来更友好地显示符号名。

不使用 likelyunlikely 选项

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 $0x8,%rsp
400444: 48 8b 7e 08 mov 0x8(%rsi),%rdi
400448: ba 0a 00 00 00 mov $0xa,%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 $0x8,%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 $0x8,%rsp
400444: 48 8b 7e 08 mov 0x8(%rsi),%rdi
400448: ba 0a 00 00 00 mov $0xa,%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 $0x1,%eax
40045b: 48 83 c4 08 add $0x8,%rsp
40045f: c3 retq
400460: 83 c0 01 add $0x1,%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 $0x8,%rsp
400444: 48 8b 7e 08 mov 0x8(%rsi),%rdi
400448: ba 0a 00 00 00 mov $0xa,%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 $0x1,%eax
40045b: 48 83 c4 08 add $0x8,%rsp
40045f: c3 retq
400460: 83 e8 01 sub $0x1,%eax // if 语句块在后面
400463: eb f6 jmp 40045b <main+0x1b>

使用 unlikely 的时候,编译器就知道,大部分情况下,if 判断的结果会是 false,所以只有 i > 0 的时候才会去跳转,这样一来大部分的情况下都不用跳转。