代码分支预测优化
我们的代码中,if/else
代码编译后,一个分支的汇编代码紧随前面的代码,而另一个分支的汇编代码需要使用 JMP 指令跳转才能访问到。很明显,通过 JMP 访问需要更多的时间。
在复杂程序中,有很多的 if/else
语句,又或者是有频繁调用且有 if/else
语句的函数,每秒被调用几万次。通常程序员在分支预测方面做的很糟糕,编译器又不能精准的预测每一个分支,这是 JMP 指令产生的时间浪费就会很大。
一、likely 和 unlikely
Linux 内核代码中,在条件判断语句中有很多 likely()
和 unlikely()
的调用。
1 2
| #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
|
__builtin_expect
的主要作用是帮助编译器判断条件跳转的预期值,避免因执行 jmp 跳转指令造成时间浪费。如下举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <stdlib.h> #include <stdio.h>
#define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
int main(int argc, char *argv[]) { int a;
/* 获取输入参数值(编译器不能进行优化) */ a = atoi (argv[1]);
if (unlikely (a == 2)) a++; else a--;
printf ("%d\n", a); return 0; }
|
使用 gcc -O2
进行编译,然后使用 objdump -S
反汇编,查看汇编代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 080483b0 <main>: // 开头 80483b0: 55 push %ebp 80483b1: 89 e5 mov %esp,%ebp 80483b3: 50 push %eax 80483b4: 50 push %eax 80483b5: 83 e4 f0 and $0xfffffff0,%esp // 调用atoi() 80483b8: 8b 45 08 mov 0x8(%ebp),%eax 80483bb: 83 ec 1c sub $0x1c,%esp 80483be: 8b 48 04 mov 0x4(%eax),%ecx 80483c1: 51 push %ecx 80483c2: e8 1d ff ff ff call 80482e4 <atoi@plt> 80483c7: 83 c4 10 add $0x10,%esp // 把输入值与2进行比较,即执行:“a == 2” 80483ca: 83 f8 02 cmp $0x2,%eax // -------------------------------------------------------- // 如果'a' 等于 2 (程序里面认为不太可能), 则跳转, // 否则继续执行, 从而不破坏CPU的指令执行顺序. // -------------------------------------------------------- 80483cd: 74 12 je 80483e1 <main+0x31> 80483cf: 48 dec %eax // 调用printf 80483d0: 52 push %edx 80483d1: 52 push %edx 80483d2: 50 push %eax 80483d3: 68 c8 84 04 08 push $0x80484c8 80483d8: e8 f7 fe ff ff call 80482d4 <printf@plt> // 返回0并退出. 80483dd: 31 c0 xor %eax,%eax 80483df: c9 leave 80483e0: c3 ret
|
在上面的程序中,用 likely()
替换其中的 unlikely()
,重新编译,再来看它的汇编代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 080483b0 <main>: // 开头 80483b0: 55 push %ebp 80483b1: 89 e5 mov %esp,%ebp 80483b3: 50 push %eax 80483b4: 50 push %eax 80483b5: 83 e4 f0 and $0xfffffff0,%esp // 调用atoi() 80483b8: 8b 45 08 mov 0x8(%ebp),%eax 80483bb: 83 ec 1c sub $0x1c,%esp 80483be: 8b 48 04 mov 0x4(%eax),%ecx 80483c1: 51 push %ecx 80483c2: e8 1d ff ff ff call 80482e4 <atoi@plt> 80483c7: 83 c4 10 add $0x10,%esp // -------------------------------------------------- // 如果'a' 等于 2 (程序认为很有可能), 则不跳转,继续执行, // 这样就不破坏CPU的指令执行顺序. // 只有当 a != 2 时才会发生跳转, 而这种情况,程序认为是不太可能的. // --------------------------------------------------- 80483ca: 83 f8 02 cmp $0x2,%eax 80483cd: 75 13 jne 80483e2 <main+0x32> // a++ 指令的优化 80483cf: b0 03 mov $0x3,%al // 调用printf() 80483d1: 52 push %edx 80483d2: 52 push %edx 80483d3: 50 push %eax 80483d4: 68 c8 84 04 08 push $0x80484c8 80483d9: e8 f6 fe ff ff call 80482d4 <printf@plt> // 返回0并退出. 80483de: 31 c0 xor %eax,%eax 80483e0: c9 leave 80483e1: c3 ret
|
因此,在代码中使用 likely()
和 unlikely()
可以在一定程度上提高代码运行效率。