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 add
un'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 struct
con due membri a 8 bit, ma in pratica non sempre.