以下から生成されるアセンブリ コードを想像してみてください:
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
以降 ジャンプする可能性は低いため、パイプラインをスラッシングする可能性は低いです。
逆コンパイルして、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 の like/unlikely 属性を使用する方法 それらはおそらく (しゃれ!) 同じことをします.
__builtin_expect
の考え方 通常は式が c に評価されることをコンパイラに伝え、コンパイラがその場合に最適化できるようにすることです。
誰かが自分は賢いと思っていて、これを行うことで物事をスピードアップしていると思います。
残念ながら、状況が十分に理解されていない場合を除きます (彼らはそのようなことをしていない可能性が高いです)、それは事態を悪化させた可能性があります.ドキュメントには次のようにも書かれています:
<ブロック引用>
一般に、これには実際のプロファイル フィードバックを使用することをお勧めします (-fprofile-arcs
)、プログラマーは自分のプログラムが実際にどのように実行されるかを予測するのが苦手であることで有名です。ただし、このデータを収集するのが難しいアプリケーションもあります。
通常、__builtin_expect
は使用しないでください。 例外:
- 非常に深刻なパフォーマンスの問題があります
- システムのアルゴリズムを適切に最適化済み
- 特定のケースが最も可能性が高いという主張を裏付けるパフォーマンス データがあります