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


144

#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



3
私はあなたの直接のコードはif ( x == 0) {} else foo();..または単にif ( x != 0 ) foo();GCCドキュメントのコードと同等であるはずだったと思います。
Nawaz 2017年

回答:


187

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

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

私はそれが次のようなものであるべきだと思います:

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

(Cコードではなく)barケースがケースに先行するような順序で命令が配置されていることがわかりfooます。ジャンプはすでにフェッチされた命令をスラッシュするため、これはCPUパイプラインをよりよく利用できます。

ジャンプが実行される前に、その下の命令(barケース)がパイプラインにプッシュされます。以来fooケースはほとんどありません、あまりにもジャンプすることはほとんどありません、したがって、パイプラインをスラッシングすることはほとんどありません。


1
それは本当にそのように機能しますか?なぜfoo定義が最初に来ないのですか?プロトタイプがある限り、関数定義の順序は関係ありません。
kingsmasher1 2009

63
これは関数の定義ではありません。これは、CPUが実行されない命令をフェッチする確率が低くなるようにマシンコードを再配置することです。
Blagovest Buyukliev

4
ああ、わかりました。つまり、確率が高いx = 0ため、バーが最初に表示されます。そしてfooは、確率(むしろ使用確率)が少ないので後で定義されますよね?
kingsmasher1 2009

1
ああ、ありがとう。それが一番の説明です。アセンブリコードは本当にトリックを作りました:)
kingsmasher1

5
これにより、CPU 分岐予測子のヒントも埋め込まれ、パイプラインが改善されます
Hasturkun

50

逆コンパイルして、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_expectCPUは、使用しない場合よりも速く実行される例を書くのに幸運です。当時のCPUは本当に賢いです。私の素朴な試みはここにあります

C ++ 20 [[likely]]および[[unlikely]]

C ++ 20はこれらのC ++ビルトインを標準化しました:if-elseステートメントでC ++ 20の見込み/非見込み属性を使用する方法彼らはおそらく(駄洒落!)同じことをするでしょう。


1
実際の最適化のために__builtin_expectを使用するlibdispatchのdispatch_once関数を確認してください。低速パスは一度だけ実行され、__ builtin_expectを利用して、高速パスを使用する必要があることを分岐予測子に通知します。高速パスは、ロックをまったく使用せずに実行されます。mikeash.com/pyblog/…–
Adam Kaplan

GCC 9.2では何の違いもないようです:gcc.godbolt.org/z/GzP6cx(実際には、すでに8.1に含まれています)
Ruslan

40

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

誰かが彼らが賢いことだと思っていて、これをすることで物事をスピードアップしていると思います。

残念ながら、状況が非常によく理解されていない限り(そのようなことを何も行っていない可能性があります)、状況が悪化した可能性があります。ドキュメンテーションは言う:

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

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

  • あなたは非常に現実的なパフォーマンスの問題を抱えています
  • システムのアルゴリズムをすでに適切に最適化している
  • 特定のケースが最も可能性が高いという主張を裏付けるパフォーマンスデータがあります

7
@Michael:それは実際には分岐予測の説明ではありません。
オリバーチャールズワース2011

3
「ほとんどのプログラマーは悪い」またはとにかくコンパイラーに劣らない。すべてのばかはforループで継続条件が真である可能性が高いことを伝えることができますが、コンパイラーもそれを知っているので、それを伝える利点はありません。何らかの理由場合は、ほとんど常に、すぐに壊れるループを書いて、あなたがPGOのためのコンパイラにプロファイルデータを提供できない場合、その後、多分プログラマは、コンパイラにはない何かを知っています。
Steve Jessop、2011

15
状況によっては、どのブランチがより可能性が高いかではなく、どのブランチが重要であるかが重要です。予期しない分岐がabort()につながる場合、可能性は問題ではなく、期待される分岐には最適化時にパフォーマンス優先度を与える必要があります。
Neowizard

1
主張の問題は、CPUが分岐確率に関して実行できる最適化は、分岐予測にかなり制限されていることです__builtin_expectこの最適化は、使用するかどうかに関係なく発生します。一方、コンパイラーは、分岐の確率に基づいて多くの最適化を実行できます。たとえば、ホットパスが隣接するようにコードを整理し、コードを移動してさらに最適化する可能性が低いか、サイズを小さくして、どの分岐をベクトル化するかを決定します。ホットパスのスケジュール設定など。
BeeOnRope 2017年

1
...開発者からの情報がなければ、それは盲目であり、中立的な戦略を選択します。開発者が確率について正しい場合(そして、多くの場合、ブランチが通常使用される/使用されないことを理解するのは簡単です)-これらの利点があります。あなたがそうでない場合は、何らかのペナルティが発生しますが、それは何とか利益よりもはるかに大きくはなく、最も重要なことに、これは何とかてCPU分岐予測を無効にしません。
BeeOnRope 2017年

13

さて、説明で述べているように、最初のバージョンは予測要素を構築に追加して、x == 0分岐がより可能性の高い分岐であることをコンパイラーに伝えます-つまり、プログラムがより頻繁に取る分岐です。

このことを念頭に置いて、コンパイラーは条件付きを最適化できるため、予期しない条件が発生した場合により多くの作業を行わなければならないという犠牲を払って、期待される条件が満たされたときに最小限の作業で済みます。

コンパイルフェーズでの条件文の実装方法と、結果のアセンブリで、1つのブランチが他のブランチよりも効率が悪い場合があるかどうかを確認します。

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


しかし、結局のところ、コンパイラによる条件のチェックがすべてです。コンパイラは常にこの分岐を想定して処理を続行しますが、一致しない場合はその後で一致するということですか?何が起こるのですか?コンパイラーの設計には、この分岐予測に関するものと、それがどのように機能するかについて、もう少し何かがあると思います。
kingsmasher1 2009

2
これは本当にミクロ最適化です。条件文がどのように実装されているかを調べます。1つのブランチに少し偏っています。架空の例として、条件がテストとアセンブリのジャンプになると仮定します。次に、ジャンプするブランチはジャンプしないブランチよりも遅いので、予想されるブランチをジャンプしないブランチにすることをお勧めします。
Kerrek SB、2011

おかげで、あなたとマイケルは同じような見解を持っていると思いますが、異なる言葉で言います:-)テストと分岐に関する正確なコンパイラの内部はここでは説明できないことを理解しています:)
kingsmasher1

インターネットを検索すれば、簡単に学ぶことができます:-)
Kerrek SB

私は大学の本compiler design - Aho, Ullmann, Sethi:-) に戻ったほうがよい
kingsmasher1

1

言い換えると、あなたが尋ねていたと思う質問に答える答えは見当たりません。

ブランチ予測をコンパイラに示唆するよりポータブルな方法はありますか?

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

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

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

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


これは、実際には、OPが最初に入力するつもりだったのとまったく同じである可能性があります(タイトルで示されているとおり)-何らかの理由で、の使用はelse投稿の本文から除外されました。
ブレントブラッドバーン

1

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.