Linux 中多处出现 likely 和 unlikely 的使用,它们的定义如下:
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
函数原型
__builtin_expect 是 GCC 提供的内置函数 (built-in functions),函数原型是
long __builtin_expect (long exp, long c)
函数的返回值是 exp,它告诉编译器, 代码期望的是 exp == c 。
注意:代码中!!
,并不是特殊符号,只是两次取反,通过这种技巧,可以将任意量转换为0或1。0 为 0,非 0 值为1。
使用该函数的作用
现代处理器的工作远超前于当前指令,比如从内存读新指令,译码指令等。只要指令遵循的是简单的顺序,那么这种指令流水线化就能很好的工作。当遇到分支的时候,处理器必须猜测分支该往哪个方向走,如果猜测错误,就会损失处理器的性能,因此需要分支预测技术。
__builtin_expect 就是用来为处理器的分支预测提供信息,帮助处理器进行分支预测。
if (unlikely(x)) {
do_something();
}
return x;
编译器会让主分支是概率最大的分支,尽量减少程序跳转的情况。以上代码会被编译器调整为如下的汇编逻辑。
if(x)
goto L1;
return x;
L1:
do_something();
return x;
通过汇编代码查看编译器实际动作
int normal_fun(int x)
{
if (x) {
do_something();
}
return x;
}
int unlikely_fun(int x)
{
if(__builtin_expect(x, 0)) {
do_something();
}
return x;
}
int likely_fun(int x)
{
if(__builtin_expect(x, 1)) {
do_something();
}
return x;
}
汇编代码如下:
normal_fun:
pushq %rbx
movl %edi, %ebx
testl %edi, %edi
jne .L4 ;normal 版本采用默认的分支预测
.L2:
movl %ebx, %eax
popq %rbx
ret
.L4:
movl $0, %eax
call do_something
jmp .L2
unlikely_fun:
pushq %rbx
movl %edi, %ebx
testl %edi, %edi
jne .L8 ;使等于1的情况作为跳转分支。
.L6:
movl %ebx, %eax
popq %rbx
ret
.L8:
movl $0, %eax
call do_something
jmp .L6
likely_fun:
pushq %rbx
movl %edi, %ebx
testl %edi, %edi
je .L10 ;使等于1的情况作为主分支。
movl $0, %eax
call do_something
.L10:
movl %ebx, %eax
popq %rbx
ret
参考:
Other Built-in Functions Provided by GCC
How do the likely/unlikely macros in the Linux kernel work and what is their benefit?
GCC __builtin_expect 解析