Disons que nous avons un seul octet:
0110110
L'application d'un seul décalage à gauche nous permet:
1101100
Le zéro le plus à gauche a été déplacé hors de l'octet et un nouveau zéro a été ajouté à l'extrémité droite de l'octet.
Les bits ne survolent pas; ils sont jetés. Cela signifie que si vous avez quitté le décalage 1101100 puis le décalage à droite, vous n'obtiendrez pas le même résultat.
Décaler à gauche par N est équivalent à la multiplication par 2 N .
Décaler à droite de N est (si vous utilisez les compléments ) est l'équivalent de diviser par 2 N et d'arrondir à zéro.
Le décalage de bits peut être utilisé pour une multiplication et une division incroyablement rapides, à condition que vous travailliez avec une puissance de 2. Presque toutes les routines graphiques de bas niveau utilisent le décalage de bits.
Par exemple, dans le passé, nous utilisions le mode 13h (320x200 256 couleurs) pour les jeux. En mode 13h, la mémoire vidéo était disposée séquentiellement par pixel. Cela signifiait pour calculer l'emplacement d'un pixel, vous utiliseriez les mathématiques suivantes:
memoryOffset = (row * 320) + column
Maintenant, à cette époque, la vitesse était critique, nous utilisions donc des décalages de bits pour effectuer cette opération.
Cependant, 320 n'est pas une puissance de deux, donc pour contourner cela, nous devons découvrir ce qu'est une puissance de deux qui, ensemble, fait 320:
(row * 320) = (row * 256) + (row * 64)
Maintenant, nous pouvons convertir cela en décalages à gauche:
(row * 320) = (row << 8) + (row << 6)
Pour un résultat final de:
memoryOffset = ((row << 8) + (row << 6)) + column
Maintenant, nous obtenons le même décalage qu'auparavant, sauf qu'au lieu d'une opération de multiplication coûteuse, nous utilisons les deux décalages de bits ... en x86, ce serait quelque chose comme ça (remarque, cela fait toujours que je n'ai pas assemblé (note de l'éditeur: corrigé) quelques erreurs et ajouté un exemple 32 bits)):
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
Total: 28 cycles sur n'importe quel ancien processeur ayant ces horaires.
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 cycles sur le même ancien CPU.
Oui, nous travaillerions dur pour raser 16 cycles CPU.
En mode 32 ou 64 bits, les deux versions deviennent beaucoup plus courtes et plus rapides. Les processeurs d'exécution modernes en panne comme Intel Skylake (voir http://agner.org/optimize/ ) ont une multiplication matérielle très rapide (faible latence et haut débit), donc le gain est beaucoup plus faible. La famille AMD Bulldozer est un peu plus lente, en particulier pour la multiplication 64 bits. Sur les processeurs Intel et AMD Ryzen, deux décalages sont une latence légèrement inférieure mais plus d'instructions qu'une multiplication (ce qui peut entraîner une baisse du débit):
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.
contre.
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.
Les compilateurs le feront pour vous: voyez comment GCC, Clang et Microsoft Visual C ++ utilisent tous shift + lea lors de l'optimisationreturn 320*row + col;
.
La chose la plus intéressante à noter ici est que x86 a une instruction shift-and-add ( LEA
) qui peut effectuer de petits décalages à gauche et ajouter en même temps, avec les performances comme add
instruction. ARM est encore plus puissant: un opérande de n'importe quelle instruction peut être déplacé à gauche ou à droite gratuitement. Ainsi, la mise à l'échelle par une constante de temps de compilation connue pour être une puissance de 2 peut être encore plus efficace qu'une multiplication.
OK, à l'époque moderne ... quelque chose de plus utile maintenant serait d'utiliser le décalage de bits pour stocker deux valeurs de 8 bits dans un entier de 16 bits. Par exemple, en C #:
// Byte1: 11110000
// Byte2: 00001111
Int16 value = ((byte)(Byte1 >> 8) | Byte2));
// value = 000011111110000;
En C ++, les compilateurs devraient le faire pour vous si vous avez utilisé un struct
avec deux membres 8 bits, mais en pratique ils ne le font pas toujours.