《C语言杂记》编译优化之__builtin_expect

Posted Bruceoxl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《C语言杂记》编译优化之__builtin_expect相关的知识,希望对你有一定的参考价值。

最近在研究压缩算法的时候,看到了以下代码:

#define LIKELY(c) (__builtin_expect(!!(c), 1))
#define UNLIKELY(c) (__builtin_expect(!!(c), 0))

在好奇心的驱使下,我决定一探究竟。

1 __builtin_expect是啥

其实可以从宏定义就能猜个大概,顾名思义,LIKELY ()指“很有可能”之意,而UNLIKELY ()指“不太可能”之意。这两个有啥用处呢,可以先看看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.

For the purposes of branch prediction optimizations, the probability that a __builtin_expect expression is true is controlled by GCC’s builtin-expect-probability parameter, which defaults to 90%.

You can also use __builtin_expect_with_probability to explicitly assign a probability value to individual expressions. If the built-in is used in a loop construct, the provided probability will influence the expected number of iterations made by loop optimizations.

大概意思就是__buildin_except向编译器提供分支预测信息,根据条件跳转(JMP)的预期值,按正确地顺序生成汇编代码,把“很有可能发生”的条件分支放在顺序执行指令段,从而帮助编译器进行代码优化,但是程序中的跳转指令会打乱CPU流水线。因此,减少程序的跳转次数可以提高程序的执行效率,__buildin_except的作用也就在于此。

使用__buildin_except 定义LIKELYUNLIKELY宏,分别代表bool型变量或表达式有很大可能性为真或者很大可能性为假。

2 __builtin_expect函数说明

在Linux内核代码和一些追求效率的应用中都能看到__buildin_except的身影。

在Linux内核中的include/linux/compiler.h其宏定义如下:

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

以上两个宏定义核心就是__builtin_expect函数,因此需要先搞清楚__builtin_expect函数。

函数__builtin_expect()是GCC v2.96版本引入的,其声明如下:

long __builtin_expect(long exp, long c);

① exp,exp为一个整型表exp 为一个整型表达式,例如: (ptr != NULL)。
② c,c 必须是一个编译期常量, 不能使用变量。

以上代码中‘!!’是为了把表达式x转化成0或者1。

3 __builtin_expect使用方法

LIKELY ()UNLIKELY ()通常和if连用,下面通过实例说明。

/**
  ******************************************************************************
  * @file                main.c
  * @author              BruceOu
  * @version             V1.0
  * @date                2021-09-06
  * @blog                https://blog.bruceou.cn/
  * @Official Accounts   嵌入式实验楼
  * @brief               
  ******************************************************************************
  */
/**Include*********************************************************************/
#include <stdio.h>            

#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)

/**
  * @brief  likely
  * @param  x
  * @retval x
  */
long likely_test(long x)
{
    if(LIKELY(x))
    {
        x = 9;
    }
    else
    {
        x = 10;
    }

    return x;
}

/**
  * @brief  unlikely
  * @param  x
  * @retval x
  */
long unlikely_test(long x)
{
    if(UNLIKELY(x))
    {
        x = 9;
    }
    else
    {
        x = 10;
    }

    return x;
}

/**
  * @brief  normal
  * @param  x
  * @retval x
  */
long normal_test(long x)
{
    if(x)
    {
        x = 9;
    }
    else
    {
        x = 10;
    }

    return x;
}

/**
  * @brief  main
  * @param  None
  * @retval int
  */
int main()
{
    return 0;

}

$gcc -fprofile-arcs -O2 -c main.c

【注】参数 ‘-fprofile-arcs’ 来收集程序运行的执行流程和分支走向的实际反馈信息。

$objdump -S main.o

【注】参数 ‘S’ 表示显示源代码和反汇编代码,当然使用‘-d’也是可以的,只‘-S’参数包含‘-d’的内容,objdump的更多参数可查看附录。

反编译结果如下:

bruceou@ubuntu:~$ objdump -S main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <likely_test>:
   0:	48 83 05 00 00 00 00 	addq   $0x1,0x0(%rip)        # 8 <likely_test+0x8>
   7:	01 
## 判断传入的参数x是否为0。若结果为0,ZF=1语句je执行跳转
   8:	48 85 ff             	test   %rdi,%rdi
   b:	b8 09 00 00 00       	mov    $0x9,%eax
  10:	74 06                	je     18 <likely_test+0x18>
## 将堆栈地址从堆栈弹出到ret来返回%rip(在32位模式下返回的是%eip)
  12:	f3 c3                	repz retq 
  14:	0f 1f 40 00          	nopl   0x0(%rax)
  18:	48 83 05 00 00 00 00 	addq   $0x1,0x0(%rip)        # 20 <likely_test+0x20>
  1f:	01 
  20:	b8 0a 00 00 00       	mov    $0xa,%eax
  25:	c3                   	retq   
  26:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  2d:	00 00 00 

0000000000000030 <unlikely_test>:
  30:	48 83 05 00 00 00 00 	addq   $0x1,0x0(%rip)        # 38 <unlikely_test+0x8>
  37:	01 
  38:	48 85 ff             	test   %rdi,%rdi
  3b:	75 13                	jne    50 <unlikely_test+0x20>
  3d:	48 83 05 00 00 00 00 	addq   $0x1,0x0(%rip)        # 45 <unlikely_test+0x15>
  44:	01 
  45:	b8 0a 00 00 00       	mov    $0xa,%eax
  4a:	c3                   	retq   
  4b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
  50:	b8 09 00 00 00       	mov    $0x9,%eax
  55:	c3                   	retq   
  56:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  5d:	00 00 00 

0000000000000060 <normal_test>:
  60:	48 83 05 00 00 00 00 	addq   $0x1,0x0(%rip)        # 68 <normal_test+0x8>
  67:	01 
  68:	48 85 ff             	test   %rdi,%rdi
  6b:	b8 09 00 00 00       	mov    $0x9,%eax
  70:	75 0d                	jne    7f <normal_test+0x1f>
  72:	48 83 05 00 00 00 00 	addq   $0x1,0x0(%rip)        # 7a <normal_test+0x1a>
  79:	01 
  7a:	b8 0a 00 00 00       	mov    $0xa,%eax
  7f:	f3 c3                	repz retq 

Disassembly of section .text.startup:

0000000000000000 <main>:
   0:	48 83 05 00 00 00 00 	addq   $0x1,0x0(%rip)        # 8 <main+0x8>
   7:	01 
   8:	31 c0                	xor    %eax,%eax
   a:	c3                   	retq   
   b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

0000000000000010 <_GLOBAL__sub_I_65535_0_likely_test>:
  10:	bf 00 00 00 00       	mov    $0x0,%edi
  15:	e9 00 00 00 00       	jmpq   1a <_GLOBAL__sub_I_65535_0_likely_test+0xa>

从前面的汇编代码可以看出, normal_test()函数只有一个返回值,而优化后的函数会在赋值后立即返回,从而避免跳转。这样做的缺点是造成了代码冗余,但提升了效率。

【附】objdump命令

objdump命令是用查看目标文件或者可执行的目标文件的构成的gcc工具。objdump可以让你更多地了解二进制文件可能带有的附加信息。

objdump语法格式

objdump [参数] [文件]

objdump常用参数

参考资料




欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎


欢迎订阅我的微信公众号

以上是关于《C语言杂记》编译优化之__builtin_expect的主要内容,如果未能解决你的问题,请参考以下文章

《C语言杂记》详解extern “C“

C语言中的__attribute__宏定义之section属性

《C语言杂记》C语言异常处理之 setjmp()和longjmp()

C语言再学习 -- __attribute__详解

6个变态的C语言Hello World程序 之 雷人的程序语言

Python学习杂记_10_三元运算符