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

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1325  
  IPD(Integrated Product Development)流程作为一种先进的产品开发管理模式,在众多企业中得到了广泛应用。它涵盖了从产品概念产生到产品退市的整个生命周期,通过整合跨部门团队、优化流程等方式,显著提升产品开发的效率和质量,进而为项目的成功奠定坚实基础。深入探究IPD流程的五个阶段与项目成功之间...
IPD流程分为几个阶段   4  
  华为作为全球知名的科技企业,其成功背后的管理体系备受关注。IPD(集成产品开发)流程作为华为核心的产品开发管理模式,其中的创新管理与实践更是蕴含着丰富的经验和深刻的智慧,对众多企业具有重要的借鉴意义。IPD流程的核心架构IPD流程旨在打破部门墙,实现跨部门的高效协作,将产品开发视为一个整体的流程。它涵盖了从市场需求分析...
华为IPD是什么   3  
  IPD(Integrated Product Development)研发管理体系作为一种先进的产品开发模式,在众多企业的发展历程中发挥了至关重要的作用。它不仅仅是一套流程,更是一种理念,一种能够全方位提升企业竞争力,推动企业持续发展的有效工具。深入探究IPD研发管理体系如何助力企业持续发展,对于众多渴望在市场中立足并...
IPD管理流程   3  
  IPD(Integrated Product Development)流程管理旨在通过整合产品开发流程、团队和资源,实现产品的快速、高质量交付。在这一过程中,有效降低成本是企业提升竞争力的关键。通过优化IPD流程管理中的各个环节,可以在不牺牲产品质量和性能的前提下,实现成本的显著降低,为企业创造更大的价值。优化产品规划...
IPD流程分为几个阶段   4  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用