Cosa sono gli operatori bitwise shift (bit-shift) e come funzionano?


1382

Ho cercato di imparare C nel mio tempo libero e altre lingue (C #, Java, ecc.) Hanno lo stesso concetto (e spesso gli stessi operatori) ...

Quello che mi chiedo è, ad un livello di base, quello che fa bit-shifting ( <<, >>, >>>) fare, quali sono i problemi che può aiutare a risolvere, e ciò che si nascondono trucchi dietro la curva? In altre parole, una guida per principianti assoluti allo spostamento dei bit in tutta la sua bontà.


2
I casi funzionali o non funzionali in cui useresti il ​​bitshifting in 3GL sono pochi.
Troy DeMonbreun,

16
Dopo aver letto queste risposte potresti voler dare un'occhiata a questi link: graphics.stanford.edu/~seander/bithacks.html & jjj.de/bitwizardry/bitwizardrypage.html
artigli

1
È importante notare che lo spostamento dei bit è estremamente facile e veloce per i computer. Trovando modi per utilizzare lo spostamento dei bit nel tuo programma, puoi ridurre notevolmente i tempi di utilizzo e di esecuzione della memoria.
Hoytman,

@Hoytman: Ma noti che i buoni compilatori conoscono già molti di questi trucchi e in genere sono più bravi a riconoscere dove ha senso.
Sebastian Mach,

Risposte:


1713

Gli operatori che spostano i bit fanno esattamente ciò che implica il loro nome. Spostano i bit. Ecco una breve (o non così breve) introduzione ai diversi operatori di turno.

Gli operatori

  • >> è l'operatore di spostamento a destra aritmetico (o firmato).
  • >>> è l'operatore di spostamento a destra logico (o senza segno).
  • << è l'operatore di spostamento a sinistra e soddisfa le esigenze di turni sia logici che aritmetici.

Tutti questi operatori possono essere applicati a valori interi ( int, long, possibilmente shorte byteo char). In alcune lingue, l'applicazione degli operatori di turno a qualsiasi tipo di dati più piccolo di quello intridimensiona automaticamente l'operando in modo che sia un int.

Nota che <<<non è un operatore, perché sarebbe ridondante.

Si noti inoltre che C e C ++ non fanno distinzione tra gli operatori di spostamento a destra . Forniscono solo l' >>operatore e il comportamento di spostamento a destra è l'implementazione definita per i tipi firmati. Il resto della risposta utilizza gli operatori C # / Java.

(In tutte le principali implementazioni C e C ++, tra cui GCC e Clang / LLVM, >>sui tipi firmati è aritmetica. Alcuni codici lo presuppongono, ma non è qualcosa che lo standard garantisce. Non è indefinito , tuttavia; lo standard richiede implementazioni per definirlo uno in un modo o nell'altro. Tuttavia, i turni a sinistra di numeri con segno negativo sono comportamenti indefiniti (overflow di numeri interi con segno). Quindi, a meno che non sia necessario lo spostamento a destra aritmetico, di solito è una buona idea eseguire lo spostamento dei bit con tipi senza segno.)


Spostamento a sinistra (<<)

Gli interi sono memorizzati, in memoria, come una serie di bit. Ad esempio, il numero 6 memorizzato come 32 bit intsarebbe:

00000000 00000000 00000000 00000110

Spostando questo schema di bit a sinistra di una posizione ( 6 << 1) si otterrebbe il numero 12:

00000000 00000000 00000000 00001100

Come puoi vedere, le cifre si sono spostate a sinistra di una posizione e l'ultima cifra a destra è riempita con uno zero. Potresti anche notare che lo spostamento a sinistra equivale alla moltiplicazione per poteri di 2. Quindi 6 << 1è equivalente a 6 * 2ed 6 << 3è equivalente a 6 * 8. Un buon compilatore di ottimizzazione sostituirà le moltiplicazioni con i turni quando possibile.

Spostamento non circolare

Si noti che questi non sono turni circolari. Spostando questo valore a sinistra di una posizione ( 3,758,096,384 << 1):

11100000 00000000 00000000 00000000

risulta 3.221.225.472:

11000000 00000000 00000000 00000000

La cifra che viene spostata "dalla fine" viene persa. Non si avvolge.


Logico spostamento a destra (>>>)

Uno spostamento logico a destra è il contrario dello spostamento a sinistra. Invece di spostare i bit verso sinistra, si spostano semplicemente verso destra. Ad esempio, spostando il numero 12:

00000000 00000000 00000000 00001100

a destra di una posizione ( 12 >>> 1) tornerà il nostro originale 6:

00000000 00000000 00000000 00000110

Quindi vediamo che spostarsi a destra equivale alla divisione per poteri di 2.

I pezzi persi sono spariti

Tuttavia, uno spostamento non può recuperare bit "persi". Ad esempio, se spostiamo questo modello:

00111000 00000000 00000000 00000110

a sinistra 4 posizioni ( 939,524,102 << 4), otteniamo 2.147.483.744:

10000000 00000000 00000000 01100000

e poi spostando indietro ( (939,524,102 << 4) >>> 4) otteniamo 134.217.734:

00001000 00000000 00000000 00000110

Non possiamo ripristinare il valore originale una volta che abbiamo perso i bit.


Aritmetica spostamento a destra (>>)

Lo spostamento aritmetico a destra è esattamente come lo spostamento logico a destra, tranne che per il padding con zero, pad con il bit più significativo. Questo perché il bit più significativo è il bit di segno o il bit che distingue i numeri positivi e negativi. Riempiendo con il bit più significativo, lo spostamento aritmetico destro preserva i segni.

Ad esempio, se interpretiamo questo modello di bit come un numero negativo:

10000000 00000000 00000000 01100000

abbiamo il numero -2.147.483.552. Spostando questo a 4 posizioni giuste con lo spostamento aritmetico (-2.147.483.552 >> 4) ci darebbe:

11111000 00000000 00000000 00000110

o il numero -134.217.722.

Quindi vediamo che abbiamo preservato il segno dei nostri numeri negativi usando lo spostamento destro aritmetico, piuttosto che lo spostamento destro logico. E ancora una volta, vediamo che stiamo eseguendo la divisione per poteri di 2.


304
La risposta dovrebbe chiarire che si tratta di una risposta specifica per Java. Non esiste un operatore >>> in C / C ++ o C #, e se >> propaga o meno il segno è l'implementazione definita in C / C ++ (un importante potenziale gotcha)
Michael Burr,

56
La risposta è totalmente errata nel contesto del linguaggio C. Non vi è alcuna divisione significativa in turni "aritmetici" e "logici" in C. In C i turni funzionano come previsto su valori senza segno e su valori con segno positivo - spostano semplicemente i bit. Su valori negativi, lo spostamento a destra è definito dall'implementazione (cioè non si può dire nulla su ciò che fa in generale), e lo spostamento a sinistra è semplicemente proibito - produce un comportamento indefinito.
AnT

10
Audrey, c'è sicuramente una differenza tra spostamento aritmetico e logico a destra. C lascia semplicemente definita l'implementazione scelta. E lo spostamento a sinistra su valori negativi non è assolutamente proibito. Sposta 0xff000000 a sinistra di un bit e otterrai 0xfe000000.
Derek Park,

16
A good optimizing compiler will substitute shifts for multiplications when possible. Che cosa? I bitbift sono ordini di grandezza più veloci quando si tratta di operazioni di basso livello di una CPU, un buon compilatore di ottimizzazione farebbe l' esatto contrario, cioè trasformando le normali moltiplicazioni per potenze di due in bit shift.
Mahn,

55
@Mahn, lo stai leggendo al contrario dal mio intento. Sostituire Y con X significa sostituire X con Y. Y è il sostituto di X. Quindi lo spostamento è il sostituto della moltiplicazione.
Derek Park,

209

Diciamo che abbiamo un singolo byte:

0110110

Applicare un singolo bit shift sinistro ci ottiene:

1101100

Lo zero più a sinistra è stato spostato fuori dal byte e un nuovo zero è stato aggiunto all'estremità destra del byte.

I bit non si ribaltano; vengono scartati. Ciò significa che se hai lasciato il turno 1101100 e poi spostato a destra, non otterrai lo stesso risultato.

Shifting data da N equivale a moltiplicare per 2 N .

Lo spostamento a destra di N è (se stai usando il complemento di uno ) equivale a dividere per 2 N e arrotondare a zero.

Il Bitshifting può essere utilizzato per una moltiplicazione e una divisione follemente veloci, a condizione che tu stia lavorando con una potenza di 2. Quasi tutte le routine grafiche di basso livello usano il bitshifting.

Ad esempio, ai vecchi tempi, abbiamo usato la modalità 13h (320x200 256 colori) per i giochi. In modalità 13h, la memoria video è stata disposta in sequenza per pixel. Ciò significava calcolare la posizione di un pixel, si userebbe la seguente matematica:

memoryOffset = (row * 320) + column

Ora, a quei tempi, la velocità era fondamentale, quindi avremmo usato i bitshift per fare questa operazione.

Tuttavia, 320 non è un potere di due, quindi per ovviare a questo dobbiamo scoprire qual è un potere di due che sommato fa 320:

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

Ora possiamo convertirlo in turni a sinistra:

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

Per un risultato finale di:

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

Ora otteniamo lo stesso offset di prima, tranne che per un'operazione di moltiplicazione costosa, usiamo i due bit-shift ... in x86 sarebbe qualcosa del genere (nota, è sempre stato da quando ho fatto il montaggio (nota dell'editor: corretta un paio di errori e ha aggiunto un esempio a 32 bit)):

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

Totale: 28 cicli su qualunque CPU antica avesse questi tempi.

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 cicli sulla stessa antica CPU.

Sì, lavoreremmo così duramente per radere via 16 cicli di CPU.

In modalità a 32 o 64 bit, entrambe le versioni diventano molto più brevi e veloci. Le moderne CPU di esecuzione fuori servizio come Intel Skylake (vedi http://agner.org/optimize/ ) hanno una moltiplicazione hardware molto rapida (bassa latenza e throughput elevato), quindi il guadagno è molto più piccolo. La famiglia di bulldozer AMD è un po 'più lenta, specialmente per la moltiplicazione a 64 bit. Nelle CPU Intel e AMD Ryzen, due turni hanno una latenza leggermente inferiore ma più istruzioni di una moltiplicazione (che può portare a un throughput inferiore):

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.

I compilatori lo faranno per te: vedi come GCC, Clang e Microsoft Visual C ++ usano tutti shift + lea durante l'ottimizzazionereturn 320*row + col; .

La cosa più interessante da notare qui è che x86 ha un'istruzione shift-and-add ( LEA) che può fare piccoli spostamenti a sinistra e aggiungere allo stesso tempo, con le prestazioni come addun'istruzione. ARM è ancora più potente: un operando di qualsiasi istruzione può essere spostato a destra oa sinistra gratuitamente. Quindi il ridimensionamento in base a una costante di tempo di compilazione che è nota per essere una potenza di 2 può essere persino più efficiente di una moltiplicazione.


OK, ai giorni nostri ... qualcosa di più utile ora sarebbe usare il bitshifting per memorizzare due valori a 8 bit in un numero intero a 16 bit. Ad esempio, in C #:

// Byte1: 11110000
// Byte2: 00001111

Int16 value = ((byte)(Byte1 >> 8) | Byte2));

// value = 000011111110000;

In C ++, i compilatori dovrebbero farlo per te se hai usato un structcon due membri a 8 bit, ma in pratica non sempre.


8
Espandendolo, sui processori Intel (e molti altri) è più veloce farlo: int c, d; c = d << 2; Di questo: c = 4 * d; A volte, anche "c = d << 2 + d << 1" è più veloce di "c = 6 * d" !! Ho usato questi trucchi ampiamente per le funzioni grafiche nell'era del DOS, non credo siano più così utili ...
Joe Pineda,

5
@James: non proprio, al giorno d'oggi è piuttosto il firmware della scheda video che include un codice del genere, che deve essere eseguito dalla GPU piuttosto che dalla CPU. Quindi teoricamente non è necessario implementare un codice come questo (o come la funzione di radice inversa della magia nera di Carmack) per le funzioni grafiche :-)
Joe Pineda,

3
@JoePineda @james Gli autori del compilatore li stanno sicuramente usando. Se scrivi c=4*dotterrai un turno. Se scrivi k = (n<0)ciò può essere fatto anche con i turni: k = (n>>31)&1per evitare un ramo. In conclusione, questo miglioramento dell'intelligenza dei compilatori significa che ora non è necessario utilizzare questi trucchi nel codice C e compromettono la leggibilità e la portabilità. Ancora molto buono conoscerli se stai scrivendo ad esempio il codice vettoriale SSE; o qualsiasi situazione in cui ne hai bisogno velocemente e c'è un trucco che il compilatore non sta usando (ad esempio il codice GPU).
Greggo,

2
Un altro buon esempio: cosa molto comune if(x >= 1 && x <= 9)che può essere fatta in quanto if( (unsigned)(x-1) <=(unsigned)(9-1)) Cambiare due test condizionali in uno può essere un grande vantaggio di velocità; soprattutto quando consente l'esecuzione prevista anziché i rami. L'ho usato per anni (dove giustificato) fino a quando ho notato circa 10 anni fa che i compilatori avevano iniziato a fare questa trasformazione nell'ottimizzatore, poi mi sono fermato. Buono a sapersi, dal momento che ci sono situazioni simili in cui il compilatore non può effettuare la trasformazione per te. O se stai lavorando su un compilatore.
Greggo,

3
C'è una ragione per cui il tuo "byte" è solo 7 bit?
Mason Watmough,

104

Le operazioni bit a bit, incluso il bit shift, sono fondamentali per l'hardware di basso livello o la programmazione integrata. Se leggi una specifica per un dispositivo o anche alcuni formati di file binari, vedrai byte, parole e parole, suddivisi in campi di bit allineati senza byte, che contengono vari valori di interesse. L'accesso a questi campi di bit per la lettura / scrittura è l'uso più comune.

Un semplice esempio reale nella programmazione grafica è che un pixel a 16 bit è rappresentato come segue:

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

Per ottenere il valore verde devi fare questo:

 #define GREEN_MASK  0x7E0
 #define GREEN_OFFSET  5

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

Spiegazione

Per ottenere il valore SOLO verde, che inizia con l'offset 5 e termina con 10 (ovvero 6 bit di lunghezza), è necessario utilizzare una maschera (bit), che se applicata sull'intero pixel a 16 bit, produrrà solo i bit a cui siamo interessati.

#define GREEN_MASK  0x7E0

La maschera appropriata è 0x7E0 che in binario è 0000011111100000 (ovvero 2016 in decimale).

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

Per applicare una maschera, utilizzare l'operatore AND (&).

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

Dopo aver applicato la maschera, ti ritroverai con un numero a 16 bit che è in realtà solo un numero a 11 bit poiché il suo MSB è nell'11 ° bit. Il verde in realtà è lungo solo 6 bit, quindi dobbiamo ridimensionarlo usando uno spostamento a destra (11 - 6 = 5), quindi l'uso di 5 come offset ( #define GREEN_OFFSET 5).

Inoltre è comune utilizzare i bit shift per una rapida moltiplicazione e divisione per potenze di 2:

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

1
0x7e0 è uguale a 11111100000, ovvero 2016 in decimale.
Saheed,

50

Bit Masking & Shifting

Lo spostamento dei bit viene spesso utilizzato nella programmazione grafica di basso livello. Ad esempio, un determinato valore di colore dei pixel codificato in una parola a 32 bit.

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

Per una migliore comprensione, lo stesso valore binario etichettato con quali sezioni rappresentano quale parte di colore.

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

Diciamo ad esempio che vogliamo ottenere il valore verde del colore di questo pixel. Possiamo facilmente ottenere quel valore mascherando e spostando .

La nostra maschera:

                  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

L' &operatore logico assicura che vengano mantenuti solo i valori in cui la maschera è 1. L'ultima cosa che ora dobbiamo fare è ottenere il valore intero corretto spostando tutti quei bit a destra di 16 posizioni (spostamento logico a destra) .

 green_value = masked_color >>> 16

Et voilà, abbiamo l'intero che rappresenta la quantità di verde nel colore del pixel:

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

Questo è spesso utilizzato per la codifica o decodifica formati immagine come jpg, pngecc


Non è più facile trasmettere l'originale, diciamo cl_uint a 32 bit, come qualcosa come cl_uchar4 e accedere al byte desiderato direttamente come * .s2?
David H Parry,

27

Un problema è che quanto segue dipende dall'implementazione (secondo lo standard ANSI):

char x = -1;
x >> 1;

x può ora essere 127 (01111111) o ancora -1 (11111111).

In pratica, di solito è quest'ultimo.


4
Se lo ricordo correttamente, lo standard ANSI C afferma esplicitamente che questo dipende dall'implementazione, quindi è necessario controllare la documentazione del compilatore per vedere come viene implementato se si desidera spostare a destra numeri interi con segno sul proprio codice.
Joe Pineda,

Sì, volevo solo enfatizzare lo standard ANSI stesso, così dice, non è un caso in cui i fornitori semplicemente non seguono lo standard o che lo standard non dice nulla su questo particolare caso.
Joe Pineda,

22

Sto solo scrivendo suggerimenti. Può essere utile in test ed esami.

  1. n = n*2: n = n<<1
  2. n = n/2: n = n>>1
  3. Verifica se n è potenza di 2 (1,2,4,8, ...): verifica !(n & (n-1))
  4. Ottenere x th bit of n:n |= (1 << x)
  5. Verifica se x è pari o dispari: x&1 == 0(pari)
  6. Alterna il n ° bit x:x ^ (1<<n)

Devono essercene ancora qualcuno che conosci ormai?
Ryyker,

@ryyker Ne ho aggiunti alcuni altri. Cercherò di continuare ad aggiornarlo :)
Ravi Prakash,

X e n 0 sono indicizzati?
reggaeguitar,

Annuncio 5 .: Cosa succede se si tratta di un numero negativo?
Peter Mortensen,

quindi, possiamo concludere che 2 in binario è come 10 in decimale? e lo spostamento dei bit è come aggiungere o sottrarre un altro numero dietro un altro numero in decimale?
Willy satrio nugroho, il

8

Si noti che nell'implementazione Java, il numero di bit da spostare viene modificato dalla dimensione della sorgente.

Per esempio:

(long) 4 >> 65

è uguale a 2. Potresti aspettarti che spostare i bit a destra di 65 volte azzererebbe tutto, ma in realtà è l'equivalente di:

(long) 4 >> (65 % 64)

Questo è vero per <<, >> e >>>. Non l'ho provato in altre lingue.


Eh, interessante! In C, questo è un comportamento tecnicamente indefinito . gcc 5.4.0dà un avvertimento, ma dà 25 >> 65; anche.
pizzapants184

2

Alcune utili operazioni / manipolazioni di bit in Python.

Ho implementato la risposta di Ravi Prakash in Python.

# Basic bit operations
# Integer to binary
print(bin(10))

# Binary to integer
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 second bit is 0

# Toggle nth bit of x : x^(1 << n)
# take bin(10) == 1010 and toggling second bit in bin(10) we get 1110 === bin(14)
print(10^(1 << 2))

-3

Tieni presente che sulla piattaforma Windows è disponibile solo la versione a 32 bit di PHP.

Quindi se ad esempio si sposta << o >> più di 31 bit, i risultati non sono prevedibili. Di solito viene restituito il numero originale anziché zero, e può essere un bug davvero complicato.

Naturalmente se usi la versione a 64 bit di PHP (Unix), dovresti evitare lo spostamento di oltre 63 bit. Tuttavia, ad esempio, MySQL utilizza BIGINT a 64 bit, quindi non dovrebbero esserci problemi di compatibilità.

AGGIORNAMENTO: Da PHP 7 Windows, i build PHP sono finalmente in grado di utilizzare numeri interi a 64 bit: la dimensione di un numero intero dipende dalla piattaforma, sebbene un valore massimo di circa due miliardi sia il valore normale (ovvero 32 bit firmati). Le piattaforme a 64 bit di solito hanno un valore massimo di circa 9E18, tranne su Windows precedente a PHP 7, dove era sempre a 32 bit.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.