Ini adalah bagian dari kode C ++ yang menunjukkan beberapa perilaku yang sangat aneh. Untuk beberapa alasan aneh, mengurutkan data secara ajaib membuat kode hampir enam kali lebih cepat:
#include <algorithm>
#include <ctime>
#include <iostream>
int main()
{
// Generate data
const unsigned arraySize = 32768;
int data[arraySize];
for (unsigned c = 0; c < arraySize; ++c)
data[c] = std::rand() % 256;
// !!! With this, the next loop runs faster.
std::sort(data, data + arraySize);
// Test
clock_t start = clock();
long long sum = 0;
for (unsigned i = 0; i < 100000; ++i)
{
// Primary loop
for (unsigned c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
std::cout << elapsedTime << std::endl;
std::cout << "sum = " << sum << std::endl;
}
- Tanpa
std::sort(data, data + arraySize);
, kode berjalan dalam 11,54 detik. - Dengan data yang diurutkan, kode ini berjalan dalam 1,93 detik.
Awalnya, saya pikir ini mungkin hanya sebuah anomali bahasa atau kompiler, jadi saya mencoba Java:
import java.util.Arrays;
import java.util.Random;
public class Main
{
public static void main(String[] args)
{
// Generate data
int arraySize = 32768;
int data[] = new int[arraySize];
Random rnd = new Random(0);
for (int c = 0; c < arraySize; ++c)
data[c] = rnd.nextInt() % 256;
// !!! With this, the next loop runs faster
Arrays.sort(data);
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0);
System.out.println("sum = " + sum);
}
}
Dengan hasil yang serupa tetapi tidak terlalu ekstrem.
Pikiran pertama saya adalah penyortiran membawa data ke dalam cache, tetapi kemudian saya berpikir betapa konyolnya karena array baru saja dihasilkan.
- Apa yang sedang terjadi?
- Mengapa memproses array yang diurutkan lebih cepat daripada memproses array yang tidak disortir?
Kode ini merangkum beberapa istilah independen, jadi urutannya tidak masalah.
java
c++
performance
optimization
branch-prediction
GManNickG
sumber
sumber
Jawaban:
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 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 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:
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:
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).
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:
dengan:
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
Java - NetBeans 7.1.1 JDK 7 - x64
Pengamatan:
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 adiladd
, terutama pada Intel sebelum Broadwell di manacmov
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 ...
sumber
Prediksi cabang.
Dengan array yang diurutkan, kondisi
data[c] >= 128
pertamafalse
- tama adalah deretan nilai, kemudian menjaditrue
untuk semua nilai yang lebih baru. Itu mudah diprediksi. Dengan array yang tidak disortir, Anda membayar biaya percabangan.sumber
Alasan mengapa kinerja meningkat secara drastis ketika data disortir adalah bahwa hukuman prediksi cabang dihapus, seperti yang dijelaskan dengan indah dalam jawaban Mysticial .
Sekarang, jika kita melihat kodenya
kita dapat menemukan bahwa arti
if... else...
cabang khusus ini adalah menambahkan sesuatu ketika suatu kondisi terpenuhi. Jenis cabang ini dapat dengan mudah diubah menjadi pernyataan pemindahan bersyarat , yang akan dikompilasi menjadi instruksi pemindahan bersyarat:,cmovl
dalam suatux86
sistem. Cabang dan dengan demikian penalti prediksi cabang potensial dihapus.Dalam
C
, dengan demikianC++
, pernyataan, yang akan dikompilasi secara langsung (tanpa optimasi apa pun) ke dalam instruksi pemindahan bersyaratx86
, adalah operator ternary... ? ... : ...
. Jadi kami menulis ulang pernyataan di atas menjadi pernyataan yang setara:Sambil mempertahankan keterbacaan, kita dapat memeriksa faktor percepatan.
Pada Intel Core i7 -2600K @ 3.4 GHz dan Mode Rilis Visual Studio 2010, patokannya adalah (format disalin dari Mysticial):
x86
x64
Hasilnya kuat dalam beberapa tes. Kami mendapatkan speedup yang hebat ketika hasil cabang tidak dapat diprediksi, tetapi kami sedikit menderita saat diprediksi. Bahkan, ketika menggunakan gerakan kondisional, kinerjanya sama terlepas dari pola data.
Sekarang mari kita melihat lebih dekat dengan menyelidiki
x86
perakitan yang mereka hasilkan. Untuk kesederhanaan, kami menggunakan dua fungsimax1
danmax2
.max1
menggunakan cabang kondisionalif... else ...
:max2
menggunakan operator ternary... ? ... : ...
:Pada mesin x86-64,
GCC -S
hasilkan perakitan di bawah ini.max2
menggunakan kode jauh lebih sedikit karena penggunaan instruksicmovge
. Tetapi keuntungan nyata adalah bahwamax2
tidak melibatkan lompatan cabangjmp
,, yang akan memiliki penalti kinerja yang signifikan jika hasil yang diprediksi tidak benar.Jadi mengapa langkah kondisional berkinerja lebih baik?
Dalam
x86
prosesor yang khas , pelaksanaan instruksi dibagi menjadi beberapa tahap. Secara kasar, kami memiliki perangkat keras yang berbeda untuk menangani tahapan yang berbeda. Jadi kita tidak perlu menunggu satu instruksi untuk menyelesaikan untuk memulai yang baru. Ini disebut pipelining .Dalam kasus cabang, instruksi berikut ditentukan oleh yang sebelumnya, jadi kami tidak bisa melakukan pipelining. Kita harus menunggu atau memprediksi.
Dalam kasus pemindahan bersyarat, instruksi pemindahan bersyarat eksekusi dibagi menjadi beberapa tahap, tetapi tahap sebelumnya suka
Fetch
danDecode
tidak bergantung pada hasil dari instruksi sebelumnya; hanya tahap terakhir yang membutuhkan hasilnya. Jadi, kami menunggu sebagian kecil dari waktu eksekusi satu instruksi. Inilah sebabnya mengapa versi pemindahan bersyarat lebih lambat daripada cabang saat prediksi mudah.Buku Computer Systems: A Programmer's Perspective, edisi kedua menjelaskan ini secara terperinci. Anda dapat memeriksa Bagian 3.6.6 untuk Petunjuk Pergerakan Bersyarat , seluruh Bab 4 untuk Arsitektur Prosesor , dan Bagian 5.11.2 untuk perawatan khusus untuk Prediksi Cabang dan Denda Kesalahan prediksi .
Kadang-kadang, beberapa kompiler modern dapat mengoptimalkan kode kami ke perakitan dengan kinerja yang lebih baik, kadang-kadang beberapa kompiler tidak dapat (kode tersebut menggunakan kompiler asli Visual Studio). Mengetahui perbedaan kinerja antara gerakan cabang dan bersyarat saat tidak dapat diprediksi dapat membantu kami menulis kode dengan kinerja yang lebih baik ketika skenario menjadi sangat kompleks sehingga kompiler tidak dapat mengoptimalkannya secara otomatis.
sumber
-O0
contoh yang menyesatkan dan untuk menunjukkan perbedaan asm dioptimalkan pada dua testcases Anda.Jika Anda ingin tahu tentang lebih banyak optimasi yang dapat dilakukan untuk kode ini, pertimbangkan ini:
Dimulai dengan loop asli:
Dengan interchange loop, kita dapat dengan aman mengubah loop ini ke:
Kemudian, Anda dapat melihat bahwa
if
kondisi bersyarat konstan selama eksekusii
loop, sehingga Anda dapat menarikif
keluar:Kemudian, Anda melihat bahwa loop dalam dapat diciutkan menjadi satu ekspresi tunggal, dengan asumsi model floating point memungkinkan (
/fp:fast
dilemparkan, misalnya)Yang itu 100.000 kali lebih cepat dari sebelumnya.
sumber
i
dari satu unit = 1e5. Tidak ada bedanya dengan hasil akhir, tetapi saya hanya ingin meluruskan karena ini adalah halaman yang sering dikunjungi.if
pada titik ini dapat dikonversi menjadi:sum += (data[j] >= 128) ? data[j] * 100000 : 0;
yang dapat dikurangicmovge
atau dikompilasi oleh kompiler .Tidak diragukan lagi beberapa dari kita akan tertarik pada cara mengidentifikasi kode yang bermasalah untuk prediktor cabang CPU. Alat Valgrind
cachegrind
memiliki simulator prediktor cabang, diaktifkan dengan menggunakan--branch-sim=yes
bendera. Menjalankannya di atas contoh dalam pertanyaan ini, dengan jumlah loop luar dikurangi menjadi 10.000 dan dikompilasi dengang++
, memberikan hasil ini:Diurutkan:
Tidak disortir:
Mengebor ke dalam output line-by-line yang diproduksi oleh
cg_annotate
kita lihat untuk loop yang dimaksud:Diurutkan:
Tidak disortir:
Ini memungkinkan Anda dengan mudah mengidentifikasi garis yang bermasalah - dalam versi yang tidak disortir,
if (data[c] >= 128)
garis tersebut menyebabkan 164.050.007 cabang kondisional yang salah prediksi (Bcm
) di bawah model prediktor cabang cachegrind, sedangkan itu hanya menyebabkan 10.006 dalam versi yang diurutkan.Atau, di Linux Anda dapat menggunakan subsistem penghitung kinerja untuk menyelesaikan tugas yang sama, tetapi dengan kinerja asli menggunakan penghitung CPU.
Diurutkan:
Tidak disortir:
Itu juga dapat melakukan anotasi kode sumber dengan pembongkaran.
Lihat tutorial kinerja untuk lebih jelasnya.
sumber
data[c] >= 128
(yang memiliki tingkat kehilangan 50% seperti yang Anda sarankan) dan satu untuk kondisi loopc < arraySize
yang memiliki ~ tingkat kehilangan 0% .Saya baru saja membaca pertanyaan ini dan jawabannya, dan saya merasa ada jawaban yang hilang.
Cara umum untuk menghilangkan prediksi cabang yang saya temukan bekerja sangat baik dalam bahasa yang dikelola adalah pencarian tabel alih-alih menggunakan cabang (meskipun saya belum mengujinya dalam kasus ini).
Pendekatan ini bekerja secara umum jika:
Latar belakang dan alasannya
Dari perspektif prosesor, memori Anda lambat. Untuk mengimbangi perbedaan dalam kecepatan, beberapa cache dibangun ke prosesor Anda (L1 / L2 cache). Jadi bayangkan Anda melakukan perhitungan yang bagus dan mencari tahu bahwa Anda perlu memori. Prosesor akan mendapatkan operasinya 'memuat' dan memuat potongan memori ke dalam cache - dan kemudian menggunakan cache untuk melakukan sisa perhitungan. Karena memori relatif lambat, 'memuat' ini akan memperlambat program Anda.
Seperti prediksi cabang, ini dioptimalkan dalam prosesor Pentium: prosesor memperkirakan bahwa ia perlu memuat sepotong data dan mencoba memuatnya ke dalam cache sebelum operasi benar-benar menyentuh cache. Seperti yang telah kita lihat, prediksi cabang terkadang salah besar - dalam skenario terburuk Anda harus kembali dan benar-benar menunggu beban memori, yang akan memakan waktu selamanya ( dengan kata lain: gagal prediksi cabang buruk, memori memuat setelah gagal prediksi cabang hanya mengerikan! ).
Untungnya bagi kita, jika pola akses memori dapat diprediksi, prosesor akan memuatnya dalam cache cepat dan semuanya baik-baik saja.
Hal pertama yang perlu kita ketahui adalah apa yang kecil ? Meskipun lebih kecil umumnya lebih baik, aturan praktis adalah tetap berpegang pada tabel pencarian yang berukuran <= 4096 byte. Sebagai batas atas: jika tabel pencarian Anda lebih besar dari 64K mungkin perlu dipertimbangkan kembali.
Membangun meja
Jadi kita sudah tahu bahwa kita bisa membuat tabel kecil. Hal berikutnya yang harus dilakukan adalah mendapatkan fungsi pencarian di tempat. Fungsi pencarian biasanya merupakan fungsi kecil yang menggunakan beberapa operasi integer dasar (dan, atau, xor, shift, tambah, hapus, dan mungkin gandakan). Anda ingin agar input Anda diterjemahkan oleh fungsi pencarian ke semacam 'kunci unik' di tabel Anda, yang kemudian hanya memberi Anda jawaban dari semua pekerjaan yang Anda inginkan.
Dalam hal ini:> = 128 berarti kita dapat menyimpan nilainya, <128 berarti kita membuangnya. Cara termudah untuk melakukannya adalah dengan menggunakan 'DAN': jika kita menyimpannya, kita DAN dengan 7FFFFFFF; jika kita ingin menyingkirkannya, kita DAN itu dengan 0. Perhatikan juga bahwa 128 adalah kekuatan 2 - jadi kita dapat melanjutkan dan membuat tabel 32768/128 bilangan bulat dan mengisinya dengan nol dan banyak 7FFFFFFFF's.
Bahasa yang dikelola
Anda mungkin bertanya-tanya mengapa ini bekerja dengan baik dalam bahasa yang dikelola. Setelah semua, bahasa yang dikelola memeriksa batas-batas array dengan cabang untuk memastikan Anda tidak mengacaukan ...
Ya, tidak persis ... :-)
Ada beberapa upaya untuk menghilangkan cabang ini untuk bahasa yang dikelola. Sebagai contoh:
Dalam kasus ini, jelas bagi kompiler bahwa kondisi batas tidak akan pernah mengenai. Setidaknya kompiler Microsoft JIT (tapi saya berharap Java melakukan hal serupa) akan melihat ini dan menghapus centangnya sama sekali. WOW, itu berarti tidak ada cabang. Demikian pula, ia akan menangani kasus-kasus nyata lainnya.
Jika Anda mengalami masalah dengan pencarian dalam bahasa yang dikelola - kuncinya adalah menambahkan
& 0x[something]FFF
fungsi pencarian Anda untuk membuat pemeriksaan batas dapat diprediksi - dan melihatnya berjalan lebih cepat.Hasil dari kasus ini
sumber
sum += lookup[data[j]]
manalookup
array dengan 256 entri, yang pertama nol dan yang terakhir sama dengan indeks?Karena data didistribusikan antara 0 dan 255 ketika array diurutkan, sekitar paruh pertama iterasi tidak akan masuk ke
if
-if
pernyataan ( pernyataan dibagikan di bawah).Pertanyaannya adalah: Apa yang membuat pernyataan di atas tidak dieksekusi dalam kasus tertentu seperti dalam kasus data yang diurutkan? Di sinilah "prediksi cabang". Prediktor cabang adalah sirkuit digital yang mencoba menebak ke arah mana suatu cabang (misalnya
if-then-else
struktur) akan berjalan sebelum ini diketahui dengan pasti. Tujuan dari prediktor cabang adalah untuk meningkatkan aliran dalam pipa instruksi. Prediktor cabang memainkan peran penting dalam mencapai kinerja efektif tinggi!Mari kita lakukan beberapa penandaan bangku untuk memahaminya dengan lebih baik
Kinerja suatu
if
pernyataan tergantung pada apakah kondisinya memiliki pola yang dapat diprediksi. Jika kondisi selalu benar atau selalu salah, logika prediksi cabang dalam prosesor akan mengambil pola. Di sisi lain, jika polanya tidak dapat diprediksi,if
pernyataan tersebut akan jauh lebih mahal.Mari kita mengukur kinerja loop ini dengan kondisi berbeda:
Berikut adalah timing dari loop dengan pola true-false yang berbeda:
Pola " benar-salah " buruk "dapat membuat
if
pernyataan hingga enam kali lebih lambat daripada pola" baik "! Tentu saja, pola mana yang baik dan mana yang buruk tergantung pada instruksi persis yang dihasilkan oleh kompiler dan pada prosesor tertentu.Jadi tidak ada keraguan tentang dampak prediksi cabang terhadap kinerja!
sumber
Salah satu cara untuk menghindari kesalahan prediksi cabang adalah membangun tabel pencarian, dan mengindeksnya menggunakan data. Stefan de Bruijn mendiskusikan hal itu dalam jawabannya.
Tetapi dalam kasus ini, kita tahu nilai berada dalam kisaran [0, 255] dan kita hanya peduli pada nilai> = 128. Itu berarti kita dapat dengan mudah mengekstraksi bit tunggal yang akan memberi tahu kita apakah kita menginginkan nilai atau tidak: dengan menggeser data ke 7 bit yang tepat, kita dibiarkan dengan 0 bit atau 1 bit, dan kita hanya ingin menambahkan nilai ketika kita memiliki 1 bit. Sebut saja bit ini "bit keputusan".
Dengan menggunakan nilai 0/1 dari bit keputusan sebagai indeks ke dalam array, kita dapat membuat kode yang akan sama cepatnya apakah data diurutkan atau tidak diurutkan. Kode kami akan selalu menambahkan nilai, tetapi ketika bit keputusan adalah 0, kami akan menambahkan nilai di tempat yang tidak kami pedulikan. Berikut kodenya:
Kode ini menghabiskan setengah dari tambahan tetapi tidak pernah memiliki kegagalan prediksi cabang. Ini jauh lebih cepat pada data acak daripada versi dengan pernyataan if aktual.
Tetapi dalam pengujian saya, tabel pencarian eksplisit sedikit lebih cepat dari ini, mungkin karena pengindeksan ke tabel pencarian sedikit lebih cepat daripada sedikit pergeseran. Ini menunjukkan bagaimana kode saya mengatur dan menggunakan tabel pencarian (secara imajinatif disebut
lut
"Tabel Pencarian" dalam kode). Berikut kode C ++:Dalam hal ini, tabel pencarian hanya 256 byte, sehingga sangat cocok dalam cache dan semuanya cepat. Teknik ini tidak akan bekerja dengan baik jika datanya bernilai 24-bit dan kami hanya ingin setengah dari mereka ... tabel pencarian akan terlalu besar untuk praktis. Di sisi lain, kita bisa menggabungkan dua teknik yang ditunjukkan di atas: pertama-tama pindahkan bit, lalu indeks tabel pencarian. Untuk nilai 24-bit yang hanya kami inginkan nilai setengahnya, kami berpotensi menggeser data dengan 12 bit, dan dibiarkan dengan nilai 12-bit untuk indeks tabel. Indeks tabel 12-bit menyiratkan tabel nilai 4096, yang mungkin praktis.
Teknik pengindeksan ke dalam array, alih-alih menggunakan
if
pernyataan, dapat digunakan untuk memutuskan pointer mana yang akan digunakan. Saya melihat perpustakaan yang mengimplementasikan pohon biner, dan bukannya memiliki dua pointer bernama (pLeft
danpRight
atau apa pun) memiliki panjang array pointer-2 dan menggunakan teknik "bit keputusan" untuk memutuskan mana yang akan diikuti. Misalnya, alih-alih:perpustakaan ini akan melakukan sesuatu seperti:
Berikut tautan ke kode ini: Red Black Trees , Eternally Confuzzled
sumber
data[c]>>7
- yang juga dibahas di sini); Saya sengaja mengabaikan solusi ini, tetapi tentu saja Anda benar. Hanya sebuah catatan kecil: Aturan praktis untuk tabel pencarian adalah bahwa jika cocok di 4KB (karena caching), itu akan berhasil - sebaiknya buat tabel sekecil mungkin. Untuk bahasa yang dikelola saya akan mendorongnya ke 64KB, untuk bahasa tingkat rendah seperti C ++ dan C, saya mungkin akan mempertimbangkan kembali (itu hanya pengalaman saya). Karenatypeof(int) = 4
, saya akan mencoba untuk tetap menggunakan maksimal 10 bit.sizeof(int) == 4
? Itu akan berlaku untuk 32-bit. Ponsel saya yang berumur dua tahun memiliki cache L1 32KB, jadi bahkan tabel lookup 4K bisa berfungsi, terutama jika nilai pencariannya adalah byte, bukan int.j
metode Anda sama dengan 0 atau 1, mengapa Anda tidak mengalikan nilainya denganj
sebelum menambahkannya daripada menggunakan pengindeksan array (mungkin harus dikalikan dengan1-j
alih-alihj
)int c = data[j]; sum += c & -(c >> 7);
yang tidak memerlukan perkalian sama sekali.Dalam kasus yang diurutkan, Anda dapat melakukan lebih baik daripada mengandalkan prediksi cabang yang berhasil atau trik perbandingan tanpa cabang: hapus cabang sepenuhnya.
Memang, array dipartisi dalam zona bersebelahan dengan
data < 128
dan dengan lainnyadata >= 128
. Jadi, Anda harus menemukan titik partisi dengan pencarian dikotomik (menggunakanLg(arraySize) = 15
perbandingan), kemudian lakukan akumulasi langsung dari titik itu.Sesuatu seperti (tidak dicentang)
atau, sedikit lebih dikaburkan
Pendekatan yang lebih cepat, yang memberikan solusi perkiraan untuk diurutkan atau tidak disortir adalah:
sum= 3137536;
(dengan asumsi distribusi yang benar-benar seragam, 16384 sampel dengan nilai yang diharapkan 191,5) :-)sumber
sum= 3137536
- pintar. Itu agak jelas bukan inti dari pertanyaan. Pertanyaannya jelas tentang menjelaskan karakteristik kinerja yang mengejutkan. Saya cenderung mengatakan bahwa penambahan melakukanstd::partition
alih - alihstd::sort
itu berharga. Padahal pertanyaan sebenarnya meluas ke lebih dari sekedar tolok ukur sintetis yang diberikan.Perilaku di atas terjadi karena prediksi Cabang.
Untuk memahami prediksi cabang, seseorang harus terlebih dahulu memahami Instruction Pipeline :
Setiap instruksi dipecah menjadi urutan langkah-langkah sehingga langkah-langkah berbeda dapat dieksekusi bersamaan secara paralel. Teknik ini dikenal sebagai pipa instruksi dan ini digunakan untuk meningkatkan throughput pada prosesor modern. Untuk memahami ini dengan lebih baik, silakan lihat contoh ini di Wikipedia .
Secara umum, prosesor modern memiliki jaringan pipa yang cukup panjang, tetapi untuk kemudahan mari kita pertimbangkan 4 langkah ini saja.
Pipa 4-tahap secara umum untuk 2 instruksi.
Kembali ke pertanyaan di atas, mari pertimbangkan petunjuk berikut:
Tanpa prediksi cabang, berikut ini akan terjadi:
Untuk menjalankan instruksi B atau instruksi C prosesor harus menunggu sampai instruksi A tidak mencapai sampai tahap EX dalam pipa, karena keputusan untuk pergi ke instruksi B atau instruksi C tergantung pada hasil instruksi A. Jadi pipa akan terlihat seperti ini.
ketika jika kondisi mengembalikan true:
Ketika jika kondisi kembali salah:
Sebagai hasil dari menunggu hasil instruksi A, total siklus CPU yang dihabiskan dalam kasus di atas (tanpa prediksi cabang; untuk benar dan salah) adalah 7.
Jadi, apa prediksi cabang?
Prediktor cabang akan mencoba menebak ke arah mana sebuah cabang (struktur if-then-else) akan berjalan sebelum ini diketahui dengan pasti. Ia tidak akan menunggu instruksi A untuk mencapai tahap EX dari pipeline, tetapi akan menebak keputusan dan pergi ke instruksi itu (B atau C dalam kasus contoh kita).
Dalam hal dugaan yang benar, pipeline terlihat seperti ini:
Jika kemudian terdeteksi bahwa tebakan itu salah maka instruksi yang dieksekusi sebagian dibuang dan pipa memulai kembali dengan cabang yang benar, menimbulkan penundaan. Waktu yang terbuang dalam kasus salah duga cabang sama dengan jumlah tahapan dalam pipa dari tahap pengambilan ke tahap pelaksanaan. Mikroprosesor modern cenderung memiliki jaringan pipa yang cukup panjang sehingga penundaan kesalahan prediksi adalah antara 10 dan 20 siklus clock. Semakin lama pipa semakin besar kebutuhan untuk prediktor cabang yang baik .
Dalam kode OP, pertama kali ketika bersyarat, prediktor cabang tidak memiliki informasi untuk mendasarkan prediksi, sehingga pertama kali secara acak akan memilih instruksi berikutnya. Kemudian dalam for loop, ini dapat mendasarkan prediksi pada histori. Untuk array yang diurutkan dalam urutan menaik, ada tiga kemungkinan:
Mari kita asumsikan bahwa prediktor akan selalu menganggap cabang yang benar pada putaran pertama.
Jadi dalam kasus pertama, ia akan selalu mengambil cabang yang benar karena secara historis semua prediksi benar. Dalam kasus ke-2, awalnya ini akan memprediksi yang salah, tetapi setelah beberapa iterasi, ia akan memprediksi dengan benar. Dalam kasus ke-3, ia awalnya akan memprediksi dengan benar sampai elemen kurang dari 128. Setelah itu akan gagal untuk beberapa waktu dan memperbaiki sendiri ketika melihat kegagalan prediksi cabang dalam sejarah.
Dalam semua kasus ini, kegagalannya akan terlalu sedikit jumlahnya dan sebagai hasilnya, hanya beberapa kali ia harus membuang instruksi yang dieksekusi sebagian dan memulai kembali dengan cabang yang benar, menghasilkan siklus CPU yang lebih sedikit.
Tetapi dalam kasus array acak yang tidak disortir, prediksi perlu membuang instruksi yang dieksekusi sebagian dan memulai kembali dengan cabang yang benar sebagian besar waktu dan menghasilkan siklus CPU lebih banyak dibandingkan dengan array yang diurutkan.
sumber
Jawaban resmi akan dari
Anda juga dapat melihat dari diagram yang indah ini mengapa prediktor cabang menjadi bingung.
Setiap elemen dalam kode asli adalah nilai acak
jadi sang prediktor akan berubah sisi sebagai
std::rand()
pukulan.Di sisi lain, setelah diurutkan, prediktor pertama-tama akan pindah ke kondisi tidak diambil dan ketika nilai berubah menjadi nilai tinggi, prediktor akan dalam tiga kali menjalankan perubahan mulai dari sangat tidak diambil menjadi sangat diambil.
sumber
Dalam baris yang sama (saya pikir ini tidak disorot oleh jawaban apa pun) ada baiknya menyebutkan bahwa kadang-kadang (khususnya dalam perangkat lunak di mana kinerja penting — seperti di kernel Linux) Anda dapat menemukan beberapa pernyataan if seperti berikut:
atau serupa:
Keduanya
likely()
danunlikely()
sebenarnya makro yang didefinisikan dengan menggunakan sesuatu seperti GCC__builtin_expect
untuk membantu kompiler memasukkan kode prediksi untuk mendukung kondisi dengan mempertimbangkan informasi yang diberikan oleh pengguna. GCC mendukung bawaan lain yang dapat mengubah perilaku program yang sedang berjalan atau memancarkan instruksi tingkat rendah seperti membersihkan cache, dll. Lihat dokumentasi ini yang melewati bawaan GCC yang tersedia.Biasanya optimasi semacam ini terutama ditemukan dalam aplikasi waktu nyata yang sulit atau sistem embedded di mana waktu eksekusi sangat penting dan sangat penting. Misalnya, jika Anda memeriksa beberapa kondisi kesalahan yang hanya terjadi 1/10000000 kali, lalu mengapa tidak memberi tahu kompiler tentang ini? Dengan cara ini, secara default, prediksi cabang akan menganggap bahwa kondisinya salah.
sumber
Operasi Boolean yang sering digunakan dalam C ++ menghasilkan banyak cabang dalam program yang dikompilasi. Jika cabang-cabang ini berada di dalam loop dan sulit untuk diprediksi, mereka dapat memperlambat eksekusi secara signifikan. Variabel Boolean disimpan sebagai bilangan bulat 8-bit dengan nilai
0
untukfalse
dan1
untuktrue
.Variabel Boolean terlalu ditentukan dalam arti bahwa semua operator yang memiliki variabel Boolean sebagai input memeriksa apakah input memiliki nilai selain
0
atau1
, tetapi operator yang memiliki Boolean sebagai output tidak dapat menghasilkan nilai selain0
atau1
. Ini membuat operasi dengan variabel Boolean sebagai input kurang efisien daripada yang diperlukan. Pertimbangkan contoh:Ini biasanya diterapkan oleh kompiler dengan cara berikut:
Kode ini jauh dari optimal. Cabang mungkin membutuhkan waktu lama jika salah duga. Operasi Boolean dapat dibuat jauh lebih efisien jika diketahui dengan pasti bahwa operan tidak memiliki nilai selain
0
dan1
. Alasan mengapa kompiler tidak membuat asumsi seperti itu adalah bahwa variabel mungkin memiliki nilai lain jika mereka tidak diinisialisasi atau berasal dari sumber yang tidak diketahui. Kode di atas dapat dioptimalkan jikaa
danb
telah diinisialisasi ke nilai yang valid atau jika berasal dari operator yang menghasilkan output Boolean. Kode yang dioptimalkan terlihat seperti ini:char
digunakan sebagai gantibool
agar memungkinkan untuk menggunakan operator bitwise (&
dan|
) alih-alih operator Boolean (&&
dan||
). Operator bitwise adalah instruksi tunggal yang hanya membutuhkan satu siklus clock. Operator ATAU (|
) bekerja bahkan jikaa
danb
memiliki nilai selain0
atau1
. Operator AND (&
) dan operator EKSKLUSIF ATAU (^
) dapat memberikan hasil yang tidak konsisten jika operan memiliki nilai selain0
dan1
.~
tidak bisa digunakan untuk TIDAK. Sebagai gantinya, Anda bisa membuat Boolean TIDAK pada variabel yang diketahui0
atau1
dengan XOR'ing dengan1
:dapat dioptimalkan untuk:
a && b
tidak dapat diganti dengana & b
jikab
adalah ekspresi yang tidak boleh dievaluasi jikaa
adalahfalse
(&&
tidak akan mengevaluasib
,&
akan). Demikian juga,a || b
tidak bisa diganti dengana | b
jikab
adalah ekspresi yang tidak dievaluasi jikaa
istrue
.Menggunakan operator bitwise lebih menguntungkan jika operan adalah variabel daripada jika operan adalah perbandingan:
optimal dalam banyak kasus (kecuali jika Anda mengharapkan
&&
ekspresi menghasilkan banyak salah duga cabang).sumber
Itu sudah pasti!...
Prediksi cabang membuat logika berjalan lebih lambat, karena pergantian yang terjadi dalam kode Anda! Ini seperti Anda akan jalan lurus atau jalan dengan banyak belokan, pasti yang lurus akan dilakukan lebih cepat! ...
Jika array diurutkan, kondisi Anda salah pada langkah pertama:,
data[c] >= 128
kemudian menjadi nilai sebenarnya untuk seluruh jalan ke ujung jalan. Begitulah cara Anda sampai ke akhir logika lebih cepat. Di sisi lain, menggunakan array yang tidak disortir, Anda perlu banyak proses dan pembalikan yang membuat kode Anda berjalan lebih lambat pasti ...Lihatlah gambar yang saya buat untuk Anda di bawah ini. Jalan mana yang akan selesai lebih cepat?
Jadi secara pemrograman, prediksi cabang menyebabkan proses menjadi lebih lambat ...
Juga pada akhirnya, ada baiknya mengetahui bahwa kami memiliki dua jenis prediksi cabang yang masing-masing akan memengaruhi kode Anda secara berbeda:
1. Statis
2. Dinamis
sumber
Pertanyaan ini telah dijawab berulang kali dengan sangat baik. Saya masih ingin menarik perhatian kelompok untuk analisis menarik lainnya.
Baru-baru ini contoh ini (dimodifikasi sangat sedikit) juga digunakan sebagai cara untuk menunjukkan bagaimana sepotong kode dapat diprofilkan dalam program itu sendiri pada Windows. Sepanjang jalan, penulis juga menunjukkan cara menggunakan hasil untuk menentukan di mana kode menghabiskan sebagian besar waktunya baik dalam kasus diurutkan & tidak disortir. Akhirnya karya ini juga menunjukkan bagaimana menggunakan fitur HAL (Hardware Abstraction Layer) yang sedikit diketahui untuk menentukan seberapa banyak kesalahan prediksi cabang yang terjadi dalam kasus yang tidak disortir.
Tautannya ada di sini: http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/profile/demo.htm
sumber
When the input is unsorted, all the rest of the loop takes substantial time. But with sorted input, the processor is somehow able to spend not just less time in the body of the loop, meaning the buckets at offsets 0x18 and 0x1C, but vanishingly little time on the mechanism of looping.
Penulis sedang mencoba untuk membahas profiling dalam konteks kode yang diposting di sini dan dalam proses mencoba menjelaskan mengapa kasus yang diurutkan jauh lebih cepat.Seperti apa yang telah disebutkan oleh orang lain, apa yang ada di balik misteri itu adalah Prediktor Cabang .
Saya tidak mencoba menambahkan sesuatu tetapi menjelaskan konsepnya dengan cara lain. Ada pengantar singkat tentang wiki yang berisi teks dan diagram. Saya suka penjelasan di bawah ini yang menggunakan diagram untuk menguraikan Prediktor Cabang secara intuitif.
Berdasarkan skenario yang dijelaskan, saya telah menulis demo animasi untuk menunjukkan bagaimana instruksi dieksekusi dalam pipa dalam situasi yang berbeda.
Contoh berisi tiga instruksi dan yang pertama adalah instruksi melompat bersyarat. Dua instruksi terakhir dapat masuk ke dalam pipa sampai instruksi lompat bersyarat dijalankan.
Diperlukan 9 siklus clock agar 3 instruksi dapat diselesaikan.
Diperlukan 7 siklus clock agar 3 instruksi dapat diselesaikan.
Diperlukan 9 siklus clock agar 3 instruksi dapat diselesaikan.
Seperti yang Anda lihat, sepertinya kami tidak punya alasan untuk tidak menggunakan Branch Predictor.
Ini adalah demo yang cukup sederhana yang menjelaskan bagian paling mendasar dari Predictor Cabang. Jika gif-gif itu menjengkelkan, silakan hapus saja dari jawabannya dan pengunjung juga bisa mendapatkan kode sumber demo langsung dari BranchPredictorDemo
sumber
if()
blok dapat dijalankan sebelum kondisi cabang diketahui. Atau untuk loop pencarian sepertistrlen
ataumemchr
, interaksi dapat tumpang tindih. Jika Anda harus menunggu agar hasil pertandingan-atau-tidak diketahui sebelum menjalankan iterasi berikutnya, Anda akan mengalami bottleneck pada cache load + latensi ALU alih-alih throughput.Keuntungan prediksi cabang!
Penting untuk dipahami bahwa misprediksi cabang tidak memperlambat program. Biaya prediksi yang terlewatkan adalah seolah-olah prediksi cabang tidak ada dan Anda menunggu evaluasi ekspresi untuk memutuskan kode apa yang akan dijalankan (penjelasan lebih lanjut pada paragraf berikutnya).
Setiap kali ada pernyataan
if-else
\switch
, ekspresi harus dievaluasi untuk menentukan blok mana yang harus dieksekusi. Dalam kode rakitan yang dihasilkan oleh kompiler, instruksi cabang bersyarat dimasukkan.Instruksi cabang dapat menyebabkan komputer mulai mengeksekusi urutan instruksi yang berbeda dan dengan demikian menyimpang dari perilaku default dari mengeksekusi instruksi secara berurutan (yaitu jika ekspresi salah, program melewatkan kode
if
blok) tergantung pada beberapa kondisi, yang merupakan evaluasi ekspresi dalam kasus kami.Yang sedang berkata, kompiler mencoba untuk memprediksi hasil sebelum benar-benar dievaluasi. Ini akan mengambil instruksi dari
if
blok, dan jika ekspresi ternyata benar, maka hebat! Kami memperoleh waktu yang dibutuhkan untuk mengevaluasinya dan membuat kemajuan dalam kode; jika tidak maka kita menjalankan kode yang salah, pipa disiram, dan blok yang benar dijalankan.Visualisasi:
Katakanlah Anda harus memilih rute 1 atau rute 2. Menunggu pasangan Anda memeriksa peta, Anda telah berhenti di ## dan menunggu, atau Anda bisa memilih route1 dan jika Anda beruntung (rute 1 adalah rute yang benar), maka hebatnya Anda tidak perlu menunggu pasangan Anda memeriksa peta (Anda menghemat waktu yang diperlukan untuk memeriksa peta), jika tidak, Anda hanya akan kembali.
Sementara pipa pembilasan sangat cepat, saat ini pertaruhan ini sepadan. Memprediksi data yang diurutkan atau data yang berubah lambat selalu lebih mudah dan lebih baik daripada memprediksi perubahan cepat.
sumber
Pada ARM, tidak diperlukan cabang, karena setiap instruksi memiliki bidang kondisi 4-bit, yang menguji (dengan biaya nol) salah satu dari 16 kondisi berbeda yang mungkin muncul dalam Daftar Status Prosesor, dan jika kondisi pada instruksi adalah salah, instruksi dilewati. Ini menghilangkan kebutuhan untuk cabang pendek, dan tidak akan ada prediksi cabang hit untuk algoritma ini. Oleh karena itu, versi yang diurutkan dari algoritma ini akan berjalan lebih lambat daripada versi yang tidak disortir pada ARM, karena overhead tambahan dari penyortiran.
Lingkaran dalam untuk algoritma ini akan terlihat seperti berikut ini dalam bahasa assembly ARM:
Tapi ini sebenarnya bagian dari gambaran yang lebih besar:
CMP
opcodes selalu memperbarui bit status dalam Prosesor Status Register (PSR), karena itulah tujuannya, tetapi sebagian besar instruksi lainnya tidak menyentuh PSR kecuali Anda menambahkanS
akhiran opsional pada instruksi, yang menetapkan bahwa PSR harus diperbarui berdasarkan pada hasil dari instruksi. Sama seperti sufiks kondisi 4-bit, kemampuan menjalankan instruksi tanpa mempengaruhi PSR adalah mekanisme yang mengurangi kebutuhan cabang pada ARM, dan juga memfasilitasi pengiriman yang tidak berurutan pada tingkat perangkat keras , karena setelah melakukan beberapa operasi X yang memperbarui bit status, selanjutnya (atau secara paralel) Anda dapat melakukan banyak pekerjaan lain yang secara eksplisit seharusnya tidak mempengaruhi bit status, maka Anda dapat menguji status bit status yang ditetapkan sebelumnya oleh X.Bidang pengujian kondisi dan bidang "set bit status" opsional dapat digabungkan, misalnya:
ADD R1, R2, R3
melakukanR1 = R2 + R3
tanpa memperbarui bit status apa pun.ADDGE R1, R2, R3
melakukan operasi yang sama hanya jika instruksi sebelumnya yang mempengaruhi bit status menghasilkan kondisi Lebih Besar dari atau Sama.ADDS R1, R2, R3
Melakukan Selain dan kemudian updateN
,Z
,C
danV
bendera di Status Processor Register berdasarkan apakah hasilnya adalah negatif, nol, Dibawa (untuk penambahan unsigned), atau meluap (untuk penambahan ditandatangani).ADDSGE R1, R2, R3
melakukan penambahan hanya jikaGE
tes benar, dan kemudian memperbarui bit status berdasarkan hasil penambahan.Sebagian besar arsitektur prosesor tidak memiliki kemampuan ini untuk menentukan apakah bit status harus diperbarui untuk operasi yang diberikan, yang dapat mengharuskan penulisan kode tambahan untuk menyimpan dan kemudian mengembalikan bit status, atau mungkin memerlukan cabang tambahan, atau dapat membatasi prosesor keluar efisiensi eksekusi order: salah satu efek samping dari sebagian besar arsitektur set instruksi CPU memperbarui bit status setelah sebagian besar instruksi adalah bahwa jauh lebih sulit untuk memisahkan instruksi mana yang dapat dijalankan secara paralel tanpa mengganggu satu sama lain. Memperbarui bit status memiliki efek samping, oleh karena itu memiliki efek linierisasi pada kode.Kemampuan ARM untuk mencampur dan mencocokkan pengujian kondisi bebas cabang pada instruksi apa pun dengan opsi untuk memperbarui atau tidak memperbarui bit status setelah instruksi apa pun sangat kuat, baik untuk programmer dan kompiler bahasa assembly, dan menghasilkan kode yang sangat efisien.
Jika Anda pernah bertanya-tanya mengapa ARM sangat sukses secara fenomenal, efektivitas dan interaksi yang cemerlang dari kedua mekanisme ini adalah bagian besar dari cerita ini, karena mereka adalah salah satu sumber terbesar dari efisiensi arsitektur ARM. Kecemerlangan desainer asli ARM ISA pada tahun 1983, Steve Furber dan Roger (sekarang Sophie) Wilson, tidak dapat dilebih-lebihkan.
sumber
R2 = data + arraySize
, lalu mulai denganR1 = -arraySize
. Bagian bawah loop menjadiadds r1, r1, #1
/bnz inner_loop
. Kompiler tidak menggunakan pengoptimalan ini karena beberapa alasan: / Tapi bagaimanapun, eksekusi tambahan yang ditentukan tidak berbeda secara mendasar dalam hal ini dari apa yang dapat Anda lakukan dengan kode branchless pada SPA lainnya, seperti x86cmov
. Meskipun tidak sebagus: flag optimasi gcc -O3 membuat kode lebih lambat dari -O2cmov
dengan operan sumber memori. Sebagian besar ISA, termasuk AArch64, hanya memiliki operasi pilih ALU. Jadi predikasi ARM dapat menjadi kuat, dan dapat digunakan lebih efisien daripada kode tanpa cabang pada sebagian besar ISA.)Ini tentang prediksi cabang. Apa itu?
Prediktor cabang adalah salah satu teknik peningkatan kinerja kuno yang masih menemukan relevansi dengan arsitektur modern. Sementara teknik prediksi sederhana memberikan pencarian cepat dan efisiensi daya, mereka menderita tingkat kesalahan prediksi yang tinggi.
Di sisi lain, prediksi cabang yang kompleks - baik berdasarkan neural atau varian prediksi cabang dua tingkat - memberikan akurasi prediksi yang lebih baik, tetapi mereka mengkonsumsi lebih banyak daya dan kompleksitas yang meningkat secara eksponensial.
Selain itu, dalam teknik prediksi yang kompleks waktu yang dibutuhkan untuk memprediksi cabang itu sendiri sangat tinggi - mulai dari 2 hingga 5 siklus - yang sebanding dengan waktu pelaksanaan cabang yang sebenarnya.
Prediksi cabang pada dasarnya adalah masalah optimisasi (minimalisasi) di mana penekanannya adalah pada untuk mencapai tingkat kesalahan serendah mungkin, konsumsi daya rendah, dan kompleksitas rendah dengan sumber daya minimum.
Ada tiga jenis cabang:
Meneruskan cabang bersyarat - berdasarkan kondisi run-time, PC (program counter) diubah untuk menunjuk ke sebuah alamat yang diteruskan dalam aliran instruksi.
Cabang conditional mundur - PC diubah untuk menunjuk mundur dalam aliran instruksi. Cabang didasarkan pada beberapa kondisi, seperti bercabang mundur ke awal loop program ketika tes di akhir loop menyatakan loop harus dieksekusi lagi.
Cabang tanpa syarat - ini termasuk lompatan, panggilan prosedur dan pengembalian yang tidak memiliki kondisi khusus. Misalnya, instruksi lompatan tanpa syarat dapat dikodekan dalam bahasa assembly hanya sebagai "jmp", dan aliran instruksi harus segera diarahkan ke lokasi target yang ditunjuk oleh instruksi lompat, sedangkan lompatan kondisional yang mungkin dikodekan sebagai "jmpne" akan mengarahkan aliran instruksi hanya jika hasil perbandingan dua nilai dalam instruksi "bandingkan" sebelumnya menunjukkan nilai-nilai tidak sama. (Skema pengalamatan tersegmentasi yang digunakan oleh arsitektur x86 menambah kompleksitas tambahan, karena lompatan dapat berupa "dekat" (dalam suatu segmen) atau "jauh" (di luar segmen). Setiap jenis memiliki efek yang berbeda pada algoritma prediksi cabang.)
Prediksi Cabang Statis / Dinamis : Prediksi cabang statis digunakan oleh mikroprosesor saat pertama kali cabang bersyarat ditemukan, dan prediksi cabang dinamis digunakan untuk eksekusi yang berhasil dari kode cabang bersyarat.
Referensi:
Prediktor cabang
Demonstrasi Penentuan Profil Diri
Tinjauan Prediksi Cabang
Prediksi Cabang
sumber
Selain fakta bahwa prediksi cabang dapat memperlambat Anda, array yang diurutkan memiliki keunggulan lain:
Anda dapat memiliki kondisi berhenti alih-alih hanya memeriksa nilainya, dengan cara ini Anda hanya mengulang data yang relevan, dan mengabaikan sisanya.
Prediksi cabang akan hilang hanya sekali.
sumber
Array yang diurutkan diproses lebih cepat daripada array yang tidak disortir, karena fenomena yang disebut prediksi cabang.
Prediktor cabang adalah sirkuit digital (dalam arsitektur komputer) yang mencoba memprediksi ke arah mana cabang akan bergerak, meningkatkan aliran dalam pipa instruksi. Sirkuit / komputer memprediksi langkah selanjutnya dan menjalankannya.
Membuat prediksi yang salah mengarah ke kembali ke langkah sebelumnya, dan mengeksekusi dengan prediksi lain. Dengan asumsi prediksi itu benar, kode akan melanjutkan ke langkah berikutnya. Prediksi yang salah menghasilkan pengulangan langkah yang sama, sampai prediksi yang benar terjadi.
Jawaban atas pertanyaan Anda sangat sederhana.
Dalam array yang tidak disortir, komputer membuat beberapa prediksi, yang mengarah ke peningkatan kemungkinan kesalahan. Padahal, dalam array yang diurutkan, komputer membuat prediksi lebih sedikit, mengurangi kemungkinan kesalahan. Membuat prediksi lebih banyak membutuhkan lebih banyak waktu.
Disortir Array: TrafoTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
Array yang Tidak Disortir: Jalan Melengkung
Prediksi cabang: Menebak / memprediksi jalan mana yang lurus dan mengikutinya tanpa memeriksa
Meskipun kedua jalan mencapai tujuan yang sama, jalan lurus lebih pendek, dan yang lainnya lebih panjang. Jika kemudian Anda memilih yang lain karena kesalahan, tidak ada jalan untuk kembali, sehingga Anda akan membuang waktu ekstra jika Anda memilih jalan yang lebih panjang. Ini mirip dengan apa yang terjadi di komputer, dan saya harap ini membantu Anda memahami lebih baik.
Saya juga ingin mengutip @Simon_Weaver dari komentar:
sumber
Saya mencoba kode yang sama dengan MATLAB 2011b dengan MacBook Pro saya (Intel i7, 64 bit, 2,4 GHz) untuk kode MATLAB berikut:
Hasil untuk kode MATLAB di atas adalah sebagai berikut:
Hasil kode C seperti di @GManNickG saya dapatkan:
Berdasarkan ini, terlihat MATLAB hampir 175 kali lebih lambat dari implementasi C tanpa penyortiran dan 350 kali lebih lambat dengan penyortiran. Dengan kata lain, efek (prediksi cabang) adalah 1,46x untuk implementasi MATLAB dan 2,7x untuk implementasi C.
sumber
Asumsi oleh jawaban lain bahwa seseorang perlu mengurutkan data tidak benar.
Kode berikut tidak mengurutkan seluruh array, tetapi hanya segmen 200 elemen, dan dengan demikian menjalankan tercepat.
Mengurutkan hanya bagian k-elemen melengkapi pra-pemrosesan dalam waktu linier
O(n)
,, daripadaO(n.log(n))
waktu yang diperlukan untuk mengurutkan seluruh array.Ini juga "membuktikan" bahwa itu tidak ada hubungannya dengan masalah algoritmik seperti urutan, dan memang prediksi cabang.
sumber
pcmpgtb
untuk menemukan elemen dengan set bit tinggi, lalu DAN ke nol elemen yang lebih kecil). Menghabiskan waktu benar-benar menyortir potongan akan lebih lambat. Versi branchless akan memiliki kinerja data-independen, juga membuktikan bahwa biaya berasal dari salah prediksi cabang. Atau cukup gunakan penghitung kinerja untuk mengamati hal itu secara langsung, seperti Skylakeint_misc.clear_resteer_cycles
atauint_misc.recovery_cycles
untuk menghitung siklus menganggur front-end dari mispredictsJawaban Bjarne Stroustrup untuk pertanyaan ini:
Itu terdengar seperti pertanyaan wawancara. Apakah itu benar Bagaimana kamu tahu? Merupakan ide yang buruk untuk menjawab pertanyaan tentang efisiensi tanpa terlebih dahulu melakukan beberapa pengukuran, jadi penting untuk mengetahui bagaimana mengukurnya.
Jadi, saya mencoba dengan vektor sejuta bilangan bulat dan mendapat:
Saya berlari itu beberapa kali untuk memastikan. Ya, fenomena itu nyata. Kode kunci saya adalah:
Setidaknya fenomena ini nyata dengan pengaturan kompiler, pustaka standar, dan pengoptimal ini. Implementasi yang berbeda dapat dan memang memberikan jawaban yang berbeda. Bahkan, seseorang melakukan penelitian yang lebih sistematis (pencarian web cepat akan menemukannya) dan sebagian besar implementasi menunjukkan efek itu.
Salah satu alasannya adalah prediksi cabang: operasi utama dalam algoritma pengurutan adalah
“if(v[i] < pivot]) …”
atau setara. Untuk urutan yang diurutkan tes itu selalu benar sedangkan, untuk urutan acak, cabang yang dipilih bervariasi secara acak.Alasan lain adalah ketika vektor sudah diurutkan, kita tidak perlu memindahkan elemen ke posisi yang benar. Efek dari detail kecil ini adalah faktor lima atau enam yang kita lihat.
Quicksort (dan memilah secara umum) adalah studi kompleks yang telah menarik beberapa pemikir besar ilmu komputer. Fungsi sortir yang baik adalah hasil dari pemilihan algoritma yang baik dan memperhatikan kinerja perangkat keras dalam implementasinya.
Jika Anda ingin menulis kode yang efisien, Anda perlu tahu sedikit tentang arsitektur mesin.
sumber
Pertanyaan ini berakar pada Model Prediksi Cabang pada CPU. Saya akan merekomendasikan membaca makalah ini:
Meningkatkan Kecepatan Ambil Instruksi melalui Beberapa Prediksi Cabang dan Cache Alamat Cabang
Ketika Anda telah mengurutkan elemen, IR tidak dapat diganggu untuk mengambil semua instruksi CPU, lagi dan lagi, itu mengambilnya dari cache.
sumber
Salah satu cara untuk menghindari kesalahan prediksi cabang adalah membangun tabel pencarian, dan mengindeksnya menggunakan data. Stefan de Bruijn mendiskusikan hal itu dalam jawabannya.
Tetapi dalam kasus ini, kita tahu nilai berada dalam kisaran [0, 255] dan kita hanya peduli pada nilai> = 128. Itu berarti kita dapat dengan mudah mengekstraksi bit tunggal yang akan memberi tahu kita apakah kita menginginkan nilai atau tidak: dengan menggeser data ke 7 bit yang tepat, kita dibiarkan dengan 0 bit atau 1 bit, dan kita hanya ingin menambahkan nilai ketika kita memiliki 1 bit. Sebut saja bit ini "bit keputusan".
Dengan menggunakan nilai 0/1 dari bit keputusan sebagai indeks ke dalam array, kita dapat membuat kode yang akan sama cepatnya apakah data diurutkan atau tidak diurutkan. Kode kami akan selalu menambahkan nilai, tetapi ketika bit keputusan adalah 0, kami akan menambahkan nilai di tempat yang tidak kami pedulikan. Berikut kodenya:
// Uji
Kode ini menghabiskan setengah dari tambahan tetapi tidak pernah memiliki kegagalan prediksi cabang. Ini jauh lebih cepat pada data acak daripada versi dengan pernyataan if aktual.
Tetapi dalam pengujian saya, tabel pencarian eksplisit sedikit lebih cepat dari ini, mungkin karena pengindeksan ke tabel pencarian sedikit lebih cepat daripada sedikit pergeseran. Ini menunjukkan bagaimana kode saya mengatur dan menggunakan tabel pencarian (secara imajinatif disebut lut untuk "Tabel Pencarian" dalam kode). Berikut kode C ++:
// Nyatakan dan isi tabel pencarian
Dalam hal ini, tabel pencarian hanya 256 byte, sehingga sangat cocok dalam cache dan semuanya cepat. Teknik ini tidak akan bekerja dengan baik jika datanya bernilai 24-bit dan kami hanya ingin setengah dari mereka ... tabel pencarian akan terlalu besar untuk praktis. Di sisi lain, kita bisa menggabungkan dua teknik yang ditunjukkan di atas: pertama-tama pindahkan bit, lalu indeks tabel pencarian. Untuk nilai 24-bit yang hanya kami inginkan nilai setengahnya, kami berpotensi menggeser data dengan 12 bit, dan dibiarkan dengan nilai 12-bit untuk indeks tabel. Indeks tabel 12-bit menyiratkan tabel nilai 4096, yang mungkin praktis.
Teknik pengindeksan ke dalam array, alih-alih menggunakan pernyataan if, dapat digunakan untuk memutuskan pointer mana yang akan digunakan. Saya melihat perpustakaan yang mengimplementasikan pohon biner, dan bukannya memiliki dua pointer bernama (pLeft dan pRight atau apa pun) memiliki panjang array array-2 dan menggunakan teknik "bit keputusan" untuk memutuskan mana yang akan diikuti. Misalnya, alih-alih:
itu solusi yang bagus mungkin itu akan berhasil
sumber
mask = tmp < 128 : 0 : -1UL;
/total += tmp & mask;