Linux内核入门-- likely和unlikely

1. 引言

最近在看VFS的源码,发现在if语句中大量使用了likely和unlikely这两个宏,就像以下代码所示:

int error = path_lookupat(nd, flags | LOOKUP_DIRECTORY, &path);
if (unlikely(error))
	return error;

经过学习后,知道调用likely()或unlikely()告诉编译器这个条件很有可能或者不太有可能发生,好让编译器对这个条件判断进行正确地优化。这两个宏的定义如下:

# define likely(x)	__builtin_expect(!!(x), 1)
# define unlikely(x)	__builtin_expect(!!(x), 0)

从宏的定义可以看出likely和unlikely都是使用__builtin_expect来实现的,其中__builtin_expect是GCC提供的內建函数,用于给GCC提供分支预测优化信息。

2. __builtin_expect內建函数

2.1 功能说明

GCC文档中对于函数的说明如下:
— Built-in Function: long  __builtin_expect (long exp, long c)
You may use  __builtin_expect to provide the compiler with branch prediction information. In general, you should prefer to use actual profile feedback for this (-fprofile-arcs), as programmers are notoriously bad at predicting how their programs actually perform. However, there are applications in which this data is hard to collect.
The return value is the value of exp, which should be an integral expression. The semantics of the built-in are that it is expected that exp == c. For example:
if (__builtin_expect (x, 0))
foo ();
indicates that we do not expect to call foo, since we expect x to be zero. Since you are limited to integral expressions for exp, you should use constructions such as
if (__builtin_expect (ptr != NULL, 1))
foo (*ptr);
when testing pointer or floating-point values.

【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料icon-default.png?t=M85Bhttps://link.zhihu.com/?target=https%3A//docs.qq.com/doc/DUGZVQk1qWVBHTEl3

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂是不是学完操作系统原理后觉得纸上谈兵不过瘾?是不是面对浩若烟海的Linux内核源代码迷失在代码的海洋里不知所措?这门课可以带您用理论结合实践的方法一步一步抓住Linux内核最核心的部分代码,理解Linux操作系统运行的基本过程及涉及的核心机制。icon-default.png?t=M85Bhttps://ke.qq.com/course/4032547?flowToken=1044374

3. 优化原理

3.1 避免指令跳转!!!

我们知道目前的CPU无一例外的都引入了流水线技术,用于加快指令的执行,提高CPU的性能。换句话说,就是CPU在处理当前指令的同时,会先取出后面的多条指令进行预处理,如下图所示:

I486拥有五级流水线。分别是:取指(Fetch),译码(D1, main decode),转址(D2, translate),执行(EX, execute),写回(WB)。某个指令可以在流水线的任何一级。
从上图可见,流水线将原本串行执行的指令变成了并行执行了,极大的提高了指令的执行效率。但是如果存在跳转指令,那么之前预取的指令都没有用了,需要从内存中重新取出跳转后的指令继续执行。因此跳转指令会降低流水线的效率,从而降低CPU的性能。
我们在编写程序时要尽量避免跳转指令,让指令尽可能的顺序执行。

那么,我们如何避免跳转指令呢?可以使用前面所说的__builtin_expect函数。

3.2 编译器的优化工作

从GCC的说明中可知,__builtin_expect的主要作用就是:帮助编译器判断条件跳转的预期值,避免因执行jmp跳转指令造成时间浪费。那么它是怎么帮助编译器进行优化的呢?
编译器优化时,根据条件跳转的预期值,按正确地顺序生成汇编代码,把“很有可能发生”的条件分支放在顺序执行指令段,而不是jmp指令段(jmp指令会打乱CPU的指令执行顺序,大大影响CPU指令执行效率)。

likely和unlikely主要用于if语句中,我们知道if语句分为3种形式:

  1. if
if (condition) {
	statement;
}

if-else

if (condition) {
	statement;
} else {
	statement;
}

if-else-if

if (condition) {
	statement;
} else if {
	statement;
} else {
	statement;
}

其中,if-else-if目前还没有遇到这种情况下会用到likely和unlikely的,后面遇到的话再进行研究。本文主要研究前两种情况。

3.2.1 if

下面举例说明。下面这个简单的C程序使用gcc -O2进行编译。

#include <stdio.h>
#include <stdlib.h>
 
#define likely(x)    __builtin_expect(!!(x), 1)
#define unlikely(x)  __builtin_expect(!!(x), 0)
 
int main(int argc, char *argv[])
{
    int a;
 
    /*获取GCC无法优化的值*/
    a  = atoi(argv[1]);
 
    if (likely(a != 2)) {
        a++;
    }
    
    printf("%d\n", a);
 
    return 0;
}

使用objdump -S反汇编,查看它的汇编代码。

100000000000400510 <main>:
  // 调用atoi()
  400510:	48 83 ec 08          	sub    $0x8,%rsp
  400514:	48 8b 7e 08          	mov    0x8(%rsi),%rdi
  400518:	ba 0a 00 00 00       	mov    $0xa,%edx
  40051d:	31 f6                	xor    %esi,%esi
  40051f:	e8 ec fe ff ff       	callq  400410 <strtol@plt>
 
  // 测试 a != 2的值
  400524:	83 f8 02             	cmp    $0x2,%eax
  400527:	89 c6                	mov    %eax,%esi
 
  // 如果a等于2的话,就会跳转;如果a不等于2的话,就会继续执行,不会破坏
  // CPU的指令执行顺序
  400529:	74 03                	je     40052e <main+0x1e>
 
  // a++;
  40052b:	83 c6 01             	add    $0x1,%esi
 
  // 调用printf
  40052e:	bf 48 06 40 00       	mov    $0x400648,%edi
  400533:	31 c0                	xor    %eax,%eax
  400535:	e8 b6 fe ff ff       	callq  4003f0 <printf@plt>
  40053a:	31 c0                	xor    %eax,%eax
  40053c:	48 83 c4 08          	add    $0x8,%rsp
  400540:	c3                   	retq   
  400541:	90                   	nop

在上面程序中,用unlikely()代替其中的likely(),重新编译,再来看它的汇编代码:

0000000000400510 <main>:
  // 调用atoi()
  400510:	48 83 ec 08          	sub    $0x8,%rsp
  400514:	48 8b 7e 08          	mov    0x8(%rsi),%rdi
  400518:	ba 0a 00 00 00       	mov    $0xa,%edx
  40051d:	31 f6                	xor    %esi,%esi
  40051f:	e8 ec fe ff ff       	callq  400410 <strtol@plt>
 
  // 测试a != 2的值
  400524:	83 f8 02             	cmp    $0x2,%eax
  400527:	89 c6                	mov    %eax,%esi
 
  // 如果a不等于2的话,就会跳转;如果a等于2的话,就会继续执行,不会破坏
  // CPU的指令执行顺序
  400529:	75 13                	jne    40053e <main+0x2e>
 
  // 调用printf
  40052b:	bf 48 06 40 00       	mov    $0x400648,%edi
  400530:	31 c0                	xor    %eax,%eax
  400532:	e8 b9 fe ff ff       	callq  4003f0 <printf@plt>
  400537:	31 c0                	xor    %eax,%eax
  400539:	48 83 c4 08          	add    $0x8,%rsp
  40053d:	c3                   	retq   
  
  // a++;
  40053e:	83 c6 01             	add    $0x1,%esi
  400541:	eb e8                	jmp    40052b <main+0x1b>
  400543:	90                   	nop

3.2.2 if-else

下面举例说明。下面这个简单的C程序使用gcc -O2进行编译。

#include <stdio.h>
#include <stdlib.h>
 
#define likely(x)    __builtin_expect(!!(x), 1)
#define unlikely(x)  __builtin_expect(!!(x), 0)
 
int main(int argc, char *argv[])
{
    int a;
 
    /*获取GCC无法优化的值*/
    a  = atoi(argv[1]);
 
    if (likely(a==2)) {
        a++;
    } else {
        a--;
    }
 
    printf("%d\n", a);
 
    return 0;
}

使用objdump -S反汇编,查看它的汇编代码。

0000000000400510 <main>:
  // 调用atoi
  400510:	48 83 ec 08          	sub    $0x8,%rsp
  400514:	48 8b 7e 08          	mov    0x8(%rsi),%rdi
  400518:	ba 0a 00 00 00       	mov    $0xa,%edx
  40051d:	31 f6                	xor    %esi,%esi
  40051f:	e8 ec fe ff ff       	callq  400410 <strtol@plt>
 
  // 测试 a == 2 的值
  400524:	83 f8 02             	cmp    $0x2,%eax
 
  // 如果 a == 2的话,会继续执行a++指令,不会进行跳转,这样就不会
  // 破坏CPU的预读处理。只有当a != 2时会进行跳转到400541处执行a--。
  400527:	75 18                	jne    400541 <main+0x31>
  
  // 因为a == 2,则gcc编译器将a++直接优化成0x3
  400529:	be 03 00 00 00       	mov    $0x3,%esi
 
  // 调用printf
  40052e:	bf 48 06 40 00       	mov    $0x400648,%edi
  400533:	31 c0                	xor    %eax,%eax
  400535:	e8 b6 fe ff ff       	callq  4003f0 <printf@plt>
  40053a:	31 c0                	xor    %eax,%eax
  40053c:	48 83 c4 08          	add    $0x8,%rsp
  400540:	c3                   	retq   
 
  // a--;
  400541:	8d 70 ff             	lea    -0x1(%rax),%esi
  400544:	eb e8                	jmp    40052e <main+0x1e>
 
  400546:	90                   	nop

在上面程序中,用unlikely()代替其中的likely(),重新编译,再来看它的汇编代码:

0000000000400510 <main>:
  // 调用atoi
  400510:	48 83 ec 08          	sub    $0x8,%rsp
  400514:	48 8b 7e 08          	mov    0x8(%rsi),%rdi
  400518:	ba 0a 00 00 00       	mov    $0xa,%edx
  40051d:	31 f6                	xor    %esi,%esi
  40051f:	e8 ec fe ff ff       	callq  400410 <strtol@plt>
 
  // 测试 a == 2 的值
  400524:	83 f8 02             	cmp    $0x2,%eax
 
  // 如果 a == 2的话,会进行跳转到40053f处执行a++。而当 a!= 2时顺序执行
  // a--指令,不会进行跳转,这样就不会破坏CPU的预读处理。
  400527:	74 16                	je     40053f <main+0x2f>
 
  // a--;
  400529:	8d 70 ff             	lea    -0x1(%rax),%esi
 
  // 调用printf
  40052c:	bf 48 06 40 00       	mov    $0x400648,%edi
  400531:	31 c0                	xor    %eax,%eax
  400533:	e8 b8 fe ff ff       	callq  4003f0 <printf@plt>
  400538:	31 c0                	xor    %eax,%eax
  40053a:	48 83 c4 08          	add    $0x8,%rsp
  40053e:	c3                   	retq   
 
  // a++; 因为a == 2,则gcc编译器将a++直接优化成0x3
  40053f:	be 03 00 00 00       	mov    $0x3,%esi
  400544:	eb e6                	jmp    40052c <main+0x1c>
  400546:	90                   	nop

可见,编译器利用程序员作出的判断,生成了高效的汇编码。即,跳转语句不生效的概率很大。

4. 如何使用

在一个条件判断语句中,当这个条件被认为是非常非常有可能满足时,则使用likely()宏,否则,条件非常非常不可能或很难满足时,则使用unlikely()宏。

原文链接:https://cosysn.github.io/2017/01/14/introduction-to-linux-kernel-2-likey-and-unlieky/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值