Bayangkan kode rakitan yang akan dihasilkan dari:
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
Saya kira itu harus seperti:
cmp $x, 0
jne _foo
_bar:
call bar
...
jmp after_if
_foo:
call foo
...
after_if:
Anda dapat melihat bahwa instruksi disusun sedemikian rupa sehingga bar
huruf besar mendahului foo
kasus (sebagai lawan dari kode C). Ini dapat memanfaatkan pipa CPU dengan lebih baik, karena lompatan meronta-ronta instruksi yang sudah diambil.
Sebelum lompatan dijalankan, instruksi di bawahnya (kode bar
kasus) didorong ke pipa. Sejak foo
kasusnya tidak mungkin, melompat juga tidak mungkin, karenanya meronta-ronta jalur pipa tidak mungkin terjadi.
Mari kita dekompilasi untuk melihat apa yang dilakukan GCC 4.8 dengannya
Blagovest menyebutkan inversi cabang untuk meningkatkan saluran pipa, tetapi apakah kompiler saat ini benar-benar melakukannya? Ayo cari tahu!
Tanpa __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;
}
Kompilasi dan dekompilasi dengan GCC 4.8.2 x86_64 Linux:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Keluaran:
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
Urutan instruksi dalam memori tidak berubah:pertama puts
lalu retq
kembali.
Dengan __builtin_expect
Sekarang ganti if (i)
dengan:
if (__builtin_expect(i, 0))
dan kami mendapatkan:
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
dipindahkan ke bagian paling akhir dari fungsi, retq
kembali!
Kode baru ini pada dasarnya sama dengan:
int i = !time(NULL);
if (i)
goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
Pengoptimalan ini tidak dilakukan dengan -O0
.
Tapi semoga berhasil menulis contoh yang berjalan lebih cepat dengan __builtin_expect
daripada tanpa, CPU benar-benar pintar saat itu. Upaya naif saya ada di sini.
C++20 [[likely]]
dan [[unlikely]]
C++20 telah menstandarkan C++ built-in tersebut:Cara menggunakan atribut C++20 kemungkinan/tidak mungkin dalam pernyataan if-else Kemungkinan besar (pun!) akan melakukan hal yang sama.
Gagasan __builtin_expect
adalah memberi tahu kompiler bahwa Anda biasanya akan menemukan bahwa ekspresi dievaluasi ke c, sehingga kompiler dapat mengoptimalkan untuk kasus tersebut.
Saya kira seseorang mengira mereka pintar dan mereka mempercepat dengan melakukan ini.
Sayangnya, kecuali situasinya dipahami dengan sangat baik (kemungkinan mereka tidak melakukan hal seperti itu), itu mungkin memperburuk keadaan. Dokumentasi bahkan mengatakan:
Secara umum, Anda sebaiknya memilih untuk menggunakan umpan balik profil aktual untuk ini (
-fprofile-arcs
), karena pemrogram terkenal buruk dalam memprediksi kinerja program mereka sebenarnya. Namun, ada aplikasi yang sulit mengumpulkan data ini.
Secara umum, Anda tidak boleh menggunakan __builtin_expect
kecuali:
- Anda memiliki masalah kinerja yang sangat nyata
- Anda telah mengoptimalkan algoritme dalam sistem dengan tepat
- Anda memiliki data kinerja untuk mendukung pernyataan Anda bahwa kemungkinan besar kasus tertentu