GCC 的 __builtin_expect 在 if else 语句中有什么优势?

2024-10-12 09:08:00
admin
原创
80
摘要:问题描述:我碰到了#define他们使用的__builtin_expect。文档说:内置函数:long __builtin_expect (long exp, long c)您可以使用__builtin_expect为编译器提供分支预测信息。一般来说,您应该更喜欢为此使用实际配置文件反馈(-fprofile-...

问题描述:

我碰到了#define他们使用的__builtin_expect

文档说:

内置函数:long __builtin_expect (long exp, long c)

您可以使用__builtin_expect为编译器提供分支预测信息。一般来说,您应该更喜欢为此使用实际配置文件反馈(-fprofile-arcs),因为众所周知,程序员在预测程序实际执行情况方面表现不佳。但是,有些应用程序中很难收集这些数据。

返回值为 的值exp,应为整数表达式。内置函数的语义是期望
exp == c。例如:

      if (__builtin_expect (x, 0))
        foo ();

表明我们不希望调用foo,因为我们预计x为零。

那么为什么不直接使用:

if (x)
    foo ();

而不是使用复杂的语法__builtin_expect


解决方案 1:

想象一下从下面生成的汇编代码:

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}

我想应该是这样的:

  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:

您可以看到,指令的排列顺序是bar先执行案例,后执行foo案例(与 C 代码相反)。这样可以更好地利用 CPU 流水线,因为跳转会破坏已经获取的指令。

在执行跳转指令之前,其下方的指令(barcase)会被推送到流水线。由于foocase 不太可能发生,因此跳转指令也不太可能发生,因此流水线不太可能发生混乱。

解决方案 2:

让我们反编译一下,看看 GCC 4.8 如何处理它

Blagovest 提到了分支反转来改善流水线,但是当前的编译器真的能做到这一点吗?让我们来一探究竟!

没有__builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        puts("a");
    return 0;
}

使用GCC 4.8.2 x86_64 Linux进行编译和反编译:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

输出:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 0a                   jne    1a <main+0x1a>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq

内存中的指令顺序保持不变:首先是puts,然后retq返回。

__builtin_expect

现在替换if (i)为:

if (__builtin_expect(i, 0))

我们得到:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 07                   je     17 <main+0x17>
  10:       31 c0                   xor    %eax,%eax
  12:       48 83 c4 08             add    $0x8,%rsp
  16:       c3                      retq
  17:       bf 00 00 00 00          mov    $0x0,%edi
                    18: R_X86_64_32 .rodata.str1.1
  1c:       e8 00 00 00 00          callq  21 <main+0x21>
                    1d: R_X86_64_PC32       puts-0x4
  21:       eb ed                   jmp    10 <main+0x10>

puts移到了函数的最末尾,retq返回!

新代码基本相同:

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

此优化未通过 完成-O0

但祝你好运,写出一个运行速度__builtin_expect比没有运行速度更快的例子,现在的 CPU 真的很聪明。我的天真尝试如下。

C++20[[likely]][[unlikely]]

C++20 已经标准化了这些 C++ 内置函数:如何在 if-else 语句中使用 C++20 的 likely/unlikely 属性它们很可能(双关语!)做同样的事情。

解决方案 3:

这个想法__builtin_expect是告诉编译器你通常会发现表达式的计算结果为 c,以便编译器可以针对这种情况进行优化。

我猜有人认为他们很聪明,认为这样做可以加快进程。

不幸的是,除非情况得到很好的理解(他们很可能没有这样做),否则情况可能会变得更糟。文档甚至说:

一般来说,您应该更愿意为此使用实际的配置文件反馈(-fprofile-arcs),因为众所周知,程序员不善于预测他们的程序的实际性能。然而,有些应用程序中这些数据很难收集。

一般而言,您不应使用,__builtin_expect除非:

  • 你有一个非常现实的性能问题

  • 您已经对系统中的算法进行了适当的优化

  • 你有性能数据来支持你的断言,即某个特定情况最有可能

解决方案 4:

嗯,正如描述中所说,第一个版本在构造中添加了一个预测元素,告诉编译器该x == 0分支是更有可能的分支 - 也就是说,它是您的程序更频繁采用的分支。

考虑到这一点,编译器可以优化条件,以便在预期条件成立时需要最少的工作,但在出现意外情况时可能需要做更多的工作。

查看在编译阶段以及在生成的程序集中条件是如何实现的,以了解一个分​​支的工作量为何可能比另一个分支少。

但是,我仅当所讨论的条件是被多次调用的紧密内循环的一部分时,才会期望这种优化产生明显的效果因为结果代码的差异相对较小。如果你以错误的方式对其进行优化,则可能会降低性能。

解决方案 5:

我没有看到任何答案可以解决我认为你要问的问题,解释如下:

是否有一种更可移植的方法来向编译器提示分支预测。

你问题的标题让我想到这样做:

if ( !x ) {} else foo();

如果编译器认为“真”的可能性更大,那么它可以优化不调用foo()

这里的问题是,一般来说,你不知道编译器会假设什么——所以任何使用这种技术的代码都需要仔细测量(并且如果上下文发生变化,可能需要随着时间的推移进行监控)。

解决方案 6:

我按照@Blagovest Buyukliev 和@Ciro 的建议在 Mac 上进行了测试。汇编看起来很清晰,我添加了注释;

命令是
gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

当我使用 -O3 时,无论 __builtin_expect(i, 0) 是否存在,它看起来都一样。

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp     
0000000000000001    movq    %rsp, %rbp    // open function stack
0000000000000004    xorl    %edi, %edi       // set time args 0 (NULL)
0000000000000006    callq   _time      // call time(NULL)
000000000000000b    testq   %rax, %rax   // check time(NULL)  result
000000000000000e    je  0x14           //  jump 0x14 if testq result = 0, namely jump to puts
0000000000000010    xorl    %eax, %eax   //  return 0   ,  return appear first 
0000000000000012    popq    %rbp    //  return 0
0000000000000013    retq                     //  return 0
0000000000000014    leaq    0x9(%rip), %rdi  ## literal pool for: "a"  // puts  part, afterwards
000000000000001b    callq   _puts
0000000000000020    xorl    %eax, %eax
0000000000000022    popq    %rbp
0000000000000023    retq

使用 -O2 编译时,有和没有 __builtin_expect(i, 0) 看起来不同

首先没有

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    jne 0x1c       //   jump to 0x1c if not zero, then return
0000000000000010    leaq    0x9(%rip), %rdi ## literal pool for: "a"   //   put part appear first ,  following   jne 0x1c
0000000000000017    callq   _puts
000000000000001c    xorl    %eax, %eax     // return part appear  afterwards
000000000000001e    popq    %rbp
000000000000001f    retq

现在有了 __builtin_expect(i, 0)

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    je  0x14   // jump to 0x14 if zero  then put. otherwise return 
0000000000000010    xorl    %eax, %eax   // return appear first 
0000000000000012    popq    %rbp
0000000000000013    retq
0000000000000014    leaq    0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b    callq   _puts
0000000000000020    jmp 0x10

总而言之,__builtin_expect 在最后一种情况下有效。

解决方案 7:

在大多数情况下,您应该保持分支预测原样,而不必担心它。

一个有益的情况是具有大量分支的 CPU 密集型算法。在某些情况下,跳转可能会导致超出当前 CPU 程序缓存,从而使 CPU 等待软件的下一部分运行。通过将不太可能的分支推到最后,您将保持内存接近并仅在不太可能的情况下跳转。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用