Nehmen wir an, wir haben ein einzelnes Byte:
0110110
Mit einer einzigen Bitverschiebung nach links erhalten wir:
1101100
Die äußerste linke Null wurde aus dem Byte herausgeschoben, und eine neue Null wurde am rechten Ende des Bytes angehängt.
Die Bits rollen nicht um, sondern werden verworfen. Das heißt, wenn Sie 1101100 nach links schieben und dann nach rechts schieben, erhalten Sie nicht das gleiche Ergebnis zurück.
Eine Linksverschiebung um N ist gleichbedeutend mit einer Multiplikation mit 2 N .
Die Rechtsverschiebung um N ist (bei Verwendung von Einerkomplement ) ist das Äquivalent zur Division durch 2 N und Rundung auf Null.
Bitshifting kann für wahnsinnig schnelle Multiplikationen und Divisionen verwendet werden, vorausgesetzt man arbeitet mit einer Potenz von 2. Fast alle Low-Level-Grafikroutinen verwenden Bitshifting.
Früher haben wir zum Beispiel den Modus 13h (320x200 256 Farben) für Spiele verwendet. Im Modus 13h wurde der Videospeicher sequentiell pro Pixel angelegt. Das bedeutete, dass man zur Berechnung des Speicherplatzes eines Pixels die folgende Rechnung anstellen musste:
memoryOffset = (row * 320) + column
Damals war die Geschwindigkeit entscheidend, deshalb haben wir für diesen Vorgang Bitshifts verwendet.
Da 320 jedoch keine Zweierpotenz ist, müssen wir herausfinden, was eine Zweierpotenz ist, die zusammengenommen 320 ergibt:
(row * 320) = (row * 256) + (row * 64)
Jetzt können wir das in Linksverschiebungen umwandeln:
(row * 320) = (row << 8) + (row << 6)
Für ein Endergebnis von:
memoryOffset = ((row << 8) + (row << 6)) + column
Jetzt erhalten wir denselben Offset wie zuvor, nur dass wir statt einer teuren Multiplikationsoperation die beiden Bitshifts verwenden... in x86 würde das ungefähr so aussehen (Anmerkung: Es ist ewig her, dass ich Assembler gemacht habe (Anmerkung des Herausgebers: ein paar Fehler korrigiert und ein 32-Bit-Beispiel hinzugefügt)):
mov ax, 320; 2 cycles
mul word [row]; 22 CPU Cycles
mov di,ax; 2 cycles
add di, [column]; 2 cycles
; di = [row]*320 + [column]
; 16-bit addressing mode limitations:
; [di] is a valid addressing mode, but [ax] isn't, otherwise we could skip the last mov
Insgesamt: 28 Zyklen auf einer uralten CPU mit diesen Timings.
Vrs
mov ax, [row]; 2 cycles
mov di, ax; 2
shl ax, 6; 2
shl di, 8; 2
add di, ax; 2 (320 = 256+64)
add di, [column]; 2
; di = [row]*(256+64) + [column]
12 Zyklen auf der gleichen alten CPU.
Ja, wir würden so hart arbeiten, um 16 CPU-Zyklen einzusparen.
Im 32- oder 64-Bit-Modus sind beide Versionen wesentlich kürzer und schneller. Moderne CPUs mit Out-of-Order-Ausführung wie Intel Skylake (siehe http://agner.org/optimize/ ) haben eine sehr schnelle Hardware-Multiplikation (niedrige Latenzzeit und hoher Durchsatz), so dass der Gewinn viel geringer ist. Die AMD Bulldozer-Familie ist etwas langsamer, insbesondere bei der 64-Bit-Multiplikation. Bei Intel-CPUs und AMD Ryzen sind zwei Verschiebungen mit etwas geringerer Latenz, aber mehr Anweisungen als eine Multiplikation (was zu einem geringeren Durchsatz führen kann):
imul edi, [row], 320 ; 3 cycle latency from [row] being ready
add edi, [column] ; 1 cycle latency (from [column] and edi being ready).
; edi = [row]*(256+64) + [column], in 4 cycles from [row] being ready.
vs.
mov edi, [row]
shl edi, 6 ; row*64. 1 cycle latency
lea edi, [edi + edi*4] ; row*(64 + 64*4). 1 cycle latency
add edi, [column] ; 1 cycle latency from edi and [column] both being ready
; edi = [row]*(256+64) + [column], in 3 cycles from [row] being ready.
Die Compiler erledigen dies für Sie: Siehe wie GCC, Clang und Microsoft Visual C++ verwenden alle die Tastenkombination Shift+Lea beim Optimieren return 320*row + col;
,l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:36.97169647716434,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g63,filters:(b:'0',commentOnly:'0',directives:'0',intel:'0'),options:'-O3+-std%3Dc%2B%2B11+-Wall',source:1),l:'5',n:'0',o:'x86-64+gcc+6.3+(Editor+%231,+Compiler+%231)',t:'0')),k:30.33026438926557,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((g:!((h:compiler,i:(compiler:clang400,filters:(b:'0',commentOnly:'0',directives:'0',intel:'0'),options:'-O3+-std%3Dc%2B%2B11+-Wall',source:1),l:'5',n:'0',o:'x86-64+clang+4.0.0+(Editor+%231,+Compiler+%232)',t:'0')),header:(),k:32.69803913357011,l:'4',m:61.21344498970813,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:cl19_64,filters:(b:'0',commentOnly:'0',directives:'0',intel:'0'),options:'-Ox',source:1),l:'5',n:'0',o:'x86-64+CL+19+2017+RTW+(Editor+%231,+Compiler+%233)',t:'0')),header:(),l:'4',m:38.78655501029187,n:'0',o:'',s:0,t:'0')),k:32.69803913357011,l:'3',n:'0',o:'',t:'0')),l:'2',n:'0',o:'',t:'0')),version:4) .
Interessant ist hier vor allem die Feststellung, dass x86 hat einen Shift-und-Add-Befehl ( LEA
) die gleichzeitig kleine Linksverschiebungen und Additionen durchführen kann, wobei die Leistung als add
Unterricht. ARM ist sogar noch leistungsfähiger: Ein Operand eines beliebigen Befehls kann kostenlos nach links oder rechts verschoben werden. So kann die Skalierung mit einer Kompilierzeitkonstante, die bekanntermaßen eine Potenz von 2 ist, sogar effizienter sein als eine Multiplikation.
OK, zurück in die modernen Tage... etwas nützlicher wäre es jetzt, Bitshifting zu verwenden, um zwei 8-Bit-Werte in einer 16-Bit-Ganzzahl zu speichern. Zum Beispiel, in C#:
// Byte1: 11110000
// Byte2: 00001111
Int16 value = ((byte)(Byte1 >> 8) | Byte2));
// value = 000011111110000;
In C++ sollten die Compiler dies für Sie tun, wenn Sie eine struct
mit zwei 8-Bit-Mitgliedern, aber in der Praxis ist das nicht immer der Fall.