关于gcc的__builtin_expect分支预测优化

在阅读 sylar框架源代码上看到了使用 __builtin_expect 的宏定义,一时有所不解,于是查找了很多资料,对并其做整理。

Linux 下的很多代码我们经常看到 likey() 和 unlikely() 这两个宏,通常这两个宏定义是如下这样的形式:

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

可以看出这两个宏都是使用了函数 __builtin_expect 来实现的,这两个函数是 GCC 的一个内建函数(build-in function),在 GCC2.96 版本中引入的,其声明如下:

long __builtin_expect(long exp, long n);

参数:exp 是一个整型表达式,如 (ptr != NULL),n 必是一个编译器常量,是我们期望的 exp 表达式的值。

返回值:返回值只等于第一个参数 exp 的表达式结果。

作用:这个函数主要是帮助我们处理 if 的分支预测。当你期望 exp 表达式的值等于常量 n 时,那么执行 if 分支的概率很大,否则执行 else 分支的概率很大,这个概率关系到编译器的优化,下面会介绍。

下面先测试该函数的的返回值:

int a = 10;
cout << __builtin_expect(a, 1) << endl; //输出:10

a = 100;
cout << __builtin_expect((a == 100), 0) << endl;  //输出:1

可以看到该函数的返回值仅仅时对 exp 表达式的结果值。__builtin_expect 这个宏定义其实对表达式本身的值是没有影响的,而主要的作用就是在汇编层优化我们的代码,减少跳转的次数。

我们再看 likely 和 unlikely 宏:

第一个参数是 !!(x) 其作用主要是将表达式 (x) 变为一个 bool 值,不论表达式 (x) 是多少,最后的结果就是 true 或 false.

因为 likely 和 unlikely 实际调用就是调用 __builtin_expect,而他们的第二个参数中 likely 是 1,而 unlikely 是 0,也就是告诉编译器优化执行哪个分支的概率很大。

我们通过如下程序,来进一步说明:

int test_likely(int x)
{
    if (LIKELY(x))
        x = 5;
    else
        x = 6;
    return x;
}

int test_unlikely(int x)
{
    if (UNLIKELY(x))
        x = 5;
    else
        x = 6;
    return x;
}

int test(int x)
{
    if (x)
        x = 5;
    else
        x = 6;
    return x;
}

test 函数就是一个 if-else 分支条件,我们正常使用都是没有什么问题,但是如果想要追求极致,我们从这段代码中发现一个问题,像这种判断都是有偏向性的,比如使用 if-else 结构时经常通过 else 补充一些以外的情况,这个时候其实更多的执行是在 if 里面的,也因此我们发现了这种偏向性可以优化的点。在上面这段代码中:

如果大部分情况落在 x = 5 时,我们采用 test_likely 中的解决办法。

当大部分情况落在 x = 6 时,我们采用 test_unlikely 。

这里可能有读者有一定疑问,为什么这样就能减少跳转的次数,下面说一下其原因。

流水线引入 CPU,可以提高 CPU 的效率,也就是 CPU 可以预先取出下一条指令,可以减少 CPU 等待取指令的耗时,从而提高 CPU 的效率。如果存在跳转指令,预先取出的指令就无用了。CPU 在执行当前指令时,从内存中取出当了当前指令的下一条指令。执行完当前的指令后,CPU 发现不是要执行下一条指令,而是执行 offset 偏移处的指令,CPU 只能重新从内存中取出 offset 偏移处的指令。所以跳转指令会降低流水线的效率。

在写程序时,尽量避免跳转语句,如果避免也就是使用 __builtin_expect。这个指令时 gcc 引入的,作用就是:允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N),其意思就是 EXP == N 的概率很大,而且一般会将其封装为两个 LIKELY 和 UNLIKELY 宏,上面已经说过。

关于其为什么会减少跳转次数,可以看上面实例程序的汇编代码,使用编译命令:

gcc -fprofile-arcs -O2 -c test_builtin_expect.c
objdump -d test_builtin_expect.o

输出的汇编代码如下:

<test_likely>:
    push     %ebp
    mov      %esp,%ebp
    mov      0x8(%ebp),%eax
    addl     $0x1,0x38
    adcl     $0x0,0x3c
    test     %eax,%eax
    jz       2d <test_likely+0x2d>    //主要看这里:此处的效果是eax不为零时,不需要跳转。即x为真是不跳转。
    addl     $0x1,0x40
    mov      $0x5,%eax
    adcl     $0x0,0x44
    pop      %ebp
    ret
    addl     $0x1,0x48
    mov      $0x6,%eax
    adcl     $0x0,0x4c
    pop      %ebp
    ret
    lea      0x0(%esi,%eiz,1),%esi
    lea      0x0(%edi,%eiz,1),%edi

<test_unlikely>:
    push     %ebp
    mov      %esp,%ebp
    mov      0x8(%ebp),%edx
    addl     $0x1,0x20
    adcl     $0x0,0x24
    test     %edx,%edx
    jne      7d <test_unlikely+0x2d>   //主要看这里:此处的效果是edx为零时,不需跳转。即x为假时不跳转。
    addl     $0x1,0x30
    mov      $0x6,%eax
    adcl     $0x0,0x34
    pop      %ebp
    ret
    addl     $0x1,0x28
    mov      $0x5,%eax
    adcl     $0x0,0x2c
    pop      %ebp
    ret
    lea      0x0(%esi,%eiz,1),%esi
    lea      0x0(%edi,%eiz,1),%edi 

可见编译器通过 __builtin_expect 做出了判断,优化并生成了高效的代码,也就是同样能达到目的但有很大概率不会跳转语句。

参考:
__builtin_expect — 分支预测优化
__builtin_expect详解
GCC __builtin_expect的作用

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_peak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值