Saya sudah berusaha mempelajari C di waktu luang saya, dan bahasa lain (C #, Java, dll.) Memiliki konsep yang sama (dan seringkali operator yang sama) ...
Yang saya bertanya-tanya adalah, di tingkat inti, apa bit-pergeseran ( <<
, >>
, >>>
) lakukan, masalah apa yang bisa ia membantu memecahkan, dan apa Gotchas mengintai di tikungan? Dengan kata lain, panduan pemula mutlak untuk sedikit mengubah semua kebaikannya.
operators
bit-manipulation
bit-shift
binary-operators
John Rudy
sumber
sumber
Jawaban:
Operator penggeser bit melakukan persis seperti namanya. Mereka menggeser bit. Berikut adalah pengantar singkat (atau tidak terlalu singkat) untuk operator shift yang berbeda.
Operator
>>
adalah operator pergeseran kanan aritmatika (atau ditandatangani).>>>
adalah operator shift kanan yang logis (atau tidak ditandatangani).<<
adalah operator shift kiri, dan memenuhi kebutuhan shift logis dan aritmatika.Semua operator ini dapat diterapkan untuk nilai integer (
int
,long
, mungkinshort
danbyte
atauchar
). Dalam beberapa bahasa, menerapkan operator shift ke semua tipe data yang lebih kecil dariint
secara otomatis mengubah ukuran operan menjadiint
.Perhatikan bahwa
<<<
itu bukan operator, karena itu akan berlebihan.Perhatikan juga bahwa C dan C ++ tidak membedakan antara operator shift kanan . Mereka hanya menyediakan
>>
operator, dan perilaku pengalihan kanan adalah implementasi yang ditentukan untuk tipe yang ditandatangani. Sisa jawabannya menggunakan operator C # / Java.(Dalam semua utama C dan C ++ implementasi termasuk GCC dan dentang / LLVM,
>>
pada jenis ditandatangani adalah aritmatika Beberapa kode mengasumsikan ini, tapi itu bukan sesuatu yang jaminan standar Ini bukan.. Terdefinisi , meskipun; standar membutuhkan implementasi untuk menentukan salah satu cara atau lainnya.Namun, meninggalkan pergeseran nomor bertanda negatif adalah perilaku yang tidak terdefinisi (ditandatangani integer overflow). Jadi, kecuali Anda memerlukan aritmatika pergeseran kanan, biasanya merupakan ide yang baik untuk melakukan bit-shifting Anda dengan tipe yang tidak ditandai.)Shift kiri (<<)
Integer disimpan, dalam memori, sebagai serangkaian bit. Misalnya, nomor 6 yang disimpan sebagai 32-bit
int
adalah:Menggeser pola bit ini ke posisi satu kiri (
6 << 1
) akan menghasilkan angka 12:Seperti yang Anda lihat, angka telah bergeser ke kiri dengan satu posisi, dan digit terakhir di sebelah kanan diisi dengan nol. Anda mungkin juga mencatat bahwa menggeser ke kiri setara dengan perkalian dengan kekuatan 2. Begitu
6 << 1
juga dengan6 * 2
, dan6 << 3
setara dengan6 * 8
. Kompilator pengoptimal yang baik akan menggantikan perkalian dengan shift jika memungkinkan.Pergeseran non-lingkaran
Harap dicatat bahwa ini bukan pergeseran melingkar. Menggeser nilai ini ke kiri dengan satu posisi (
3,758,096,384 << 1
):hasil dalam 3.221.225.472:
Digit yang digeser "dari ujung" hilang. Itu tidak membungkus.
Pergeseran kanan logis (>>>)
Pergeseran kanan logis adalah kebalikan dari pergeseran kiri. Alih-alih memindahkan bit ke kiri, mereka hanya bergerak ke kanan. Misalnya, menggeser angka 12:
ke kanan dengan satu posisi (
12 >>> 1
) akan mengembalikan 6 asli kami:Jadi kita melihat bahwa bergeser ke kanan setara dengan pembagian dengan kekuatan 2.
Bit hilang hilang
Namun, pergeseran tidak bisa mendapatkan kembali bit yang "hilang". Misalnya, jika kita menggeser pola ini:
ke 4 posisi kiri (
939,524,102 << 4
), kita mendapatkan 2.147.483.744:dan kemudian bergeser kembali (
(939,524,102 << 4) >>> 4
) kita dapatkan 134.217.734:Kami tidak bisa mendapatkan kembali nilai asli kami setelah kehilangan bit.
Aritmatika bergeser kanan (>>)
Pergeseran kanan aritmatika persis seperti pergeseran kanan logis, kecuali alih-alih bantalan dengan nol, itu bergeser dengan bit yang paling signifikan. Ini karena bit yang paling signifikan adalah bit tanda , atau bit yang membedakan angka positif dan negatif. Dengan mengisi dengan bit yang paling signifikan, pergeseran kanan aritmatika adalah mempertahankan tanda.
Misalnya, jika kita menafsirkan pola bit ini sebagai angka negatif:
kami memiliki nomor -2.147.483.552. Menggeser ini ke posisi 4 kanan dengan perubahan aritmatika (-2,147.483.552 >> 4) akan memberi kita:
atau angka -134.217.722.
Jadi kita melihat bahwa kita telah mempertahankan tanda angka negatif kita dengan menggunakan pergeseran kanan aritmatika, daripada pergeseran kanan logis. Dan sekali lagi, kita melihat bahwa kita melakukan pembagian dengan kekuatan 2.
sumber
A good optimizing compiler will substitute shifts for multiplications when possible.
Apa? Bitshifts adalah lipat lebih cepat ketika datang ke operasi tingkat rendah dari CPU, baik mengoptimalkan compiler akan melakukan hal yang tepat berlawanan, yaitu, mengubah perkalian biasa dengan kekuatan dua menjadi sedikit pergeseran.Katakanlah kita memiliki satu byte:
Menerapkan satu bithift kiri memberi kita:
Nol paling kiri digeser keluar dari byte, dan nol baru ditambahkan ke ujung kanan byte.
Bit tidak terguling; mereka dibuang. Itu berarti jika Anda meninggalkan shift 1101100 dan kemudian shift kanan, Anda tidak akan mendapatkan hasil yang sama kembali.
Pergeseran ditinggalkan oleh N setara dengan mengalikan dengan 2 N .
Menggeser kanan oleh N adalah (jika Anda menggunakan komplemen yang ) adalah sama dengan membagi dengan 2 N dan pembulatan ke nol.
Bitshifting dapat digunakan untuk perkalian dan pembagian yang sangat cepat, asalkan Anda bekerja dengan kekuatan 2. Hampir semua rutinitas grafis tingkat rendah menggunakan bitshifting.
Misalnya, pada masa lalu, kami menggunakan mode 13j (320x200 256 warna) untuk game. Dalam Mode 13h, memori video diletakkan berurutan per piksel. Itu berarti menghitung lokasi untuk piksel, Anda akan menggunakan matematika berikut:
Sekarang, pada zaman itu, kecepatan sangat penting, jadi kami akan menggunakan bithift untuk melakukan operasi ini.
Namun, 320 bukan kekuatan dua, jadi untuk mengatasi ini kita harus mencari tahu apa kekuatan dua yang ditambahkan bersama membuat 320:
Sekarang kita bisa mengubahnya menjadi shift kiri:
Untuk hasil akhir dari:
Sekarang kita mendapatkan offset yang sama seperti sebelumnya, kecuali alih-alih operasi perkalian yang mahal, kita menggunakan dua bithifts ... di x86 itu akan menjadi sesuatu seperti ini (catatan, sudah selamanya sejak saya melakukan perakitan (catatan editor: dikoreksi beberapa kesalahan dan menambahkan contoh 32-bit)):
Total: 28 siklus pada CPU kuno apa pun yang memiliki timing ini.
Vrs
12 siklus pada CPU kuno yang sama.
Ya, kami akan bekerja keras untuk mengurangi 16 siklus CPU.
Dalam mode 32 atau 64-bit, kedua versi menjadi jauh lebih pendek dan lebih cepat. CPU eksekusi modern out-of-order seperti Intel Skylake (lihat http://agner.org/optimize/ ) memiliki perkalian perangkat keras yang sangat cepat (latensi rendah dan throughput tinggi), sehingga keuntungannya jauh lebih kecil. AMD Bulldozer-family sedikit lebih lambat, terutama untuk 64-bit. Pada CPU Intel, dan AMD Ryzen, dua shift latensi sedikit lebih rendah tetapi lebih banyak instruksi daripada multiply (yang dapat menyebabkan throughput lebih rendah):
vs.
Compiler akan melakukan ini untuk Anda: Lihat bagaimana GCC, Dentang, dan Microsoft Visual C ++ semua menggunakan shift + lea saat mengoptimalkan
return 320*row + col;
.Hal yang paling menarik untuk dicatat di sini adalah bahwa x86 memiliki instruksi shift-and-add (
LEA
) yang dapat melakukan shift kiri kecil dan menambahkan pada saat bersamaan, dengan kinerja sebagaiadd
instruksi. ARM bahkan lebih kuat: satu operan dari instruksi apa pun dapat digeser ke kiri atau kanan secara gratis. Jadi penskalaan oleh konstanta waktu-kompilasi yang dikenal sebagai kekuatan-2 bisa lebih efisien daripada perkalian.OK, kembali ke zaman modern ... sesuatu yang lebih berguna sekarang adalah menggunakan bitshifting untuk menyimpan dua nilai 8-bit dalam integer 16-bit. Misalnya, dalam C #:
Dalam C ++, kompiler harus melakukan ini untuk Anda jika Anda menggunakan anggota
struct
dengan dua 8-bit, tetapi dalam praktiknya mereka tidak selalu.sumber
c=4*d
Anda akan mendapat perubahan. Jika Anda menulisk = (n<0)
itu dapat dilakukan dengan shift juga:k = (n>>31)&1
untuk menghindari cabang. Intinya, peningkatan kepintaran kompiler ini berarti sekarang tidak perlu menggunakan trik-trik ini dalam kode C, dan mereka mengkompromikan keterbacaan dan portabilitas. Masih sangat bagus untuk mengenal mereka jika Anda sedang menulis misalnya kode vektor SSE; atau situasi apa pun di mana Anda membutuhkannya dengan cepat dan ada trik yang tidak digunakan kompiler (mis. kode GPU).if(x >= 1 && x <= 9)
yang sangat umum adalah yang dapat dilakukan karenaif( (unsigned)(x-1) <=(unsigned)(9-1))
Mengubah dua tes bersyarat menjadi satu bisa menjadi keuntungan kecepatan besar; terutama ketika itu memungkinkan eksekusi yang ditentukan alih-alih cabang. Saya menggunakan ini selama bertahun-tahun (di mana dibenarkan) sampai saya perhatikan sekitar 10 tahun yang lalu bahwa kompiler mulai melakukan transformasi ini dalam pengoptimal, kemudian saya berhenti. Masih bagus untuk diketahui, karena ada situasi serupa di mana kompiler tidak dapat melakukan transformasi untuk Anda. Atau jika Anda sedang mengerjakan kompiler.Operasi bitwise, termasuk bit shift, merupakan hal mendasar untuk perangkat keras tingkat rendah atau pemrograman tertanam. Jika Anda membaca spesifikasi untuk perangkat atau bahkan beberapa format file biner, Anda akan melihat byte, kata-kata, dan kata-kata, yang dipecah menjadi bitfield yang tidak sejajar, yang berisi berbagai nilai menarik. Mengakses bit-field ini untuk membaca / menulis adalah penggunaan yang paling umum.
Contoh nyata yang sederhana dalam pemrograman grafis adalah pixel 16-bit direpresentasikan sebagai berikut:
Untuk mendapatkan nilai hijau, Anda akan melakukan ini:
Penjelasan
Untuk mendapatkan nilai HANYA hijau, yang dimulai pada offset 5 dan berakhir pada 10 (yaitu panjang 6-bit), Anda perlu menggunakan masker (bit), yang bila diterapkan terhadap seluruh piksel 16-bit, akan menghasilkan hanya bit yang kami minati.
Topeng yang sesuai adalah 0x7E0 yang dalam biner adalah 0000011111100000 (yang 2016 dalam desimal).
Untuk menerapkan masker, Anda menggunakan operator DAN (&).
Setelah menerapkan topeng, Anda akan berakhir dengan angka 16-bit yang benar-benar hanya angka 11-bit karena MSB-nya berada di bit ke-11. Hijau sebenarnya hanya panjang 6-bit, jadi kita perlu menurunkannya menggunakan shift kanan (11 - 6 = 5), maka penggunaan 5 sebagai offset (
#define GREEN_OFFSET 5
).Juga umum menggunakan bit shift untuk perkalian dan pembagian cepat dengan kekuatan 2:
sumber
Bit Masking & Shifting
Bit shifting sering digunakan dalam pemrograman grafis tingkat rendah. Misalnya, nilai warna piksel yang diberikan dikodekan dalam kata 32-bit.
Untuk pemahaman yang lebih baik, nilai biner yang sama dilabeli dengan bagian apa yang mewakili bagian warna apa.
Katakanlah misalnya kita ingin mendapatkan nilai hijau dari warna piksel ini. Kita bisa dengan mudah mendapatkan nilai itu dengan menutupi dan mengubah .
Topeng kami:
&
Operator logis memastikan bahwa hanya nilai-nilai di mana topeng 1 disimpan. Hal terakhir yang harus kita lakukan sekarang, adalah mendapatkan nilai integer yang benar dengan menggeser semua bit ke kanan sebanyak 16 tempat (logical right shift) .Demikian juga, kami memiliki bilangan bulat yang mewakili jumlah hijau dalam warna piksel:
Ini sering digunakan untuk encoding atau decoding format gambar seperti
jpg
,png
, dllsumber
Satu gotcha adalah bahwa yang berikut ini tergantung pada implementasi (menurut standar ANSI):
x sekarang bisa menjadi 127 (01111111) atau masih -1 (11111111).
Dalam praktiknya, biasanya yang terakhir.
sumber
Saya hanya menulis tip dan trik. Mungkin bermanfaat dalam ujian dan ujian.
n = n*2
:n = n<<1
n = n/2
:n = n>>1
!(n & (n-1))
n
:n |= (1 << x)
x&1 == 0
(genap)x ^ (1<<n)
sumber
Perhatikan bahwa dalam implementasi Java, jumlah bit yang diubah diubah oleh ukuran sumber.
Sebagai contoh:
sama dengan 2. Anda mungkin berharap menggeser bit ke kanan 65 kali akan nol semuanya, tapi itu sebenarnya setara dengan:
Ini berlaku untuk <<, >>, dan >>>. Saya belum mencobanya dalam bahasa lain.
sumber
gcc 5.4.0
memberi peringatan, tetapi memberi2
untuk 5 >> 65; demikian juga.Beberapa operasi bit / manipulasi yang berguna dalam Python.
Saya menerapkan jawaban Ravi Prakash dengan Python.
sumber
Ketahuilah bahwa hanya versi PHP 32 bit yang tersedia di platform Windows.
Kemudian jika Anda misalnya menggeser << atau >> lebih dari 31 bit, hasilnya tidak dapat diharapkan. Biasanya angka asli bukan nol akan dikembalikan, dan itu bisa menjadi bug yang sangat rumit.
Tentu saja jika Anda menggunakan PHP versi 64 bit (Unix), Anda harus menghindari pergeseran lebih dari 63 bit. Namun, misalnya, MySQL menggunakan BIGINT 64-bit, jadi seharusnya tidak ada masalah kompatibilitas.
PEMBARUAN: Dari PHP 7 Windows, PHP build akhirnya dapat menggunakan integer 64 bit penuh: Ukuran integer bergantung pada platform, meskipun nilai maksimum sekitar dua miliar adalah nilai biasa (yang ditandatangani 32 bit). Platform 64-bit biasanya memiliki nilai maksimum sekitar 9E18, kecuali pada Windows sebelum PHP 7, di mana selalu 32 bit.
sumber