Anda adalah korban gagal prediksi cabang .
Apa itu Prediksi Cabang?
Pertimbangkan persimpangan jalan kereta:
Gambar oleh Mecanismo, via Wikimedia Commons. Digunakan di bawah lisensi CC-By-SA 3.0 .
Sekarang demi argumen, anggaplah ini kembali pada 1800-an - sebelum komunikasi jarak jauh atau radio.
Anda adalah operator persimpangan dan Anda mendengar kereta datang. Anda tidak tahu ke mana harus pergi. Anda menghentikan kereta untuk bertanya kepada pengemudi ke arah mana mereka inginkan. Dan kemudian Anda mengatur sakelar dengan tepat.
Kereta berat dan banyak inersia. Jadi mereka butuh selamanya untuk memulai dan memperlambat.
Apakah ada cara yang lebih baik? Anda menebak ke arah mana kereta akan pergi!
- Jika Anda menebak dengan benar, itu berlanjut.
- Jika Anda salah menebak, kapten akan berhenti, mundur, dan berteriak kepada Anda untuk membalik sakelar. Kemudian dapat memulai kembali di jalur lain.
Jika Anda menebak dengan benar setiap waktu , kereta tidak akan pernah berhenti.
Jika Anda salah menebak terlalu sering , kereta akan menghabiskan banyak waktu untuk berhenti, mencadangkan, dan memulai kembali.
Pertimbangkan pernyataan if: Pada level prosesor, ini adalah instruksi cabang:
Anda adalah prosesor dan Anda melihat cabang. Anda tidak tahu ke mana akan pergi. Apa yang kamu kerjakan? Anda menghentikan eksekusi dan menunggu hingga instruksi sebelumnya selesai. Kemudian Anda melanjutkan jalan yang benar.
Prosesor modern rumit dan memiliki jaringan pipa yang panjang. Jadi mereka butuh selamanya untuk "pemanasan" dan "melambat".
Apakah ada cara yang lebih baik? Anda menebak ke arah mana cabang akan pergi!
- Jika Anda menebak dengan benar, Anda terus mengeksekusi.
- Jika Anda salah menebak, Anda perlu menyiram pipa dan kembali ke cabang. Kemudian Anda dapat memulai kembali jalan lain.
Jika Anda menebak dengan benar setiap kali , eksekusi tidak akan pernah berhenti.
Jika Anda salah menebak terlalu sering , Anda menghabiskan banyak waktu untuk menunda, memutar kembali, dan memulai kembali.
Ini adalah prediksi cabang. Saya akui itu bukan analogi terbaik karena kereta hanya bisa memberi sinyal arah dengan bendera. Tetapi di komputer, prosesor tidak tahu ke arah mana cabang akan pergi sampai saat terakhir.
Jadi, bagaimana menurut Anda secara strategis untuk meminimalkan berapa kali kereta harus naik dan turun ke jalur lain? Anda melihat sejarah masa lalu! Jika kereta pergi ke kiri 99% dari waktu, maka Anda menebak ke kiri. Jika itu bergantian, maka Anda mengubah tebakan Anda. Jika berjalan satu arah setiap tiga kali, Anda menebak yang sama ...
Dengan kata lain, Anda mencoba mengidentifikasi suatu pola dan mengikutinya. Ini kurang lebih bagaimana alat prediksi cabang bekerja.
Sebagian besar aplikasi memiliki cabang yang berperilaku baik. Jadi prediktor cabang modern biasanya akan mencapai> 90% hit rate. Tetapi ketika dihadapkan dengan cabang yang tidak dapat diprediksi tanpa pola yang dapat dikenali, prediktor cabang hampir tidak berguna.
Bacaan lebih lanjut: artikel "Prediktor cabang" di Wikipedia .
Seperti yang diisyaratkan dari atas, pelakunya adalah pernyataan if ini:
if (data[c] >= 128)
sum += data[c];
Perhatikan bahwa data terdistribusi secara merata antara 0 dan 255. Ketika data diurutkan, kira-kira setengah dari iterasi tidak akan memasukkan pernyataan if. Setelah itu, mereka semua akan memasukkan pernyataan if.
Ini sangat bersahabat dengan prediktor cabang karena cabang secara berurutan pergi ke arah yang sama berkali-kali. Bahkan penghitung jenuh sederhana akan dengan benar memprediksi cabang kecuali untuk beberapa iterasi setelah berganti arah.
Visualisasi cepat:
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
Namun, ketika data benar-benar acak, prediktor cabang dianggap tidak berguna, karena tidak dapat memprediksi data acak. Dengan demikian kemungkinan akan ada sekitar 50% kesalahan prediksi (tidak lebih baik dari menebak secara acak).
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
Jadi apa yang bisa dilakukan?
Jika kompiler tidak dapat mengoptimalkan cabang menjadi gerakan bersyarat, Anda dapat mencoba beberapa peretasan jika Anda bersedia mengorbankan keterbacaan untuk kinerja.
Menggantikan:
if (data[c] >= 128)
sum += data[c];
dengan:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
Ini menghilangkan cabang dan menggantinya dengan beberapa operasi bitwise.
(Perhatikan bahwa peretasan ini tidak sepenuhnya setara dengan pernyataan if asli. Namun dalam kasus ini, peretasan ini berlaku untuk semua nilai input data[]
.)
Benchmark: Core i7 920 @ 3.5 GHz
C ++ - Visual Studio 2010 - Rilis x64
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
Java - NetBeans 7.1.1 JDK 7 - x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
Pengamatan:
- Dengan Cabang: Ada perbedaan besar antara data yang diurutkan dan yang tidak disortir.
- Dengan Peretasan: Tidak ada perbedaan antara data yang diurutkan dan yang tidak disortir.
- Dalam kasus C ++, peretasan sebenarnya sedikit lebih lambat dibandingkan dengan cabang saat data diurutkan.
Aturan umum adalah untuk menghindari percabangan yang bergantung pada data dalam loop kritis (seperti dalam contoh ini).
Memperbarui:
GCC 4.6.1 dengan -O3
atau -ftree-vectorize
pada x64 dapat menghasilkan gerakan bersyarat. Jadi tidak ada perbedaan antara data yang diurutkan dan yang tidak disortir - keduanya cepat.
(Atau agak cepat: untuk kasus yang sudah disortir, cmov
bisa lebih lambat terutama jika GCC menempatkannya di jalur kritis alih-alih adil add
, terutama pada Intel sebelum Broadwell di mana cmov
memiliki 2 siklus latensi: flag optimasi gcc -O3 membuat kode lebih lambat dari -O2 )
VC ++ 2010 tidak dapat menghasilkan gerakan bersyarat untuk cabang ini bahkan di bawah /Ox
.
Intel C ++ Compiler (ICC) 11 melakukan sesuatu yang ajaib. Ini menukar kedua loop , sehingga mengangkat cabang yang tidak dapat diprediksi ke loop luar. Jadi tidak hanya itu kebal terhadap ramalan, itu juga dua kali lebih cepat dari apa pun yang dapat dihasilkan oleh VC ++ dan GCC! Dengan kata lain, ICC memanfaatkan loop-tes untuk mengalahkan benchmark ...
Jika Anda memberikan kompiler Intel kode branchless, itu hanya akan langsung membuat vektor ... dan sama cepat dengan cabang (dengan pertukaran loop).
Ini menunjukkan bahwa kompiler modern yang matang sekalipun dapat sangat bervariasi dalam kemampuannya untuk mengoptimalkan kode ...