Ringkasan:
Saya mencari cara tercepat untuk menghitung
(int) x / (int) y
tanpa mendapatkan pengecualian untuk y==0
. Sebaliknya saya hanya menginginkan hasil yang sewenang-wenang.
Latar Belakang:
Saat mengkodekan algoritma pemrosesan gambar, saya sering kali perlu membagi dengan nilai alfa (terakumulasi). Varian paling sederhana adalah kode C biasa dengan aritmatika integer. Masalah saya adalah bahwa saya biasanya mendapatkan kesalahan pembagian dengan nol untuk piksel hasil dengan alpha==0
. Namun ini persis piksel di mana hasilnya tidak masalah sama sekali: Saya tidak peduli dengan nilai warna piksel alpha==0
.
Rincian:
Saya mencari sesuatu seperti:
result = (y==0)? 0 : x/y;
atau
result = x / MAX( y, 1 );
x dan y adalah bilangan bulat positif. Kode tersebut dieksekusi berkali-kali dalam loop bersarang, jadi saya mencari cara untuk menyingkirkan percabangan bersyarat.
Ketika y tidak melebihi kisaran byte, saya senang dengan solusinya
unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];
Tapi ini jelas tidak berfungsi dengan baik untuk rentang yang lebih besar.
Saya kira pertanyaan terakhirnya adalah: Apa bit twiddling hack tercepat yang mengubah 0 menjadi nilai integer lainnya, sementara membiarkan semua nilai lain tidak berubah?
Klarifikasi
Saya tidak 100% yakin bahwa percabangan itu terlalu mahal. Namun, kompiler yang berbeda digunakan, jadi saya lebih suka melakukan benchmarking dengan sedikit pengoptimalan (yang memang dipertanyakan).
Yang pasti, kompiler sangat bagus dalam hal bit twiddling, tapi saya tidak bisa mengungkapkan hasil "tidak peduli" di C, jadi kompilator tidak akan pernah bisa menggunakan berbagai optimasi lengkap.
Kode harus sepenuhnya kompatibel dengan C, platform utamanya adalah Linux 64 Bit dengan gcc & clang dan MacOS.
sumber
y += !y
? Tidak ada cabang yang diperlukan untuk menghitungnya. Anda bisa membandingkanx / (y + !y)
terhadapx / max(y, 1)
dan mungkin jugay ? (x/y) : 0
. Saya kira tidak akan ada cabang di salah satu dari mereka, setidaknya dengan pengoptimalan diaktifkan.0
sangat besar dan berdekatan. Ada tempat untuk bermain-main dengan pengoptimalan mikro, dan operasi per piksel adalah tempat yang tepat .Jawaban:
Terinspirasi oleh beberapa komentar yang saya singkirkan dari cabang di Pentium dan
gcc
kompiler saya menggunakanKompilator pada dasarnya mengenali bahwa ia dapat menggunakan tanda kondisi pengujian sebagai tambahan.
Sesuai permintaan perakitan:
Karena ini ternyata pertanyaan dan jawaban yang populer, saya akan menjelaskan sedikit lebih banyak. Contoh di atas didasarkan pada idiom pemrograman yang dikenali oleh compiler. Dalam kasus di atas ekspresi boolean digunakan dalam aritmatika integral dan penggunaan bendera kondisi ditemukan di perangkat keras untuk tujuan ini. Secara umum, flag hanya dapat diakses di C dengan menggunakan idiom. Itulah mengapa sangat sulit untuk membuat pustaka integer presisi multipel portabel di C tanpa menggunakan perakitan (inline). Dugaan saya adalah bahwa kompiler yang paling baik akan memahami idiom di atas.
Cara lain untuk menghindari cabang, seperti yang juga disebutkan dalam beberapa komentar di atas, adalah eksekusi berpredikat. Oleh karena itu, saya mengambil kode pertama philipp dan kode saya dan menjalankannya melalui kompiler dari ARM dan kompiler GCC untuk arsitektur ARM, yang menampilkan eksekusi berpredikat. Kedua kompiler menghindari cabang di kedua contoh kode:
Versi Philipp dengan kompiler ARM:
Versi Philipp dengan GCC:
Kode saya dengan kompiler ARM:
Kode saya dengan GCC:
Semua versi masih memerlukan cabang ke rutinitas divisi, karena versi ARM ini tidak memiliki perangkat keras untuk sebuah divisi, tetapi pengujian untuk
y == 0
sepenuhnya diimplementasikan melalui eksekusi predikat.sumber
constexpr
dan menghindari jenis gips yang tidak perlu seperti ini:template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); }
Dan jika Anda mau255
,(lhs)/(rhs+!rhs) & -!rhs
|
tidak&
. Ooops -( (lhs)/(rhs+!rhs) ) | -!rhs
harus menetapkan nilai untuk0xFFFFFFF
jikarhs
ini0
, danlhs/rhs
jikarhs!=0
.Berikut adalah beberapa angka konkret, di Windows yang menggunakan GCC 4.7.2:
Perhatikan bahwa saya sengaja tidak menelepon
srand()
, sehinggarand()
selalu mengembalikan hasil yang persis sama. Perhatikan juga bahwa-DCHECK=0
hanya menghitung angka nol, sehingga jelas seberapa sering muncul.Sekarang, kompilasi dan pengaturan waktunya dengan berbagai cara:
menunjukkan keluaran yang dapat diringkas dalam sebuah tabel:
Jika nol jarang terjadi,
-DCHECK=2
versi tersebut berkinerja buruk. Saat angka nol mulai muncul lebih banyak,-DCHECK=2
kasing mulai bekerja secara signifikan lebih baik. Dari opsi lain, sebenarnya tidak banyak perbedaan.Karena
-O3
, bagaimanapun, ini adalah cerita yang berbeda:Di sana, centang 2 tidak memiliki kekurangan dibandingkan dengan pemeriksaan lainnya, dan ini menjaga manfaat karena angka nol menjadi lebih umum.
Anda harus benar-benar mengukur untuk melihat apa yang terjadi dengan compiler Anda dan data sampel perwakilan Anda.
sumber
d=0
acak, daripada membuatnya hampir selalud!=0
, dan Anda akan melihat lebih banyak kegagalan prediksi cabang. Prediksi cabang sangat bagus jika satu cabang hampir selalu diikuti, atau jika cabang berikut atau cabang lainnya benar-benard
Iterasi adalah loop dalam, sehinggad == 0
kasus didistribusikan secara merata. Dan apakah membuat 50% kasusd == 0
realistis?0.002%
kasusd==0
realistis? Mereka didistribusikan ke seluruh, setiap 65000 iterasi yang Anda lakukan padad==0
kasus Anda . Meskipun50%
mungkin tidak sering terjadi,10%
atau1%
dapat dengan mudah terjadi, atau bahkan90%
atau99%
. Tes yang ditampilkan hanya benar-benar menguji "jika pada dasarnya Anda tidak pernah, pernah turun ke cabang, apakah prediksi cabang membuat penghapusan cabang tidak berguna?", Yang jawabannya adalah "ya, tapi itu tidak menarik".Tanpa mengetahui platformnya, tidak ada cara untuk mengetahui metode paling efisien yang tepat, namun, pada sistem umum ini mungkin mendekati optimal (menggunakan sintaks assembler Intel):
(asumsikan pembagi masuk
ecx
dan pembagi masukeax
)Empat instruksi siklus tunggal yang tidak bercabang ditambah pembagian. Hasil bagi akan masuk
eax
dan sisanya akan masukedx
di akhir. (Jenis ini menunjukkan mengapa Anda tidak ingin mengirim kompiler untuk melakukan pekerjaan pria).sumber
Menurut tautan ini , Anda cukup memblokir sinyal SIGFPE dengan
sigaction()
(Saya belum mencobanya sendiri, tetapi saya yakin ini akan berfungsi).Ini adalah pendekatan tercepat yang mungkin jika kesalahan bagi dengan nol sangat jarang terjadi: Anda hanya membayar untuk divisi dengan nol, bukan untuk divisi yang valid, jalur eksekusi normal tidak berubah sama sekali.
Namun, OS akan terlibat dalam setiap pengecualian yang diabaikan, yang mahal harganya. Saya pikir, Anda harus memiliki setidaknya seribu divisi bagus per divisi dengan nol yang Anda abaikan. Jika pengecualian lebih sering dari itu, Anda mungkin akan membayar lebih dengan mengabaikan pengecualian daripada dengan memeriksa setiap nilai sebelum pembagian.
sumber