likely && unlikely in GCC

时间:2021-04-05 07:13:29

在linux内核源码或一些比较成熟的c语言架构源码中,我们常会见到类似下面的代码:

 if (unlikely(!packet)) {
return res_failed;
} // OR if (likely(packet->type = HTTP)) {
do_something();
} 有的地方可能会用大写,LIKELY() / UNLIKELY(),意思一样。

然后我们看一下unlikely和likely的定义,大部分都是类似如下的宏定义:

 #define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0) GCC 中用的是 _G_BOOLEAN_EXPR(expr) 来代替 !!(expr), 意思一样,都是把expr或x转成相应布尔变量。

两个定义无一例外调用了一个内置函数 __builtin_expect(bool expr,  int x)。

先解释一下:  LIKELY 和 UNLIKELY 不会对原expr的布尔值产生任何影响,也就是说只要expr == true, LIKELY(expr) 与 UNLIKELY(expr) 都为 true。他们起的只是编译器优化作用。

我们先测试一段代码:

 /**
* @author Lhfcws
* @file test__builtin_expect.c
* @time 2013-07-22
**/ #define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0) int test_likely(int x) {
if(LIKELY(x))
x = 0x00;
else
x = 0xff; return x;
} int test_unlikely(int x) {
if(UNLIKELY(x))
x = 0x00;
else
x = 0xff; return x;
} int test_justif(int x) {
if(x)
x = 0x00;
else
x = 0xff; return x;
}

可见,三个函数唯一的区别就是 if (x) 那里。

我们执行一下命令编译和反汇编(要用 __builtin_expect 的话  -fprofile-arcs 必须加):

gcc -fprofile-arcs -c test__builtin_expect.c -o test__builtin_expect.o
objdump -d test__builtin_expect > builtin.asm

此时打开生成的asm文件,就可以看到上面代码由gcc编译生成的汇编代码。我们截取那三个函数的汇编源码查看。

  <test_likely>:
: push %ebp
: e5 mov %esp,%ebp
: 7d cmpl $0x0,0x8(%ebp)
: 0f c0 setne %al
a: 0f b6 c0 movzbl %al,%eax
d: c0 test %eax,%eax
f: je 1a <test_likely+0x1a>
: c7 movl $0x0,0x8(%ebp)
: eb jmp 3d <test_likely+0x3d>
1a: c7 ff movl $0xff,0x8(%ebp)
: a1 mov 0x20,%eax
: 8b mov 0x24,%edx
2c: c0 add $0x1,%eax
2f: d2 adc $0x0,%edx
: a3 mov %eax,0x20
: mov %edx,0x24
3d: 8b 4d mov 0x8(%ebp),%ecx
: a1 mov 0x28,%eax
: 8b 2c mov 0x2c,%edx
4b: c0 add $0x1,%eax
4e: d2 adc $0x0,%edx
: a3 mov %eax,0x28
: 2c mov %edx,0x2c
5c: c8 mov %ecx,%eax
5e: 5d pop %ebp
5f: c3 ret <test_unlikely>:
: push %ebp
: e5 mov %esp,%ebp
: 7d cmpl $0x0,0x8(%ebp)
: 0f c0 setne %al
6a: 0f b6 c0 movzbl %al,%eax
6d: c0 test %eax,%eax
6f: je 7a <test_unlikely+0x1a>
: c7 movl $0x0,0x8(%ebp)
: eb jmp 9d <test_unlikely+0x3d>
7a: c7 ff movl $0xff,0x8(%ebp)
: a1 mov 0x10,%eax
: 8b mov 0x14,%edx
8c: c0 add $0x1,%eax
8f: d2 adc $0x0,%edx
: a3 mov %eax,0x10
: mov %edx,0x14
9d: 8b 4d mov 0x8(%ebp),%ecx
a0: a1 mov 0x18,%eax
a5: 8b 1c mov 0x1c,%edx
ab: c0 add $0x1,%eax
ae: d2 adc $0x0,%edx
b1: a3 mov %eax,0x18
b6: 1c mov %edx,0x1c
bc: c8 mov %ecx,%eax
be: 5d pop %ebp
bf: c3 ret 000000c0 <test_justif>:
c0: push %ebp
c1: e5 mov %esp,%ebp
c3: 7d cmpl $0x0,0x8(%ebp)
c7: je d2 <test_justif+0x12>
c9: c7 movl $0x0,0x8(%ebp)
d0: eb jmp f5 <test_justif+0x35>
d2: c7 ff movl $0xff,0x8(%ebp)
d9: a1 mov 0x0,%eax
de: 8b mov 0x4,%edx
e4: c0 add $0x1,%eax
e7: d2 adc $0x0,%edx
ea: a3 mov %eax,0x0
ef: mov %edx,0x4
f5: 8b 4d mov 0x8(%ebp),%ecx
f8: a1 mov 0x8,%eax
fd: 8b 0c mov 0xc,%edx
: c0 add $0x1,%eax
: d2 adc $0x0,%edx
: a3 mov %eax,0x8
10e: 0c mov %edx,0xc
: c8 mov %ecx,%eax
: 5d pop %ebp
: c3 ret

如上,我们看到,貌似test_likely 和 test_unlikely 没什么区别, test_justif就是少了setne al开始的三行代码而已(实际上是执行__builtin_expect(!!(x), 1)的代码)。

其实这证明了一件事: LIKELY 和 UNLIKELY 的调用不会影响最终结果,实际两者的结果是一样的。

我们之前提到他们起的作用是优化,因此我们编译的时候加上优化指令。

gcc -O2 -fprofile-arcs -c test__builtin_expect.c -o test__builtin_expect.o
objdump -d test__builtin_expect > builtin_O2.asm

得到汇编:

  <test_likely>:
: ec sub $0x4,%esp
: 8b mov 0x8(%esp),%eax
: addl $0x1,0x0
e: adcl $0x0,0x4
: c0 test %eax,%eax
: je 1f <test_likely+0x1f>
: c0 xor %eax,%eax
1b: c4 add $0x4,%esp
1e: c3 ret
1f: addl $0x1,0x8
: b8 ff mov $0xff,%eax
2b: 0c adcl $0x0,0xc
: eb e7 jmp 1b <test_likely+0x1b>
: 8d b6 lea 0x0(%esi),%esi
3a: 8d bf lea 0x0(%edi),%edi <test_unlikely>:
: ec sub $0x4,%esp
: 8b mov 0x8(%esp),%edx
: addl $0x1,0x10
4e: adcl $0x0,0x14
: d2 test %edx,%edx
: jne <test_unlikely+0x30>
: addl $0x1,0x18
: b8 ff mov $0xff,%eax
: 1c adcl $0x0,0x1c
6c: c4 add $0x4,%esp
6f: c3 ret
: c0 xor %eax,%eax
: eb f8 jmp 6c <test_unlikely+0x2c>
: 8d b6 lea 0x0(%esi),%esi
7a: 8d bf lea 0x0(%edi),%edi <test_justif>:
: ec sub $0x4,%esp
: 8b 4c mov 0x8(%esp),%ecx
: addl $0x1,0x20
8e: adcl $0x0,0x24
: c0 xor %eax,%eax
: c9 test %ecx,%ecx
: jne ab <test_justif+0x2b>
9b: addl $0x1,0x28
a2: b0 ff mov $0xff,%al
a4: 2c adcl $0x0,0x2c
ab: c4 add $0x4,%esp
ae: c3 ret

现在三个函数就有很明显的不同了。

留意一下每个函数其中的三行代码:

 je ... / jne ...    ; 跳转
xor %eap, %eap      ; 其实是 mov 0x00, %eap,改用 xor 是编译器自己的优化。结果等价。
mov 0xff, %eap      ; x = 0xff

可以看到,likely版本和unlikely版本最大的区别是跳转的不同。

likely版本编译器会认为执行 x == true 的可能性比较大,因此将 x == false 的情况作为分支,减少跳转开销。

同理,unlikely版本编译器会认为执行 x == false 的可能性比较大,因此将 x == true 的情况作为分支,减少跳转开销。

总结:

likely 和 unlikely 的使用实际上是为了分支优化,不影响结果,据传,众多程序员平常很少注意分支优化情况,因此gcc有了这个选项。。。

unlikely 一般适用于(但不仅限于)一些错误检查,比如本文开头示例。likely适用于主分支场景,即根据期望大部分情况都会执行的场景。