C语言进阶——likely和unlikely

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

摘自:https://blog.csdn.net/weixin_44873133/article/details/107302688

这篇文章分析的很牛逼啊!

1、 定义

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

builtin_expect是编译器内建函数,原型为long builtin_expect (long exp, long c)。该函数并不会改变exp的值,但是可以对if-else分支或者if分支结构进行优化。likely代表if分支大概率会发生,unlikely代表if分支大概率不会发生。

Tips: !!是C语言中处理逻辑表达式的一个技巧。因为C语言中没有布尔变量,所以布尔值是用整形来代替的,0为假,非0为真。当x为0时,!(x)为1,!!(x)为0,!!的运算没有什么意义;但当x为非0时(比如100),!(x)为0,!!(x)为1,这样就达到了将非0值(比如100)全部都映射为1的效果。

通过分析发现,unlikely的定义其实是可以不使用!!运算符的。

在文件linux-5.13.7/include/linux/compiler.h找到:

 39 /*
 40  * Using __builtin_constant_p(x) to ignore cases where the return
 41  * value is always the same.  This idea is taken from a similar patch
 42  * written by Daniel Walker.
 43  */
 44 # ifndef likely
 45 #  define likely(x) (__branch_check__(x, 1, __builtin_constant_p(x)))
 46 # endif
 47 # ifndef unlikely
 48 #  define unlikely(x)   (__branch_check__(x, 0, __builtin_constant_p(x)))
 49 # endif


 77 # define likely(x)  __builtin_expect(!!(x), 1)
 78 # define unlikely(x)    __builtin_expect(!!(x), 0)
 79 # define likely_notrace(x)  likely(x)
 80 # define unlikely_notrace(x)    unlikely(x)

既然程序是我们程序员所写,在一些明确的场景下,我们应该比CPU和编译器更了解哪个分支条件更有可能被满足。我们是否可将这一先验知识告知编译器和CPU, 提高分支预测的准确率,从而减少CPU流水线分支预测错误带来的性能损失呢?答案是可以!它便是likely和unlikely。在Linux内核代码中,这两个宏的应用比比皆是。

2、应用场景

总的来说,对代码运行效率有要求的if-else或if分支就应该使用likely或unlikely优化选项。

3、注意事项

  • likely和unlikely的概率判断务必准确,不要写反了,否则非但不能提升运行效率,反而会起到反作用。
  • 选择表达式时要选择编译阶段编译器无法推测出真假的表达式,否则优化不起作用。
  • 编译时需要至少使用-O2选项,否则优化不起作用。

4、作用机理

4.1、理论

使用likely或unlikely为什么会起到提升代码运行效率的优化效果呢?

主要的作用机理有以下2点:

  • gcc编译器在编译生成汇编代码时会在编译选项的引导下调整if分支内代码的位置,如果是likely修饰过的就调整到前面,如果是unlikely修饰过的就调整到后面。放到前面的代码可以节省跳转指令带来的时间开销,从而达到提升效率的目的。
  • 当代CPU都有ICache和流水线机制,在运行当前这条指令时,ICache会预读取后面的指令,以提升运行效率。但是如果条件分支的结果是跳转到了其他指令,那预取的下一条指令(有的CPU设计是4级流水,也就是4条指令)就没用了,这样就降低了流水线的效率。如果使用likely和unlikely来指导编译器总是将大概率执行的代码放在靠前的位置,就可以大大提高预取值的命中率,从而达到提升效率的目的。

4.2、实践

下面通过一个小栗子来感受一下likely和unlikely的行为。

期间使用的工具有gcc和objdump。涉及到的指令如下:

# 编译生成a.out,注意使用-O2选项,否则不生效
gcc -O2 test.c
# 根据生成的a.out生成反汇编代码
objdump -CS a.out > objdump.txt

Tips: objdump命令是用查看目标文件或者可执行的目标文件的构成的gcc工具

(1)-d:反汇编目标文件中包含的可执行指令。 (2)-S:混合显示源码和汇编代码,前提是在编译目标文件时加上-g,否则相当于-d。 (3)-C:一般针对C++语言,用来更友好地显示符号名。

4.2.1 不使用likely或unlikely选项

通过实践发现,如果按照博主代码来做,会很难发现if-else语句在哪里,根本无法进行判断,我在这里花费了大半天的时间来研究。 最终终于明白:这个自增和自减在O2优化中被优化掉了。

两种方法可以解决:

  • 如果把变量变成全局变量就不会被优化。
  • 使用其他函数可以区分,如puts和fprintf。
/* 未使用likely或unlikely选项 */
int main(int argc, char *argv[])
{
    // 下面这句注释根本不可靠,明明GCC能优化掉
    int i = atoi(argv[1]); /* init i with the value that GCC can't optimize */
    if (i > 0) {
        i--;
    } else {
        i++;
    }
    return i;
}
0000000000400430 <main>:
  400430:       48 83 ec 08             sub    $0x8,%rsp
  400434:       48 8b 7e 08             mov    0x8(%rsi),%rdi
  400438:       31 c0                   xor    %eax,%eax
  40043a:       e8 d1 ff ff ff          callq  400410 <atoi@plt>
  40043f:       8d 48 ff                lea    -0x1(%rax),%ecx
  400442:       8d 50 01                lea    0x1(%rax),%edx
  400445:       85 c0                   test   %eax,%eax
  400447:       89 c8                   mov    %ecx,%eax
  400449:       0f 4e c2                cmovle %edx,%eax
  40044c:       48 83 c4 08             add    $0x8,%rsp
  400450:       c3                      retq
  400451:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  400458:       00 00 00
  40045b:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

使用全局变量:

/* 未使用likely或unlikely选项 */
int i;
int main(int argc, char *argv[])
{
    i = atoi(argv[1]); /* init i with the value that GCC can't optimize */

    if (i > 0) {
        i--;
    } else {
        i++;
    }
    return i;
}
0000000000400430 <main>:
  400430:       48 83 ec 08             sub    $0x8,%rsp
  400434:       48 8b 7e 08             mov    0x8(%rsi),%rdi
  400438:       31 c0                   xor    %eax,%eax
  40043a:       e8 d1 ff ff ff          callq  400410 <atoi@plt>
  40043f:       85 c0                   test   %eax,%eax
  400441:       7e 14                   jle    400457 <main+0x27>
  400443:       83 e8 01                sub    $0x1,%eax                  注意到if分支内容在前
  400446:       89 05 f0 0b 20 00       mov    %eax,0x200bf0(%rip)        # 60103c <i>
  40044c:       8b 05 ea 0b 20 00       mov    0x200bea(%rip),%eax        # 60103c <i>
  400452:       48 83 c4 08             add    $0x8,%rsp
  400456:       c3                      retq
  400457:       83 c0 01                add    $0x1,%eax                  注意到else分支内容在后
  40045a:       89 05 dc 0b 20 00       mov    %eax,0x200bdc(%rip)        # 60103c <i>
  400460:       eb ea                   jmp    40044c <main+0x1c>
  400462:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  400469:       00 00 00
  40046c:       0f 1f 40 00             nopl   0x0(%rax)

JLE/JNG: 小于或等于转移。

4.2.2 使用likely选项

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

int i;
int main(int argc, char *argv[])
{  
    i = atoi(argv[1]); /* init i with the value that GCC can't optimize */

    if (likely(i > 0)) {
        i--;
    } else {
        i++;
    }
    return i;
}
0000000000400430 <main>:
  400430:       48 83 ec 08             sub    $0x8,%rsp
  400434:       48 8b 7e 08             mov    0x8(%rsi),%rdi
  400438:       31 c0                   xor    %eax,%eax
  40043a:       e8 d1 ff ff ff          callq  400410 <atoi@plt>
  40043f:       85 c0                   test   %eax,%eax
  400441:       7e 0e                   jle    400451 <main+0x21>
  400443:       83 e8 01                sub    $0x1,%eax                  注意到if分支内容在前
  400446:       89 05 f0 0b 20 00       mov    %eax,0x200bf0(%rip)        # 60103c <i>
  40044c:       48 83 c4 08             add    $0x8,%rsp
  400450:       c3                      retq
  400451:       83 c0 01                add    $0x1,%eax                  注意到else分支内容在后
  400454:       89 05 e2 0b 20 00       mov    %eax,0x200be2(%rip)        # 60103c <i>
  40045a:       eb f0                   jmp    40044c <main+0x1c>
  40045c:       0f 1f 40 00             nopl   0x0(%rax)

4.2.3 使用unlikely选项

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

int i;
int main(int argc, char *argv[])
{  
    i = atoi(argv[1]); /* init i with the value that GCC can't optimize */

    if (unlikely(i > 0)) {
        i--;
    } else {
        i++;
    }
    return i;
}
0000000000400430 <main>:
  400430:       48 83 ec 08             sub    $0x8,%rsp
  400434:       48 8b 7e 08             mov    0x8(%rsi),%rdi
  400438:       31 c0                   xor    %eax,%eax
  40043a:       e8 d1 ff ff ff          callq  400410 <atoi@plt>
  40043f:       85 c0                   test   %eax,%eax
  400441:       7f 0e                   jg     400451 <main+0x21>
  400443:       83 c0 01                add    $0x1,%eax                  注意到else分支内容被提前了
  400446:       89 05 f0 0b 20 00       mov    %eax,0x200bf0(%rip)        # 60103c <i>
  40044c:       48 83 c4 08             add    $0x8,%rsp
  400450:       c3                      retq
  400451:       83 e8 01                sub    $0x1,%eax                  注意到if分支内容被后移了
  400454:       89 05 e2 0b 20 00       mov    %eax,0x200be2(%rip)        # 60103c <i>
  40045a:       eb f0                   jmp    40044c <main+0x1c>
  40045c:       0f 1f 40 00             nopl   0x0(%rax)

JG/JNLE: 大于转移。

5、汇编指令查询

https://baike.baidu.com/item/%E6%B1%87%E7%BC%96%E6%8C%87%E4%BB%A4/9979890?fr=aladdin

results matching ""

    No results matching ""