web-dev-qa-db-ja.com

if elseステートメントでのGCCの__builtin_expectの利点は何ですか?

_#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_を使用した複雑な構文の代わりに?

128
kingsmasher1

以下から生成されるアセンブリコードを想像してください。

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パイプラインをよりよく利用できます。

ジャンプが実行される前に、その下の命令(barケース)がパイプラインにプッシュされます。 fooの場合はありそうもないので、ジャンプもありそうにないので、パイプラインをスラッシングすることはまずありません。

161

__builtin_expectの考え方は、通常式がcに評価されることをコンパイラーに伝えることで、コンパイラーはその場合に最適化できるようにします。

誰かが自分が賢いと思っていて、これを行うことで物事をスピードアップしていたと思う。

残念ながら、状況が非常によく理解されている(そのようなことをしていない可能性が高い)でない限り、事態を悪化させた可能性があります。ドキュメントには次のようにも書かれています:

一般に、プログラマはプログラムの実際の動作を予測するのが悪名高いため、これには実際のプロファイルフィードバック(-fprofile-arcs)を使用することをお勧めします。ただし、このデータを収集するのが難しいアプリケーションがあります。

一般的に、次の場合を除き、__builtin_expectを使用しないでください。

  • 非常に現実的なパフォーマンスの問題がある
  • システム内のアルゴリズムはすでに適切に最適化されています
  • 特定のケースが最も可能性が高いという主張をバックアップするためのパフォーマンスデータがあります。
40
Michael Kohne

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は本当に賢いです 。私の素朴な試み はこちら です。

説明で述べているように、最初のバージョンでは、予測要素を構成に追加し、x == 0ブランチは、より可能性の高いブランチです。つまり、プログラムがより頻繁に使用するブランチです。

それを念頭に置いて、コンパイラは条件を最適化して、予想される条件が満たされたときに必要な作業量を最小限に抑えることができます。

コンパイルフェーズおよび結果のアセンブリで条件がどのように実装されるかを見て、1つのブランチが他のブランチよりも作業が少ないかどうかを確認してください。

ただし、結果のコードの差が比較的小さいため、問題の条件がlotと呼ばれるタイトな内部ループの一部である場合にのみ、この最適化が顕著な効果をもたらすと予想します。間違った方法で最適化すると、パフォーマンスが低下する可能性があります。

13
Kerrek SB

私はあなたが尋ねていたと思う質問に対処する答えのいずれも言い換えていません:

コンパイラに分岐予測を示唆する、より移植性の高い方法はありますか。

あなたの質問のタイトルは、私にそれをこのようにすることを考えさせました:

_if ( !x ) {} else foo();
_

コンパイラが「true」の可能性が高いと想定する場合、foo()を呼び出さないように最適化できます。

ここでの問題は、一般に、コンパイラが何を想定するかを知らないことです。したがって、この種の手法を使用するコードは、慎重に測定する必要があります(コンテキストが変更された場合は、時間をかけて監視する必要があります)。

1
nobar

@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は最後のケースで機能します。

0
Victor Choy