Saya mencari cara yang efisien untuk menentukan posisi bit paling tidak signifikan yang diatur dalam bilangan bulat, misalnya untuk 0x0FF0 akan menjadi 4.
Implementasi sepele adalah ini:
unsigned GetLowestBitPos(unsigned value)
{
assert(value != 0); // handled separately
unsigned pos = 0;
while (!(value & 1))
{
value >>= 1;
++pos;
}
return pos;
}
Ada ide bagaimana memeras beberapa siklus darinya?
(Catatan: pertanyaan ini untuk orang-orang yang menikmati hal-hal seperti itu, bukan untuk orang-orang yang mengatakan bahwa optimasi xyz itu jahat.)
[Sunting] Terima kasih semua orang atas idenya! Saya juga telah mempelajari beberapa hal lain. Keren!
c++
c
optimization
bit-manipulation
peterchen
sumber
sumber
Jawaban:
Bit Twiddling Hacks menawarkan koleksi yang sangat baik dari, er, bit twiddling hacks, dengan diskusi kinerja / pengoptimalan terlampir. Solusi favorit saya untuk masalah Anda (dari situs itu) adalah «perbanyak dan cari»:
Referensi yang berguna:
sumber
__builtin_ffsl
atauffsl
?Mengapa tidak menggunakan ffs bawaan ? (Saya mengambil halaman manual dari Linux, tetapi lebih banyak tersedia dari itu.)
sumber
Ada instruksi assembly x86 (
bsf
) yang akan melakukannya. :)Lebih dioptimalkan ?!
Catatan Samping:
Pengoptimalan pada level ini secara inheren bergantung pada arsitektur. Prosesor saat ini terlalu kompleks (dalam hal prediksi cabang, cache miss, pipelining) sehingga sangat sulit untuk memprediksi kode mana yang dieksekusi lebih cepat pada arsitektur mana. Mengurangi operasi dari 32 menjadi 9 atau hal-hal seperti itu bahkan dapat menurunkan kinerja pada beberapa arsitektur. Kode yang dioptimalkan pada satu arsitektur dapat menghasilkan kode yang lebih buruk di arsitektur lain. Saya pikir Anda akan mengoptimalkan ini untuk CPU tertentu atau membiarkannya apa adanya dan membiarkan kompiler memilih apa yang menurutnya lebih baik.
sumber
Kebanyakan arsitektur modern memiliki beberapa instruksi untuk menemukan posisi bit set terendah, atau bit set tertinggi, atau menghitung jumlah nol di depan, dll.
Jika Anda memiliki satu instruksi dari kelas ini, Anda dapat dengan murah meniru yang lain.
Luangkan waktu sejenak untuk mengerjakannya di atas kertas dan sadari bahwa itu
x & (x-1)
akan menghapus bit set terendah dalam x, dan( x & ~(x-1) )
hanya akan mengembalikan bit set terendah, terlepas dari arsitektur, panjang kata, dll. Mengetahui hal ini, sangat mudah menggunakan perangkat keras count-leading -zeroes / tertinggi-set-bit untuk menemukan bit set terendah jika tidak ada instruksi eksplisit untuk melakukannya.Jika tidak ada dukungan perangkat keras yang relevan sama sekali, implementasi multiply-and-lookup dari count-leading-zero yang diberikan di sini atau salah satu yang ada di halaman Bit Twiddling Hacks dapat dengan mudah dikonversi untuk memberikan bit set terendah menggunakan identitas di atas dan memiliki keuntungan karena tidak memiliki cabang.
sumber
Weee, banyak solusi dan bukan patokan yang terlihat. Kalian harus malu pada dirimu sendiri ;-)
Mesin saya adalah Intel i530 (2,9 GHz), menjalankan Windows 7 64-bit. Saya mengkompilasi dengan versi 32-bit dari MinGW.
Kode saya:
sumber
BSF
Memiliki ketergantungan palsu pada outputnya (karena perilaku sebenarnya ketika input = 0 adalah membiarkan output tidak berubah). sayangnya gcc mengubahnya menjadi dependensi yang dibawa loop dengan tidak membersihkan register di antara iterasi loop. Jadi loop harus berjalan pada satu per 5 siklus, terhambat pada BSF (3) + CMOV (2) latensi.ffs()
seharusnya memiliki throughput satu per jam (3 uops, 1 untuk BSF dan 2 untuk CMOV, dan mereka dapat berjalan di port yang berbeda). Dengan overhead loop yang sama, 7 ALU uops yang dapat berjalan (pada CPU Anda) pada 3 jam. Overhead mendominasi! Sumber: agner.org/optimizebsf ecx, [ebx+edx*4]
tidak diperlakukanecx
sebagai input yang harus ditunggu. (ECX terakhir ditulis oleh CMOV iterasi sebelumnya). Tetapi CPU berperilaku seperti itu, untuk mengimplementasikan perilaku "biarkan dest tidak dimodifikasi jika sumbernya nol" (jadi ini bukan benar-benar dep palsu seperti untuk TZCNT; ketergantungan data diperlukan karena tidak ada eksekusi spekulatif + percabangan pada asumsi bahwa masukannya bukan nol). Kita bisa mengatasinya dengan menambahkanxor ecx,ecx
beforebsf
, untuk memutus ketergantungan pada ECX.Solusi tercepat (non-intrinsik / non-assembler) untuk ini adalah menemukan byte terendah dan kemudian menggunakan byte itu dalam tabel pencarian 256 entri. Ini memberi Anda kinerja kasus terburuk dari empat instruksi bersyarat dan kasus terbaik 1. Tidak hanya ini jumlah instruksi yang paling sedikit, tetapi juga jumlah cabang yang paling sedikit yang sangat penting pada perangkat keras modern.
Tabel Anda (256 entri 8-bit) harus berisi indeks LSB untuk setiap nomor dalam kisaran 0-255. Anda memeriksa setiap byte dari nilai Anda dan menemukan byte bukan nol terendah, lalu gunakan nilai ini untuk mencari indeks sebenarnya.
Ini memang membutuhkan memori 256-byte, tetapi jika kecepatan fungsi ini sangat penting maka 256-byte itu sangat berharga,
Misalnya
sumber
OMG baru saja berputar.
Yang kurang dari sebagian besar contoh ini adalah sedikit pemahaman tentang cara kerja semua perangkat keras.
Setiap kali Anda memiliki cabang, CPU harus menebak cabang mana yang akan diambil. Pipa instruksi dimuat dengan instruksi yang mengarah ke jalur yang ditebak. Jika CPU salah menebak maka pipa instruksi akan dibilas, dan cabang lainnya harus dimuat.
Pertimbangkan loop sementara di bagian atas. Tebakannya adalah tetap berada dalam lingkaran. Ini akan salah setidaknya sekali ketika meninggalkan lingkaran. Ini AKAN menyiram pipa instruksi. Perilaku ini sedikit lebih baik daripada menebak bahwa ia akan meninggalkan loop, dalam hal ini ia akan membuang pipa instruksi pada setiap iterasi.
Jumlah siklus CPU yang hilang sangat bervariasi dari satu jenis prosesor ke prosesor berikutnya. Tetapi Anda dapat mengharapkan antara 20 dan 150 siklus CPU yang hilang.
Grup lebih buruk berikutnya adalah di mana Anda berpikir Anda akan menyimpan beberapa iterasi dengan membagi nilai menjadi potongan-potongan yang lebih kecil dan menambahkan beberapa cabang lagi. Masing-masing cabang ini menambahkan peluang tambahan untuk menyiram pipa instruksi dan menghabiskan 20 hingga 150 siklus jam lagi.
Mari kita pertimbangkan apa yang terjadi ketika Anda mencari nilai dalam tabel. Kemungkinan nilainya saat ini tidak ada di cache, setidaknya bukan pertama kali fungsi Anda dipanggil. Artinya, CPU terhenti saat nilainya dimuat dari cache. Sekali lagi ini bervariasi dari satu mesin ke mesin berikutnya. Chip Intel yang baru benar-benar menggunakan ini sebagai kesempatan untuk menukar utas sementara utas saat ini sedang menunggu pemuatan cache selesai. Ini bisa dengan mudah menjadi lebih mahal daripada penyiraman pipa instruksi, namun jika Anda melakukan operasi ini beberapa kali kemungkinan hanya terjadi sekali.
Jelas solusi waktu konstan tercepat adalah yang melibatkan matematika deterministik. Solusi murni dan elegan.
Saya mohon maaf jika ini sudah ditutup.
Setiap kompiler yang saya gunakan, kecuali XCODE AFAIK, memiliki intrinsik kompiler untuk forward bitscan dan reverse bitscan. Ini akan mengkompilasi ke instruksi perakitan tunggal pada sebagian besar perangkat keras tanpa Cache Miss, tanpa Cabang Miss-Prediction dan Tidak ada pemrogram lain yang menghasilkan batu sandungan.
Untuk kompiler Microsoft, gunakan _BitScanForward & _BitScanReverse.
Untuk GCC, gunakan __builtin_ffs, __builtin_clz, __builtin_ctz.
Selain itu, mohon jangan memposting jawaban dan berpotensi menyesatkan pendatang baru jika Anda tidak memiliki cukup pengetahuan tentang subjek yang sedang dibahas.
Maaf saya benar-benar lupa memberikan solusi .. Ini adalah kode yang saya gunakan di iPad yang tidak memiliki instruksi tingkat perakitan untuk tugas tersebut:
Hal yang perlu dipahami di sini adalah bahwa bukan pembanding yang mahal, tetapi cabang yang muncul setelah pembandingan. Perbandingan dalam kasus ini dipaksa menjadi nilai 0 atau 1 dengan .. == 0, dan hasilnya digunakan untuk menggabungkan matematika yang akan terjadi di kedua sisi cabang.
Edit:
Kode di atas rusak total. Kode ini berfungsi dan masih bebas cabang (jika dioptimalkan):
Ini mengembalikan -1 jika diberikan 0. Jika Anda tidak peduli tentang 0 atau senang mendapatkan 31 untuk 0, hapus kalkulasi i0, menghemat waktu.
sumber
-O3
godbolt.org/z/gcsUHdTerinspirasi oleh posting serupa ini yang melibatkan pencarian sedikit, saya menawarkan yang berikut:
Kelebihan:
Kekurangan:
Pembaruan: Seperti yang ditunjukkan di komentar, serikat pekerja adalah implementasi yang lebih bersih (untuk C, setidaknya) dan akan terlihat seperti:
Ini mengasumsikan int 32-bit dengan penyimpanan little-endian untuk semuanya (pikirkan prosesor x86).
sumber
int
adalahint32_t
, dan bahwa pergeseran kanan menandatangani adalah pergeseran aritmatika (di C ++ itu pelaksanaan yang ditetapkan)Ini dapat dilakukan dengan kasus terburuk kurang dari 32 operasi:
Prinsip: Memeriksa 2 bit atau lebih sama efisiennya dengan memeriksa 1 bit.
Jadi misalnya tidak ada yang menghentikan Anda untuk memeriksa pengelompokan mana yang pertama, kemudian memeriksa setiap bit dari yang terkecil hingga terbesar dalam kelompok itu.
Jadi ...
jika Anda memeriksa 2 bit sekaligus, Anda memiliki kasus terburuk (Nbits / 2) + 1 total pemeriksaan.
jika Anda memeriksa 3 bit pada satu waktu yang Anda miliki dalam kasus terburuk (Nbits / 3) + 2 pemeriksaan total.
...
Optimal akan memeriksa dalam kelompok 4. Yang akan membutuhkan dalam kasus terburuk 11 operasi alih-alih 32 Anda.
Kasus terbaik beralih dari 1 pemeriksaan algoritme Anda ke 2 pemeriksaan jika Anda menggunakan ide pengelompokan ini. Tetapi 1 cek ekstra dalam kasus terbaik itu sepadan untuk penghematan kasus terburuk.
Catatan: Saya menuliskannya secara penuh daripada menggunakan loop karena cara itu lebih efisien.
sumber
Mengapa tidak menggunakan pencarian biner ? Ini akan selalu selesai setelah 5 operasi (dengan asumsi ukuran int 4 byte):
sumber
Metode lain (pembagian modulus dan pencarian) layak mendapat perhatian khusus di sini dari tautan yang sama yang disediakan oleh @ anton-tykhyy. Metode ini sangat mirip dalam performanya dengan metode penggandaan dan pencarian DeBruijn dengan sedikit perbedaan namun penting.
divisi modulus dan pencarian
pembagian modulus dan metode pencarian mengembalikan nilai yang berbeda untuk v = 0x00000000 dan v = FFFFFFFF sedangkan metode perkalian dan pencarian DeBruijn mengembalikan nol pada kedua input.
uji:-
sumber
mod
lambat. Sebagai gantinya, Anda dapat menggunakan metode perkalian-dan-pencarian asli dan kurangi!v
darir
untuk menangani kasus tepi.Menurut halaman BitScan Pemrograman Catur dan pengukuran saya sendiri, kurangi dan xor lebih cepat daripada negate dan mask.
(Perhatikan daripada jika Anda akan menghitung nol di belakangnya
0
, metode yang saya miliki mengembalikannya63
sedangkan negate dan mask kembali0
.)Berikut adalah pengurangan 64-bit dan xor:
Untuk referensi, berikut adalah versi 64-bit dari metode negate and mask:
sumber
(v ^ (v-1))
bekerja disediakanv != 0
. Dalam kasusv == 0
mengembalikan 0xFF .... FF sementara(v & -v)
memberikan nol (yang omong-omong salah, juga, buf setidaknya itu mengarah ke hasil yang wajar).v ^ (v-1)
, jadi tidak ada yang membedakan keduanya. Dalam skenario saya, nol tidak akan pernah menjadi masukan.Anda dapat memeriksa apakah ada bit urutan bawah yang disetel. Jika demikian maka lihat urutan bawah dari bit yang tersisa. misalnya,:
32bit int - periksa apakah salah satu dari 16 pertama disetel. Jika demikian, periksa apakah salah satu dari 8 yang pertama telah disetel. jika begitu, ....
jika tidak, periksa apakah salah satu dari 16 di atas sudah diatur ..
Pada dasarnya ini adalah pencarian biner.
sumber
Lihat jawaban saya di sini untuk mengetahui cara melakukannya dengan satu instruksi x86, kecuali bahwa untuk menemukan bit set yang paling tidak signifikan, Anda akan menginginkan
BSF
instruksi ("bit scan forward") daripadaBSR
dijelaskan di sana.sumber
Namun solusi lain, mungkin bukan yang tercepat, tetapi tampaknya cukup bagus.
Setidaknya tidak memiliki cabang. ;)
sumber
1
dari 1 yang paling tidak signifikan hingga LSB, gunakan((x & -x) - 1) << 1
sebagai gantinyax ^ (x-1)
50% dari semua nomor akan ditampilkan di baris pertama kode.
75% dari semua angka akan kembali pada 2 baris kode pertama.
87% dari semua angka akan kembali dalam 3 baris kode pertama.
94% dari semua angka akan kembali dalam 4 baris kode pertama.
97% dari semua angka akan kembali dalam 5 baris kode pertama.
dll.
Saya pikir orang-orang yang mengeluh tentang betapa tidak efisiennya skenario kasus terburuk untuk kode ini tidak memahami betapa langka kondisi itu akan terjadi.
sumber
Menemukan trik pintar ini menggunakan 'topeng ajaib' dalam "Seni pemrograman, bagian 4", yang melakukannya dalam waktu O (log (n)) untuk bilangan n-bit. [dengan log (n) spasi ekstra]. Solusi khas yang memeriksa bit set adalah O (n) atau membutuhkan O (n) ruang ekstra untuk tabel pencarian, jadi ini adalah kompromi yang baik.
Masker ajaib:
Ide kunci: Tidak ada angka nol di belakang di x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ...
sumber
Jika C ++ 11 tersedia untuk Anda, terkadang kompiler dapat melakukan tugas tersebut untuk Anda :)
Hasilnya adalah indeks berbasis 1.
sumber
ffs()
pada waktu kompilasi, jadi Anda tidak perlu menggunakan ini agar propagasi konstan berfungsi. (Anda harus menghindari inline-asm, tentu saja.) Jika Anda benar-benar membutuhkan sesuatu yang bekerja sebagai C ++ 11constexpr
, Anda masih dapat menggunakan GNU C__builtin_ffs
.Ini sehubungan dengan jawaban @Anton Tykhyy
Berikut adalah implementasi constexpr C ++ 11 saya menghilangkan gips dan menghapus peringatan pada VC ++ 17 dengan memotong hasil 64bit menjadi 32 bit:
Untuk mengatasi masalah 0x1 dan 0x0, keduanya mengembalikan 0, Anda dapat melakukan:
tetapi jika kompilator tidak dapat atau tidak mau melakukan praproses, panggilan itu akan menambahkan beberapa siklus ke kalkulasi.
Terakhir, jika tertarik, berikut daftar statik yang menegaskan untuk memeriksa bahwa kode melakukan apa yang dimaksudkan untuk:
sumber
Berikut ini satu alternatif sederhana, meskipun mencari log agak mahal.
sumber
baru-baru ini saya melihat bahwa perdana menteri singapura memposting program yang dia tulis di facebook, ada satu baris untuk menyebutkannya ..
Logikanya hanyalah "nilai & -nilai", misalkan Anda memiliki 0x0FF0, lalu, 0FF0 & (F00F + 1), yang sama dengan 0x0010, itu berarti 1 terendah ada di bit ke-4 .. :)
sumber
Jika Anda memiliki sumber daya, Anda dapat mengorbankan memori untuk meningkatkan kecepatan:
Catatan: Tabel ini akan menghabiskan setidaknya 4 GB (16 GB jika kita membiarkan tipe pengembalian sebagai
unsigned
). Ini adalah contoh perdagangan satu sumber daya terbatas (RAM) dengan yang lain (kecepatan eksekusi).Jika fungsi Anda perlu tetap portabel dan berjalan secepat mungkin dengan biaya berapa pun, ini adalah cara yang tepat. Di sebagian besar aplikasi dunia nyata, tabel 4GB tidak realistis.
sumber
:)
@Dan: Anda benar tentang cache memori. Lihat komentar Mikeage di atas.