それらは、分岐予測がジャンプ命令の「可能性が高い」側を支持するようにする命令を発行するためのコンパイラへのヒントです。これは大きな勝利になる可能性があります。予測が正しければ、ジャンプ命令は基本的に無料であり、ゼロサイクルかかることを意味します。一方、予測が間違っている場合は、プロセッサ パイプラインをフラッシュする必要があり、数サイクルのコストがかかる可能性があることを意味します。ほとんどの場合、予測が正しい限り、これはパフォーマンスに良い傾向があります。
このようなすべてのパフォーマンスの最適化と同様に、広範なプロファイリングを行ってからコードが実際にボトルネックになっていることを確認してから実行する必要があります。これはおそらく、コードがタイトなループで実行されているというミクロの性質を考慮したものです。一般的に、Linux 開発者はかなりの経験を積んでいるので、彼らがそうしただろうと想像します。彼らは gcc のみを対象としているため、移植性についてはあまり気にしません。また、gcc で生成したいアセンブリについて非常に近い考えを持っています。
逆コンパイルして、GCC 4.8 で何ができるか見てみましょう
__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)
printf("%d\n", 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 14 jne 24 <main+0x24>
10: ba 01 00 00 00 mov $0x1,%edx
15: be 00 00 00 00 mov $0x0,%esi
16: R_X86_64_32 .rodata.str1.1
1a: bf 01 00 00 00 mov $0x1,%edi
1f: e8 00 00 00 00 callq 24 <main+0x24>
20: R_X86_64_PC32 __printf_chk-0x4
24: bf 00 00 00 00 mov $0x0,%edi
25: R_X86_64_32 .rodata.str1.1+0x4
29: e8 00 00 00 00 callq 2e <main+0x2e>
2a: R_X86_64_PC32 puts-0x4
2e: 31 c0 xor %eax,%eax
30: 48 83 c4 08 add $0x8,%rsp
34: c3 retq
メモリ内の命令順序は変更されていません。最初に printf
そして 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 11 je 21 <main+0x21>
10: bf 00 00 00 00 mov $0x0,%edi
11: R_X86_64_32 .rodata.str1.1+0x4
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
21: ba 01 00 00 00 mov $0x1,%edx
26: be 00 00 00 00 mov $0x0,%esi
27: R_X86_64_32 .rodata.str1.1
2b: bf 01 00 00 00 mov $0x1,%edi
30: e8 00 00 00 00 callq 35 <main+0x35>
31: R_X86_64_PC32 __printf_chk-0x4
35: eb d9 jmp 10 <main+0x10>
printf
(__printf_chk
にコンパイル ) は、puts
の後、関数の最後に移動されました 他の回答で述べられているように、分岐予測を改善するためのリターン。
したがって、基本的には次と同じです:
int main() {
int i = !time(NULL);
if (i)
goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
}
この最適化は -O0
では行われませんでした .
しかし、__builtin_expect
でより高速に実行される例を書いて頑張ってください。 最近の CPU は非常にスマートです。私の単純な試みはここにあります。
C++20 [[likely]]
と [[unlikely]]
C++20 はこれらの C++ ビルトインを標準化しました:if-else ステートメントで C++20 の like/unlikely 属性を使用する方法 それらはおそらく (しゃれ!) 同じことをします.
これらは、分岐がどの方向に進むかについてコンパイラーにヒントを与えるマクロです。マクロは、利用可能な場合、GCC 固有の拡張機能に展開されます。
GCC はこれらを使用して分岐予測を最適化します。たとえば、次のようなものがあるとします
if (unlikely(x)) {
dosomething();
}
return x;
次に、このコードを次のように再構築できます。
if (!x) {
return x;
}
dosomething();
return x;
これの利点は、プロセッサが最初に分岐するときに、かなりのオーバーヘッドが発生することです。これは、プロセッサが投機的にコードを読み込んで実行していた可能性があるためです。分岐を取ると判断したら、それを無効にし、分岐先から開始する必要があります。
最新のプロセッサのほとんどは、ある種の分岐予測を備えていますが、それは以前に分岐を通過したことがあり、分岐がまだ分岐予測キャッシュにある場合にのみ役立ちます。
これらのシナリオでコンパイラとプロセッサが使用できる戦略は他にも多数あります。ウィキペディアで分岐予測子の仕組みの詳細を確認できます:http://en.wikipedia.org/wiki/Branch_predictor