_#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
_を使用した複雑な構文の代わりに?
以下から生成されるアセンブリコードを想像してください。
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
の場合はありそうもないので、ジャンプもありそうにないので、パイプラインをスラッシングすることはまずありません。
__builtin_expect
の考え方は、通常式がcに評価されることをコンパイラーに伝えることで、コンパイラーはその場合に最適化できるようにします。
誰かが自分が賢いと思っていて、これを行うことで物事をスピードアップしていたと思う。
残念ながら、状況が非常によく理解されている(そのようなことをしていない可能性が高い)でない限り、事態を悪化させた可能性があります。ドキュメントには次のようにも書かれています:
一般に、プログラマはプログラムの実際の動作を予測するのが悪名高いため、これには実際のプロファイルフィードバック(
-fprofile-arcs
)を使用することをお勧めします。ただし、このデータを収集するのが難しいアプリケーションがあります。
一般的に、次の場合を除き、__builtin_expect
を使用しないでください。
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と呼ばれるタイトな内部ループの一部である場合にのみ、この最適化が顕著な効果をもたらすと予想します。間違った方法で最適化すると、パフォーマンスが低下する可能性があります。
私はあなたが尋ねていたと思う質問に対処する答えのいずれも言い換えていません:
コンパイラに分岐予測を示唆する、より移植性の高い方法はありますか。
あなたの質問のタイトルは、私にそれをこのようにすることを考えさせました:
_if ( !x ) {} else foo();
_
コンパイラが「true」の可能性が高いと想定する場合、foo()
を呼び出さないように最適化できます。
ここでの問題は、一般に、コンパイラが何を想定するかを知らないことです。したがって、この種の手法を使用するコードは、慎重に測定する必要があります(コンテキストが変更された場合は、時間をかけて監視する必要があります)。
@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は最後のケースで機能します。