Khususnya, jika saya memiliki serangkaian if
... else if
pernyataan, dan entah bagaimana saya tahu sebelumnya probabilitas relatif yang akan dievaluasi oleh setiap pernyataan true
, berapa banyak perbedaan dalam waktu eksekusi yang dibuat untuk menyortirnya dalam urutan probabilitas? Misalnya, saya harus memilih ini:
if (highly_likely)
//do something
else if (somewhat_likely)
//do something
else if (unlikely)
//do something
untuk ini?:
if (unlikely)
//do something
else if (somewhat_likely)
//do something
else if (highly_likely)
//do something
Tampaknya jelas bahwa versi yang diurutkan akan lebih cepat, namun untuk keterbacaan atau adanya efek samping, kami mungkin ingin memesannya secara tidak optimal. Juga sulit untuk mengatakan seberapa baik CPU akan melakukan dengan prediksi cabang sampai Anda benar-benar menjalankan kode.
Jadi, dalam percobaan dengan ini, saya akhirnya menjawab pertanyaan saya sendiri untuk kasus tertentu, namun saya ingin mendengar pendapat / wawasan lain juga.
Penting: pertanyaan ini mengasumsikan bahwa if
pernyataan dapat ditata ulang secara sewenang-wenang tanpa memiliki efek lain pada perilaku program. Dalam jawaban saya, ketiga tes bersyarat ini saling eksklusif dan tidak menghasilkan efek samping. Tentu saja, jika pernyataan harus dievaluasi dalam urutan tertentu untuk mencapai perilaku yang diinginkan, maka masalah efisiensi diperdebatkan.
Jawaban:
Sebagai aturan umum, kebanyakan jika tidak semua CPU Intel menganggap cabang ke depan tidak diambil saat pertama kali melihatnya. Lihat karya Godbolt .
Setelah itu, cabang masuk ke cache prediksi cabang, dan perilaku masa lalu digunakan untuk menginformasikan prediksi cabang di masa depan.
Jadi dalam loop yang ketat, efek misordering akan relatif kecil. Prediktor cabang akan mempelajari set cabang mana yang paling mungkin, dan jika Anda memiliki jumlah pekerjaan non-sepele dalam loop, perbedaan kecil tidak akan bertambah banyak.
Dalam kode umum, kebanyakan kompiler secara default (tidak memiliki alasan lain) akan memesan kode mesin yang diproduksi kira-kira seperti Anda memesannya dalam kode Anda. Jadi, jika pernyataan adalah cabang maju ketika mereka gagal.
Jadi, Anda harus memesan cabang Anda dalam urutan penurunan kemungkinan untuk mendapatkan prediksi cabang terbaik dari "pertemuan pertama".
Sebuah microbenchmark yang berulang kali berulang kali ketat pada serangkaian kondisi dan melakukan pekerjaan sepele akan didominasi oleh efek kecil dari jumlah instruksi dan sejenisnya, dan sedikit dalam hal masalah prediksi cabang relatif. Jadi dalam hal ini Anda harus profil , karena aturan praktis tidak akan dapat diandalkan.
Selain itu, vektorisasi dan banyak optimasi lainnya berlaku untuk loop ketat kecil.
Jadi dalam kode umum, masukkan kode yang paling mungkin ke dalam
if
blok, dan itu akan menghasilkan prediksi cabang un-cache paling sedikit. Dalam putaran yang ketat, ikuti aturan umum untuk memulai, dan jika Anda perlu tahu lebih banyak, Anda tidak punya banyak pilihan selain profil.Tentu ini semua keluar jendela jika beberapa tes jauh lebih murah daripada yang lain.
sumber
Saya membuat tes berikut untuk menghitung waktu eksekusi dua blok
if
... berbedaelse if
, satu diurutkan berdasarkan probabilitas, yang lain diurutkan dalam urutan terbalik:Menggunakan MSVC2017 dengan / O2, hasilnya menunjukkan bahwa versi yang diurutkan secara konsisten sekitar 28% lebih cepat daripada versi yang tidak disortir. Per komentar luk32, saya juga mengganti urutan dua tes, yang membuat perbedaan nyata (22% vs 28%). Kode dijalankan di bawah Windows 7 pada Intel Xeon E5-2697 v2. Ini, tentu saja, sangat spesifik masalah dan tidak boleh ditafsirkan sebagai jawaban konklusif.
sumber
if... else if
pernyataan dapat memiliki efek besar pada bagaimana logika mengalir melalui kode. Theunlikely
cek mungkin tidak muncul sering, tapi mungkin ada kebutuhan bisnis untuk memeriksaunlikely
kondisi pertama sebelum memeriksa orang lain.g++ -O2 -march=native -std=c++14
memang memberikan sedikit keunggulan untuk pernyataan kondisi bersurutan, tetapi sebagian besar waktu, perbedaan persen antara dua berjalan adalah ~ 5%. Beberapa kali, itu sebenarnya lebih lambat (karena variasi). Saya cukup yakin bahwa memesanif
seperti ini tidak perlu dikhawatirkan; PGO mungkin akan sepenuhnya menangani kasus-kasus seperti ituTidak, Anda tidak boleh, kecuali Anda benar-benar yakin bahwa sistem target terpengaruh.Secara default, pergi dengan keterbacaan.
Saya sangat meragukan hasil Anda. Saya telah sedikit memodifikasi contoh Anda, jadi membalikkan eksekusi lebih mudah. Ideone agak konsisten menunjukkan bahwa urutan terbalik lebih cepat, meskipun tidak banyak. Pada menjalankan tertentu bahkan ini kadang-kadang terbalik. Saya akan mengatakan hasilnya tidak meyakinkan. coliru melaporkan tidak ada perbedaan nyata juga. Saya dapat memeriksa CPU Exynos5422 pada x4 odroid saya nanti.
Masalahnya adalah bahwa CPU modern memiliki prediktor cabang. Ada banyak-banyak logika yang didedikasikan untuk mengambil data dan instruksi, dan CPU x86 modern agak pintar, ketika sampai pada hal ini. Beberapa arsitektur yang lebih ramping seperti ARM atau GPU mungkin rentan terhadap hal ini. Tetapi ini sangat tergantung pada kompiler dan sistem target.
Saya akan mengatakan bahwa optimasi pemesanan cabang cukup rapuh dan fana. Lakukan hanya sebagai langkah yang benar-benar selaras.
Kode:
sumber
Hanya 5 sen saya. Tampaknya efek memesan jika pernyataan harus bergantung pada:
Probabilitas masing-masing pernyataan if.
Jumlah iterasi, sehingga prediktor cabang bisa masuk.
Petunjuk kompiler yang mungkin / tidak mungkin, yaitu tata letak kode.
Untuk menjelajahi faktor-faktor itu, saya membuat tolok ukur fungsi-fungsi berikut:
ordered_ifs ()
reversed_ifs ()
ordered_ifs_with_hints ()
reversed_ifs_with_hints ()
data
Array data berisi angka acak antara 0 dan 100:
Hasil
Hasil berikut untuk Intel i5 @ 3,2 GHz dan G ++ 6.3.0. Argumen pertama adalah check_point (yaitu probabilitas dalam %% untuk pernyataan if sangat mungkin), argumen kedua adalah data_sz (yaitu jumlah iterasi).
Analisis
1. Pemesanan Tidak Penting
Untuk iterasi 4K dan (hampir) 100% kemungkinan pernyataan yang sangat disukai, perbedaannya sangat besar: 223%:
Untuk iterasi 4K dan probabilitas 50% dari pernyataan yang sangat disukai, perbedaannya adalah sekitar 14%:
2. Jumlah Iterasi Tidak Peduli
Perbedaan antara iterasi 4K dan 8K untuk (hampir) 100% kemungkinan pernyataan yang sangat disukai sekitar dua kali (seperti yang diharapkan):
Tetapi perbedaan antara iterasi 4K dan 8K untuk probabilitas 50% dari pernyataan yang sangat disukai adalah 5,5 kali:
Kenapa begitu? Karena prediktor cabang meleset. Inilah cabang yang terlewatkan untuk setiap kasus yang disebutkan di atas:
Jadi pada i5 saya, prediktor cabang gagal secara spektakuler untuk cabang yang tidak begitu mungkin dan kumpulan data besar.
3. Petunjuk Bantuan Sedikit
Untuk iterasi 4K hasilnya agak lebih buruk untuk probabilitas 50% dan agak lebih baik untuk mendekati probabilitas 100%:
Tetapi untuk iterasi 8K hasilnya selalu sedikit lebih baik:
Jadi, petunjuknya juga membantu, tetapi hanya sedikit.
Kesimpulan keseluruhan adalah: selalu membandingkan kode, karena hasilnya mungkin mengejutkan.
Semoga itu bisa membantu.
sumber
g++ -O2
atau-O3 -fno-tree-vectorize
, tetapi Anda harus mengatakannya.Berdasarkan beberapa jawaban lain di sini, sepertinya satu-satunya jawaban nyata adalah: itu tergantung . Itu tergantung pada paling tidak hal-hal berikut (meskipun tidak harus dalam urutan kepentingan ini):
Satu-satunya cara untuk mengetahui secara pasti adalah dengan membandingkan kasus spesifik Anda, lebih disukai pada sistem yang identik dengan (atau sangat mirip dengan) sistem yang dimaksud di mana kode akhirnya akan berjalan. Jika ini dimaksudkan untuk berjalan pada satu set sistem yang berbeda-beda dengan perangkat keras yang berbeda, sistem operasi, dll., Maka itu ide yang baik untuk melakukan benchmarking di berbagai variasi untuk melihat mana yang terbaik. Bahkan mungkin ide yang baik untuk membuat kode dikompilasi dengan satu pemesanan pada satu jenis sistem dan satu lagi pemesanan pada jenis sistem lainnya.
Aturan praktis saya (untuk kebanyakan kasus, tanpa adanya patokan) adalah memesan berdasarkan:
sumber
Cara saya biasanya melihat ini diselesaikan untuk kode kinerja tinggi adalah menjaga urutan yang paling mudah dibaca, tetapi memberikan petunjuk kepada kompiler. Ini adalah salah satu contoh dari kernel Linux :
Di sini asumsinya adalah bahwa pemeriksaan akses akan berlalu, dan tidak ada kesalahan yang dikembalikan
res
. Mencoba untuk menyusun ulang salah satu dari ini jika klausa hanya akan membingungkan kode, tetapilikely()
danunlikely()
makro benar-benar membantu keterbacaan dengan menunjukkan apa kasus normal dan apa pengecualiannya.Implementasi Linux dari makro tersebut menggunakan fitur spesifik GCC . Tampaknya dentang dan kompiler Intel C mendukung sintaks yang sama, tetapi MSVC tidak memiliki fitur tersebut .
sumber
likely()
danunlikely()
makro didefinisikan, dan termasuk beberapa informasi tentang fitur kompiler yang sesuai.else if
jika kompiler tidak cukup pintar untuk mengetahui bahwa kondisinya saling eksklusif.Juga tergantung pada kompiler Anda dan platform yang Anda kompilasi.
Secara teori, kondisi yang paling mungkin harus membuat kontrol melompat seminimal mungkin.
Biasanya kondisi yang paling mungkin adalah yang pertama:
ASM paling populer didasarkan pada cabang kondisional yang melompat ketika kondisinya benar . Kode C itu kemungkinan akan diterjemahkan ke pseudo asm tersebut:
Ini karena lompatan membuat cpu membatalkan pipa eksekusi dan berhenti karena penghitung program berubah (untuk arsitektur yang mendukung pipa yang benar-benar umum). Kemudian tentang kompiler, yang mungkin atau mungkin tidak menerapkan beberapa optimasi canggih tentang memiliki kondisi yang paling mungkin secara statistik untuk mendapatkan kontrol membuat lebih sedikit lompatan.
sumber
clang
sebenarnya mengambil pendekatan yang berbeda untuktest2
dantest3
: karena heuristik yang menunjukkan bahwa tes< 0
atau== 0
kemungkinan salah, itu memutuskan untuk mengkloning sisa fungsi di kedua jalur, sehingga dapat membuatcondition == false
jalan jatuh melalui jalur. Ini layak hanya karena sisa fungsi pendek: ditest4
saya menambahkan satu operasi lagi dan kembali ke pendekatan yang saya uraikan di atas.jmp
tidak berguna sehingga pengambilan / decode bandwidth terbuang sia-sia (2) bahkan dengan prediksi core besar modern hanya melakukan satu pengambilan per siklus sehingga menempatkan batas keras 1 cabang / siklus yang diambil (OTOH modern Intel dapat melakukan 2 tidak mengambil / siklus) (3 ) lebih sulit untuk prediksi cabang untuk berurusan dengan cabang yang diambil berturut-turut dan dalam kasus prediktor cepat + lambat ...Saya memutuskan untuk menjalankan kembali tes pada mesin saya sendiri menggunakan kode Lik32. Saya harus mengubahnya karena windows atau kompiler saya berpikir resolusi tinggi adalah 1 ms, menggunakan
mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -feksepsi -g
GCC telah melakukan transformasi yang sama pada kedua kode asli.
Perhatikan bahwa hanya dua kondisi pertama yang diuji karena yang ketiga harus selalu benar, GCC adalah sejenis Sherlock di sini.
Balik
Jadi ini tidak memberi tahu kita banyak kecuali bahwa kasus terakhir tidak memerlukan prediksi cabang.
Sekarang saya mencoba semua 6 kombinasi if, 2 teratas adalah yang asli terbalik dan diurutkan. tinggi> = 95, rendah <20, sedang 20-94 dengan 10.000.000 iterasi masing-masing.
Jadi mengapa urutannya tinggi, rendah, med maka lebih cepat (sedikit)
Karena yang paling tidak dapat diprediksi adalah yang terakhir dan karena itu tidak pernah dijalankan melalui prediktor cabang.
Jadi cabang akan diprediksi diambil, diambil, dan sisanya dengan
6% + (0,94 *) 20% mispredicts.
"Diurutkan"
Cabang-cabang akan diprediksi dengan tidak diambil, tidak diambil dan Sherlock.
25% + (0,75 *) 24% salah duga
Memberikan perbedaan 18-23% (perbedaan terukur ~ 9%) tetapi kita perlu menghitung siklus alih-alih salah mengartikan%.
Mari kita asumsikan 17 siklus kesalahan hukuman pada CPU Nehalem saya dan bahwa setiap cek membutuhkan 1 siklus untuk mengeluarkan (4-5 instruksi) dan loop mengambil satu siklus juga. Ketergantungan data adalah variabel penghitung dan loop, tetapi begitu salah duga tidak keluar dari situ seharusnya tidak mempengaruhi waktu.
Jadi untuk "membalikkan", kita mendapatkan timing (ini harus menjadi rumus yang digunakan dalam Arsitektur Komputer: Pendekatan Kuantitatif IIRC).
dan sama untuk "diurutkan"
(8.26-7.24) /8.26 = 13.8% vs. ~ 9% diukur (dekat dengan yang diukur!?!).
Jadi yang jelas dari OP tidak jelas.
Dengan tes ini, tes lain dengan kode yang lebih rumit atau lebih banyak ketergantungan data tentu akan berbeda, jadi ukur kasus Anda.
Mengubah urutan pengujian mengubah hasil, tetapi itu bisa jadi karena keberpihakan yang berbeda pada awal loop yang idealnya harus 16 byte yang diluruskan pada semua CPU Intel yang lebih baru tetapi tidak dalam kasus ini.
sumber
Masukkan mereka dalam urutan logis apa pun yang Anda suka. Tentu, cabang mungkin lebih lambat, tetapi tidak seharusnya bercabang menjadi mayoritas pekerjaan yang dilakukan komputer Anda.
Jika Anda bekerja pada bagian kode kinerja kritis, maka tentu saja menggunakan urutan logis, optimasi dipandu profil dan teknik lainnya, tetapi untuk kode umum, saya pikir itu benar-benar lebih dari pilihan gaya.
sumber
i++
kapan++i
akan melakukannya, karena saya sadar bahwai++
untuk beberapa iterator sulit untuk dioptimalkan++i
dan perbedaan (bagi saya) tidak masalah. Ini tentang menghindari pesimisasi; menempatkan blok yang paling mungkin sebagai prioritas utama sebagai kebiasaan tidak akan menyebabkan pengurangan keterbacaan yang nyata (dan mungkin benar-benar membantu!), sementara menghasilkan kode yang ramah prediksi cabang (dan dengan demikian memberi Anda dorongan kinerja kecil yang seragam yang tidak dapat ditangkap kembali. dengan optimasi mikro nanti)Jika Anda sudah tahu probabilitas relatif pernyataan if-else, maka untuk tujuan kinerja lebih baik menggunakan cara yang diurutkan, karena hanya akan memeriksa satu kondisi (yang benar).
Dengan cara yang tidak disortir kompiler akan memeriksa semua kondisi yang tidak perlu dan akan memakan waktu.
sumber