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

2024-10-12 09:08:00
admin
原创
180
摘要:问题描述:我碰到了#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 等待软件的下一部分运行。通过将不太可能的分支推到最后,您将保持内存接近并仅在不太可能的情况下跳转。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1120  
  IPD(Integrated Product Development,集成产品开发)流程是一种广泛应用于高科技和制造业的产品开发方法论。它通过跨职能团队的紧密协作,将产品开发周期缩短,同时提高产品质量和市场成功率。在IPD流程中,CDCP(Concept Decision Checkpoint,概念决策检查点)是一个关...
IPD培训课程   75  
  研发IPD(集成产品开发)流程作为一种系统化的产品开发方法,已经在许多行业中得到广泛应用。它不仅能够提升产品开发的效率和质量,还能够通过优化流程和资源分配,显著提高客户满意度。客户满意度是企业长期成功的关键因素之一,而IPD流程通过其独特的结构和机制,能够确保产品从概念到市场交付的每个环节都围绕客户需求展开。本文将深入...
IPD流程   66  
  IPD(Integrated Product Development,集成产品开发)流程是一种以跨职能团队协作为核心的产品开发方法,旨在通过优化资源分配、提高沟通效率以及减少返工,从而缩短项目周期并提升产品质量。随着企业对产品上市速度的要求越来越高,IPD流程的应用价值愈发凸显。通过整合产品开发过程中的各个环节,IPD...
IPD项目管理咨询   76  
  跨部门沟通是企业运营中不可或缺的一环,尤其在复杂的产品开发过程中,不同部门之间的协作效率直接影响项目的成败。集成产品开发(IPD)作为一种系统化的项目管理方法,旨在通过优化流程和增强团队协作来提升产品开发的效率和质量。然而,跨部门沟通的复杂性往往成为IPD实施中的一大挑战。部门之间的目标差异、信息不对称以及沟通渠道不畅...
IPD是什么意思   70  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用