代码分支预测优化

代码分支预测优化

我们的代码中,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() 可以在一定程度上提高代码运行效率。