it-swarm.com.de

Was sind bitweise Verschiebungsoperatoren und wie funktionieren sie?

Ich habe versucht, C in meiner Freizeit zu lernen, und andere Sprachen (C #, Java usw.) haben das gleiche Konzept (und oft die gleichen Operatoren) ...

Was ich mich wundere, ist, auf einer Kernebene, was Bit-Shifting (<<, >>, >>>) bewirkt, welche Probleme es lösen kann und welche Fallstricke sich herumschleichen die Kurve? Mit anderen Worten, ein absoluter Anfängerleitfaden für das Verschieben von Bits in seiner ganzen Güte.

1315
John Rudy

Die Bitverschiebungsoperatoren tun genau das, was ihr Name impliziert. Sie verschieben Bits. Hier ist eine kurze (oder nicht so kurze) Einführung in die verschiedenen Schichtoperatoren.

Die Betreiber

  • >> ist der arithmetische (oder vorzeichenbehaftete) Rechtsschiebeoperator.
  • >>> ist der logische (oder vorzeichenlose) Rechtsschaltoperator.
  • << ist der Linksschiebeoperator und erfüllt die Anforderungen sowohl für logische als auch für arithmetische Verschiebungen.

Alle diese Operatoren können auf ganzzahlige Werte angewendet werden (int, long, möglicherweise short und byte oder char). In einigen Sprachen ändert das Anwenden der Umschaltoperatoren auf einen Datentyp, der kleiner als int ist, die Größe des Operanden automatisch in int.

Beachten Sie, dass <<< kein Operator ist, da er redundant wäre.

Beachten Sie auch, dass C und C++ nicht zwischen den richtigen Shift-Operatoren unterscheiden. Sie stellen nur den Operator >> bereit, und das Verhalten beim Verschieben nach rechts ist eine Implementierung, die für signierte Typen definiert ist. In der restlichen Antwort werden die Operatoren C #/Java verwendet.

(In allen gängigen C- und C++ - Implementierungen, einschließlich gcc und clang/LLVM, ist >> für signierte Typen arithmetisch. Einige Codes gehen davon aus, dies ist jedoch keine Standardgarantie. Es ist nicht undefined , der Standard erfordert Implementierungen, um ihn auf die eine oder andere Weise zu definieren. Linksverschiebungen von negativ vorzeichenbehafteten Zahlen sind jedoch undefiniertes Verhalten (vorzeichenbehafteter Ganzzahlüberlauf). Wenn Sie also keine arithmetische Verschiebung nach rechts benötigen, ist es normalerweise eine gute Idee, die Bitverschiebung mit vorzeichenlosen Typen durchzuführen.)


Linksverschiebung (<<)

Ganzzahlen werden im Speicher als eine Reihe von Bits gespeichert. Die als 32-Bit-int gespeicherte Nummer 6 wäre beispielsweise:

00000000 00000000 00000000 00000110

Wenn Sie dieses Bitmuster um eine Position nach links verschieben (6 << 1), erhalten Sie die Zahl 12:

00000000 00000000 00000000 00001100

Wie Sie sehen, sind die Ziffern um eine Stelle nach links verschoben, und die letzte Ziffer rechts ist mit einer Null gefüllt. Sie können auch feststellen, dass Linksverschiebung der Multiplikation mit Potenzen von 2 entspricht. Daher entspricht 6 << 16 * 2 und 6 << 36 * 8. Ein guter optimierender Compiler ersetzt Multiplikationen nach Möglichkeit durch Verschiebungen.

Unrunde Schaltung

Bitte beachten Sie, dass dies keine kreisförmigen Verschiebungen sind. Verschieben Sie diesen Wert um eine Position nach links (3,758,096,384 << 1):

11100000 00000000 00000000 00000000

ergebnisse in 3.221.225.472:

11000000 00000000 00000000 00000000

Die Ziffer, die "vom Ende weg" verschoben wird, geht verloren. Es wickelt sich nicht um.


Logische Rechtsverschiebung (>>>)

Eine logische Verschiebung nach rechts ist das Gegenteil der Verschiebung nach links. Anstatt Bits nach links zu bewegen, bewegen sie sich einfach nach rechts. Zum Beispiel die Zahl 12 verschieben:

00000000 00000000 00000000 00001100

um eine Position nach rechts (12 >>> 1) wird unsere ursprüngliche 6 zurückgegeben:

00000000 00000000 00000000 00000110

Wir sehen also, dass eine Verschiebung nach rechts einer Division durch Zweierpotenzen entspricht.

Verlorene Teile sind verschwunden

Eine Verschiebung kann jedoch keine "verlorenen" Bits zurückfordern. Wenn wir zum Beispiel dieses Muster verschieben:

00111000 00000000 00000000 00000110

an den linken 4 Stellen (939,524,102 << 4) erhalten wir 2.147.483.744:

10000000 00000000 00000000 01100000

und dann zurückschieben ((939,524,102 << 4) >>> 4) erhalten wir 134,217,734:

00001000 00000000 00000000 00000110

Wir können unseren ursprünglichen Wert nicht wiederherstellen, wenn wir etwas verloren haben.


Arithmetische Rechtsverschiebung (>>)

Die arithmetische Verschiebung nach rechts ist genau wie die logische Verschiebung nach rechts, außer dass anstelle des Auffüllens mit Null das höchstwertige Bit aufgefüllt wird. Dies liegt daran, dass das höchstwertige Bit das Vorzeichen oder das Bit ist, das positive und negative Zahlen unterscheidet. Durch Auffüllen mit dem höchstwertigen Bit wird die arithmetische Rechtsverschiebung vorzeichenerhaltend.

Wenn wir zum Beispiel dieses Bitmuster als negative Zahl interpretieren:

10000000 00000000 00000000 01100000

wir haben die Nummer -2.147.483.552. Wenn Sie dies mit der arithmetischen Verschiebung (-2.147.483.552 >> 4) um 4 Positionen nach rechts verschieben, erhalten Sie:

11111000 00000000 00000000 00000110

oder die Nummer -134,217,722.

Wir sehen also, dass wir das Vorzeichen unserer negativen Zahlen erhalten haben, indem wir die arithmetische Rechtsverschiebung anstelle der logischen Rechtsverschiebung verwenden. Und wieder sehen wir, dass wir eine Division durch Zweierpotenzen durchführen.

1640
Derek Park

Nehmen wir an, wir haben ein einzelnes Byte:

0110110

Durch Anwenden einer einzelnen linken Bitverschiebung erhalten wir:

1101100

Die am weitesten links stehende Null wurde aus dem Byte verschoben, und eine neue Null wurde an das rechte Ende des Bytes angehängt.

Die Bits rollen nicht über. Sie werden verworfen. Das heißt, wenn Sie die Schicht 1101100 nach links und dann nach rechts verschieben, erhalten Sie nicht das gleiche Ergebnis zurück.

Eine Linksverschiebung um N entspricht einer Multiplikation mit 2N.

Die Rechtsverschiebung um N ist (wenn Sie Einerkomplement verwenden) das Äquivalent zur Division durch 2N und auf Null runden.

Die Bitverschiebung kann für unglaublich schnelle Multiplikationen und Divisionen verwendet werden, vorausgesetzt, Sie arbeiten mit einer Potenz von 2. Fast alle Grafikroutinen auf niedriger Ebene verwenden die Bitverschiebung.

Zum Beispiel haben wir in früheren Zeiten den Modus 13h (320x200 256 Farben) für Spiele verwendet. Im Modus 13h wurde der Videospeicher nacheinander pro Pixel angeordnet. Um die Position eines Pixels zu berechnen, verwenden Sie die folgende Mathematik:

memoryOffset = (row * 320) + column

In jenen Zeiten war die Geschwindigkeit entscheidend, daher verwendeten wir Bitverschiebungen, um diese Operation durchzuführen.

320 ist jedoch keine Zweierpotenz. Um dies zu umgehen, müssen wir herausfinden, was eine Zweierpotenz ist, die 320 ergibt:

(row * 320) = (row * 256) + (row * 64)

Jetzt können wir das in Linksschichten umwandeln:

(row * 320) = (row << 8) + (row << 6)

Für ein Endergebnis von:

memoryOffset = ((row << 8) + (row << 6)) + column

Jetzt erhalten wir den gleichen Versatz wie zuvor, außer dass wir anstelle einer teuren Multiplikationsoperation die zwei Bitverschiebungen verwenden ... in x86 wäre es ungefähr so ​​(Anmerkung: Es ist für immer her, seit ich Assembly gemacht habe (Anmerkung des Editors: korrigiert) ein paar Fehler 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 jeder alten 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 derselben alten CPU.

Ja, wir würden so hart arbeiten, um 16 CPU-Zyklen zu vermeiden.

Im 32- oder 64-Bit-Modus werden beide Versionen viel kürzer und schneller. Moderne außer Betrieb befindliche Ausführungs-CPUs wie Intel Skylake (siehe http://agner.org/optimize/ ) haben eine sehr schnelle Vervielfachung der Hardware (niedrige Latenz und hoher Durchsatz), sodass der Gewinn viel geringer ist . Die AMD Bulldozer-Familie ist etwas langsamer, insbesondere für 64-Bit-Multiplikationen. Bei Intel-CPUs und AMD Ryzen bedeuten zwei Schichten eine geringfügig geringere 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.

Compiler erledigen das für Sie: Sehen Sie, wie gcc, clang und MSVC verwenden alle Shift + Lea, wenn Sie return 320*row + col; optimieren .

Das Interessanteste dabei ist, dass x86 hat eine Anweisung zum Verschieben und Hinzufügen (LEA) , die kleine Verschiebungen nach links ausführen und gleichzeitig mit der Leistung als und hinzufügen kann add Anweisung. ARM ist noch mächtiger: Ein Operand einer Anweisung kann frei nach links oder rechts verschoben werden. Die Skalierung mit einer Kompilierzeitkonstante, die als Zweierpotenz bekannt ist, kann also noch effizienter sein als eine Multiplikation.


OK, in der heutigen Zeit ... Etwas nützlicheres wäre jetzt die Verwendung der Bitverschiebung, 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 Compiler dies für Sie tun, wenn Sie struct mit zwei 8-Bit-Elementen verwendet haben, in der Praxis jedoch nicht immer.

197
FlySwat

Bitweise Operationen, einschließlich Bitverschiebung, sind für Hardware auf niedriger Ebene oder eingebettete Programmierung von grundlegender Bedeutung. Wenn Sie eine Spezifikation für ein Gerät oder sogar einige Binärdateiformate lesen, werden Bytes, Wörter und Dwords in nicht-byte-ausgerichtete Bitfelder aufgeteilt, die verschiedene interessierende Werte enthalten. Der Zugriff auf diese Bitfelder zum Lesen/Schreiben ist die häufigste Verwendung.

Ein einfaches reales Beispiel bei der Grafikprogrammierung ist, dass ein 16-Bit-Pixel wie folgt dargestellt wird:

  bit | 15| 14| 13| 12| 11| 10| 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1  | 0 |
      |       Blue        |         Green         |       Red          |

Um den grünen Wert zu erreichen, würden Sie dies tun:

 #define GREEN_MASK  0x7E0
 #define GREEN_OFFSET  5

 // Read green
 uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;

Erklärung

Um den Wert von NUR GRÜN zu erhalten, der bei Versatz 5 beginnt und bei 10 endet (dh 6 Bits lang), müssen Sie eine (Bit-) Maske verwenden, die bei Anwendung auf das gesamte 16-Bit-Pixel resultiert Nur die Teile, an denen wir interessiert sind.

#define GREEN_MASK  0x7E0

Die entsprechende Maske ist 0x7E0, die im Binärformat 0000011111100000 lautet (dh 2016 in Dezimalform).

uint16_t green = (pixel & GREEN_MASK) ...;

Um eine Maske anzuwenden, verwenden Sie den AND-Operator (&).

uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;

Nach dem Anwenden der Maske erhalten Sie eine 16-Bit-Zahl, die eigentlich nur eine 11-Bit-Zahl ist, da das MSB im 11. Bit liegt. Grün ist eigentlich nur 6 Bit lang, also müssen wir es mit einer Rechtsverschiebung (11 - 6 = 5) verkleinern, daher wird 5 als Versatz verwendet (#define GREEN_OFFSET 5).

Ebenfalls gebräuchlich ist die Verwendung von Bitverschiebungen zur schnellen Multiplikation und Division durch Potenzen von 2:

 i <<= x;  // i *= 2^x;
 i >>= y;  // i /= 2^y;
96
robottobor

Bit Masking & Shifting

Bitverschiebung wird häufig in der Grafikprogrammierung auf niedriger Ebene verwendet. Zum Beispiel ein gegebener Pixelfarbwert, der in einem 32-Bit-Wort codiert ist.

 Pixel-Color Value in Hex:    B9B9B900
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

Zum besseren Verständnis repräsentiert der gleiche Binärwert, der mit welchen Abschnitten beschriftet ist, welchen Farbteil.

                                 Red     Green     Blue       Alpha
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

Angenommen, wir möchten den Grünwert dieser Pixelfarbe ermitteln. Wir können diesen Wert leicht durch Maskierung und Verschiebung erhalten.

Unsere Maske:

                  Red      Green      Blue      Alpha
 color :        10111001  10111001  10111001  00000000
 green_mask  :  00000000  11111111  00000000  00000000

 masked_color = color & green_mask

 masked_color:  00000000  10111001  00000000  00000000

Der logische Operator & stellt sicher, dass nur die Werte beibehalten werden, bei denen die Maske 1 ist. Das Letzte, was wir jetzt tun müssen, ist, den richtigen ganzzahligen Wert zu erhalten, indem alle diese Bits um 16 Stellen nach rechts verschoben werden (logische Verschiebung nach rechts).

 green_value = masked_color >>> 16

Und nun haben wir die ganze Zahl, die die Menge an Grün in der Pixelfarbe darstellt:

 Pixels-Green Value in Hex:     000000B9
 Pixels-Green Value in Binary:  00000000 00000000 00000000 10111001 
 Pixels-Green Value in Decimal: 185

Dies wird häufig zum Codieren oder Decodieren von Bildformaten wie jpg, png, ... verwendet.

47
Basti Funck

Ein Problem ist, dass Folgendes von der Implementierung abhängig ist (gemäß dem ANSI-Standard):

char x = -1;
x >> 1;

x kann jetzt 127 (01111111) oder immer noch -1 (11111111) sein.

In der Praxis ist es normalerweise Letzteres.

27
AShelly

Ich schreibe nur Tipps und Tricks, die bei Tests/Prüfungen nützlich sein können.

  1. n = n*2: n = n<<1
  2. n = n/2: n = n>>1
  3. Prüfen, ob n Potenz von 2 ist (1,2,4,8, ...): !(n & (n-1))
  4. Erhalten von xth bisschen von n: n |= (1 << x)
  5. Prüfen, ob x gerade oder ungerade ist: x&1 == 0 (gerade)
  6. Schalten Sie das n umth Bit von x: x ^ (1<<n)
19
Ravi Prakash

Beachten Sie, dass in der Implementierung von Java die Anzahl der zu verschiebenden Bits von der Größe der Quelle abhängt.

Zum Beispiel:

(long) 4 >> 65

gleich 2. Sie könnten erwarten, dass eine Verschiebung der Bits um das 65-fache nach rechts alles auf Null setzt, aber es ist tatsächlich das Äquivalent von:

(long) 4 >> (65 % 64)

Dies gilt für <<, >> und >>>. Ich habe es nicht in anderen Sprachen ausprobiert.

8

Einige nützliche Bit-Operationen/Manipulationen in Python. @ Ravi Prakash-Antworten in Python implementiert.

# basic bit operations
# int to bin
print(bin(10))

# bin to int
print(int('1010',2))

# multiplying x with 2 .... x**2== x << 1
print(200<<1)

# dividing x with 2 .... x /2 == x >> 1
print(200>>1)

# modulo x with 2 .... x%2 == x&1
if 20&1==0:
    print("20 is a even number")

# check if n is power of 2 : check !(n & (n-1))
print(not(33 &(33-1)))

# getting xth bit of n : (n>>x)&1
print((10>>2)&1) # bin of 10==1010 and 2nd bit is 0

# toggle nth bit of x : x^(1<<n)
# take bin(10)==1010 and toggling 2nd bit in bin(10) we get 1110 === bin(14)
print(10^(1<<2)) 
0
Amin Ahmed