Mereka memberi petunjuk kepada kompiler untuk mengeluarkan instruksi yang akan menyebabkan prediksi cabang mendukung sisi "kemungkinan" dari instruksi lompat. Ini bisa menjadi kemenangan besar, jika prediksi benar itu berarti instruksi lompat pada dasarnya gratis dan akan memakan waktu nol siklus. Di sisi lain, jika prediksi salah, berarti pipa prosesor perlu dibilas dan dapat menghabiskan beberapa siklus. Selama sebagian besar waktu prediksi benar, ini akan cenderung bagus untuk performa.
Seperti semua pengoptimalan kinerja semacam itu, Anda hanya boleh melakukannya setelah pembuatan profil ekstensif untuk memastikan kode benar-benar berada dalam hambatan, dan mungkin mengingat sifat mikronya, bahwa kode dijalankan dalam putaran yang ketat. Umumnya pengembang Linux cukup berpengalaman jadi saya membayangkan mereka akan melakukannya. Mereka tidak terlalu peduli tentang portabilitas karena mereka hanya menargetkan gcc, dan mereka memiliki gagasan yang sangat dekat tentang rakitan yang ingin mereka hasilkan.
Mari kita dekompilasi untuk melihat apa yang dilakukan GCC 4.8 dengannya
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)
printf("%d\n", 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 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
Urutan instruksi dalam memori tidak berubah:pertama printf
lalu puts
dan 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 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
(dikompilasi ke __printf_chk
) dipindahkan ke akhir fungsi, setelah puts
dan pengembalian untuk meningkatkan prediksi cabang seperti yang disebutkan oleh jawaban lain.
Jadi pada dasarnya sama dengan:
int main() {
int i = !time(NULL);
if (i)
goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
}
Pengoptimalan ini tidak dilakukan dengan -O0
.
Tapi semoga berhasil menulis contoh yang berjalan lebih cepat dengan __builtin_expect
daripada tanpa, CPU benar-benar pintar akhir-akhir ini. 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.
Ini adalah makro yang memberi petunjuk kepada kompiler tentang ke mana arah cabang. Makro diperluas ke ekstensi khusus GCC, jika tersedia.
GCC menggunakan ini untuk mengoptimalkan prediksi cabang. Misalnya, jika Anda memiliki sesuatu seperti berikut
if (unlikely(x)) {
dosomething();
}
return x;
Kemudian dapat merestrukturisasi kode ini menjadi sesuatu yang lebih seperti:
if (!x) {
return x;
}
dosomething();
return x;
Keuntungan dari hal ini adalah ketika prosesor mengambil cabang untuk pertama kalinya, ada overhead yang signifikan, karena mungkin secara spekulatif memuat dan mengeksekusi kode lebih jauh ke depan. Ketika ditentukan akan mengambil cabang, maka harus membatalkannya, dan mulai dari target cabang.
Sebagian besar prosesor modern sekarang memiliki semacam prediksi cabang, tetapi itu hanya membantu jika Anda telah melalui cabang sebelumnya, dan cabang tersebut masih dalam cache prediksi cabang.
Ada sejumlah strategi lain yang dapat digunakan kompiler dan prosesor dalam skenario ini. Anda dapat menemukan detail lebih lanjut tentang cara kerja prediktor cabang di Wikipedia:http://en.wikipedia.org/wiki/Branch_predictor