Que sont les opérateurs de décalage au niveau du bit (décalage de bit) et comment fonctionnent-ils?


1382

J'ai essayé d'apprendre le C pendant mon temps libre, et d'autres langages (C #, Java, etc.) ont le même concept (et souvent les mêmes opérateurs) ...

Ce que je me demande est, à un niveau de base, ce qui fait peu de décalage ( <<, >>, >>>) faire, quels problèmes peut - il aider à résoudre, et ce qui se cachent gotchas autour du coude? En d'autres termes, un guide du débutant absolu pour le décalage de bits dans toute sa qualité.


2
Les cas fonctionnels ou non fonctionnels dans lesquels vous utiliseriez le décalage de bits dans les 3GL sont rares.
Troy DeMonbreun

16
Après avoir lu ces réponses, vous pouvez consulter ces liens: graphics.stanford.edu/~seander/bithacks.html & jjj.de/bitwizardry/bitwizardrypage.html
claws

1
Il est important de noter que le décalage de bits est extrêmement facile et rapide pour les ordinateurs. En trouvant des moyens d'utiliser le décalage de bits dans votre programme, vous pouvez réduire considérablement l'utilisation de la mémoire et les temps d'exécution.
Hoytman

@Hoytman: Mais notez que les bons compilateurs connaissent déjà bon nombre de ces astuces et sont généralement mieux à même de reconnaître où cela a du sens.
Sebastian Mach

Réponses:


1713

Les opérateurs de décalage de bits font exactement ce que leur nom implique. Ils décalent des bits. Voici une brève (ou pas si brève) introduction aux différents opérateurs de décalage.

Les opérateurs

  • >> est l'opérateur de décalage arithmétique (ou signé) vers la droite.
  • >>> est l'opérateur de décalage à droite logique (ou non signé).
  • << est l'opérateur de décalage gauche et répond aux besoins des décalages logiques et arithmétiques.

Tous ces opérateurs peuvent être appliqués à des valeurs entières ( int, longéventuellement shortet byteou char). Dans certaines langues, l'application des opérateurs de décalage à tout type de données plus petit que intredimensionne automatiquement l'opérande en un int.

Notez que ce <<<n'est pas un opérateur, car il serait redondant.

Notez également que C et C ++ ne font pas de distinction entre les opérateurs de décalage à droite . Ils fournissent uniquement l' >>opérateur, et le comportement de décalage vers la droite est défini par l'implémentation pour les types signés. Le reste de la réponse utilise les opérateurs C # / Java.

(Dans toutes les implémentations C et C ++ courantes, y compris GCC et Clang / LLVM, >>sur les types signés est arithmétique. Certains codes le supposent, mais ce n'est pas quelque chose que la norme garantit. Ce n'est pas indéfini , cependant; la norme nécessite des implémentations pour le définir un d'une manière ou d'une autre. Cependant, les décalages à gauche des nombres signés négatifs sont un comportement indéfini (dépassement d'entier signé). Donc, à moins que vous n'ayez besoin d'un décalage arithmétique à droite, c'est généralement une bonne idée de faire votre décalage de bits avec des types non signés.)


Décalage à gauche (<<)

Les entiers sont stockés, en mémoire, sous la forme d'une série de bits. Par exemple, le nombre 6 stocké en 32 bits intserait:

00000000 00000000 00000000 00000110

Décaler ce motif binaire vers la gauche ( 6 << 1) entraînerait le nombre 12:

00000000 00000000 00000000 00001100

Comme vous pouvez le voir, les chiffres se sont déplacés vers la gauche d'une position, et le dernier chiffre à droite est rempli d'un zéro. Vous pourriez également noter que le décalage vers la gauche équivaut à une multiplication par des puissances de 2. Donc, 6 << 1est équivalent à 6 * 2, et 6 << 3est équivalent à 6 * 8. Un bon compilateur d'optimisation remplacera les multiplications par des décalages lorsque cela sera possible.

Décalage non circulaire

Veuillez noter qu'il ne s'agit pas de changements circulaires. Décaler cette valeur vers la gauche d'une position ( 3,758,096,384 << 1):

11100000 00000000 00000000 00000000

donne 3 221 225 472:

11000000 00000000 00000000 00000000

Le chiffre qui est décalé "de la fin" est perdu. Il ne s'enroule pas.


Décalage à droite logique (>>>)

Un décalage à droite logique est l'inverse du décalage à gauche. Plutôt que de déplacer des bits vers la gauche, ils se déplacent simplement vers la droite. Par exemple, en décalant le nombre 12:

00000000 00000000 00000000 00001100

à droite d'une position ( 12 >>> 1) récupérera notre 6 d'origine:

00000000 00000000 00000000 00000110

Nous voyons donc que le décalage vers la droite équivaut à une division par les pouvoirs de 2.

Les morceaux perdus sont partis

Cependant, un décalage ne peut pas récupérer les bits "perdus". Par exemple, si nous modifions ce modèle:

00111000 00000000 00000000 00000110

à gauche 4 positions ( 939,524,102 << 4), on obtient 2.147.483.744:

10000000 00000000 00000000 01100000

puis en reculant ( (939,524,102 << 4) >>> 4), nous obtenons 134 217 734:

00001000 00000000 00000000 00000110

Nous ne pouvons pas récupérer notre valeur d'origine une fois que nous avons perdu des bits.


Décalage vers la droite arithmétique (>>)

Le décalage arithmétique à droite est exactement comme le décalage logique à droite, sauf qu'au lieu de remplir avec zéro, il remplit avec le bit le plus significatif. En effet, le bit le plus significatif est le bit de signe , ou le bit qui distingue les nombres positifs et négatifs. En remplissant le bit le plus significatif, le décalage arithmétique vers la droite préserve les signes.

Par exemple, si nous interprétons ce modèle de bits comme un nombre négatif:

10000000 00000000 00000000 01100000

nous avons le nombre -2.147.483.552. Le déplacer vers la droite de 4 positions avec le décalage arithmétique (-2.147.483.552 >> 4) nous donnerait:

11111000 00000000 00000000 00000110

ou le numéro -134,217,722.

Nous voyons donc que nous avons conservé le signe de nos nombres négatifs en utilisant le décalage arithmétique à droite, plutôt que le décalage logique à droite. Et encore une fois, nous voyons que nous effectuons la division par des puissances de 2.


304
La réponse devrait indiquer plus clairement qu'il s'agit d'une réponse spécifique à Java. Il n'y a pas d'opérateur >>> en C / C ++ ou C #, et si oui ou non >> propage le signe, l'implémentation est définie en C / C ++ (un potentiel majeur)
Michael Burr

56
La réponse est totalement incorrecte dans le contexte du langage C. Il n'y a pas de division significative en décalages "arithmétiques" et "logiques" en C. En C, les décalages fonctionnent comme prévu sur les valeurs non signées et sur les valeurs signées positives - ils décalent simplement les bits. Sur les valeurs négatives, le décalage à droite est défini par l'implémentation (c'est-à-dire que rien ne peut être dit sur ce qu'il fait en général), et le décalage à gauche est simplement interdit - il produit un comportement indéfini.
AnT

10
Audrey, il y a certainement une différence entre l'arithmétique et le décalage logique à droite. C laisse simplement l'implémentation de choix définie. Et le décalage à gauche sur des valeurs négatives n'est certainement pas interdit. Déplacez 0xff000000 vers la gauche un bit et vous obtiendrez 0xfe000000.
Derek Park

16
A good optimizing compiler will substitute shifts for multiplications when possible. Quelle? Les décalages binaires sont des ordres de grandeur plus rapides en ce qui concerne les opérations de bas niveau d'un processeur, un bon compilateur d'optimisation ferait exactement le contraire, c'est-à-dire transformer les multiplications ordinaires par puissances de deux en décalages de bits.
Mahn

55
@Mahn, vous le lisez à l'envers de mon intention. Remplacer Y par X signifie remplacer X par Y. Y est le substitut de X. Ainsi, le décalage est le substitut de la multiplication.
Derek Park

209

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 addinstruction. 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 structavec deux membres 8 bits, mais en pratique ils ne le font pas toujours.


8
En étendant cela, sur les processeurs Intel (et beaucoup d'autres), il est plus rapide de le faire: int c, d; c = d << 2; Ensuite: c = 4 * d; Parfois, même "c = d << 2 + d << 1" est plus rapide que "c = 6 * d" !! J'ai beaucoup utilisé ces astuces pour les fonctions graphiques à l'ère DOS, je ne pense plus qu'elles soient si utiles ...
Joe Pineda

5
@James: pas tout à fait, de nos jours c'est plutôt le firmware de la carte vidéo qui inclut un code comme ça, à exécuter par le GPU plutôt que par le CPU. Donc, théoriquement, vous n'avez pas besoin d'implémenter un code comme celui-ci (ou comme la fonction racine inverse de magie noire de Carmack) pour les fonctions graphiques :-)
Joe Pineda

3
@JoePineda @james Les rédacteurs du compilateur les utilisent définitivement. Si vous écrivez, c=4*dvous obtiendrez un changement. Si vous écrivez, k = (n<0)cela peut aussi être fait avec des quarts: k = (n>>31)&1pour éviter une branche. En fin de compte, cette amélioration de l'habileté des compilateurs signifie qu'il n'est plus nécessaire d'utiliser ces astuces dans le code C, et ils compromettent la lisibilité et la portabilité. Encore très bon de les connaître si vous écrivez par exemple du code vectoriel SSE; ou toute situation où vous en avez besoin rapidement et il y a une astuce que le compilateur n'utilise pas (par exemple le code GPU).
greggo

2
Un autre bon exemple: une chose très courante est if(x >= 1 && x <= 9)ce qui peut être fait car if( (unsigned)(x-1) <=(unsigned)(9-1)) changer deux tests conditionnels en un seul peut être un gros avantage de vitesse; surtout quand il permet une exécution prédite au lieu de branches. J'ai utilisé cela pendant des années (lorsque cela était justifié) jusqu'à ce que je remarque il y a environ 10 ans que les compilateurs avaient commencé à faire cette transformation dans l'optimiseur, puis je me suis arrêté. Toujours bon à savoir, car il existe des situations similaires où le compilateur ne peut pas effectuer la transformation pour vous. Ou si vous travaillez sur un compilateur.
greggo

3
Y a-t-il une raison pour laquelle votre "octet" n'est que de 7 bits?
Mason Watmough

104

Les opérations au niveau du bit, y compris le décalage de bits, sont fondamentales pour le matériel de bas niveau ou la programmation intégrée. Si vous lisez une spécification pour un périphérique ou même certains formats de fichiers binaires, vous verrez des octets, des mots et des mots-clés, divisés en champs de bits alignés non octets, qui contiennent diverses valeurs intéressantes. L'accès à ces champs binaires pour la lecture / écriture est l'utilisation la plus courante.

Un exemple réel simple dans la programmation graphique est qu'un pixel 16 bits est représenté comme suit:

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

Pour obtenir la valeur verte, vous devez procéder comme suit:

 #define GREEN_MASK  0x7E0
 #define GREEN_OFFSET  5

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

Explication

Afin d'obtenir la valeur du vert UNIQUEMENT, qui commence à l'offset 5 et se termine à 10 (c'est-à-dire 6 bits de long), vous devez utiliser un masque (bit) qui, lorsqu'il est appliqué contre l'ensemble du pixel 16 bits, donnera seuls les bits qui nous intéressent.

#define GREEN_MASK  0x7E0

Le masque approprié est 0x7E0 qui en binaire est 0000011111100000 (qui est 2016 en décimal).

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

Pour appliquer un masque, vous utilisez l'opérateur AND (&).

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

Après avoir appliqué le masque, vous vous retrouverez avec un nombre de 16 bits qui est vraiment juste un nombre de 11 bits puisque son MSB est dans le 11e bit. Le vert ne fait en fait que 6 bits, nous devons donc le réduire en utilisant un décalage vers la droite (11 - 6 = 5), d'où l'utilisation de 5 comme décalage ( #define GREEN_OFFSET 5).

Il est également courant d'utiliser des décalages de bits pour une multiplication et une division rapides par puissances de 2:

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

1
0x7e0 est le même que 11111100000 qui est 2016 en décimal.
Saheed

50

Masquage et décalage des bits

Le décalage de bits est souvent utilisé dans la programmation graphique de bas niveau. Par exemple, une valeur de couleur de pixel donnée codée dans un mot de 32 bits.

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

Pour une meilleure compréhension, la même valeur binaire étiquetée avec quelles sections représentent quelle couleur.

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

Disons par exemple que nous voulons obtenir la valeur verte de la couleur de ce pixel. Nous pouvons facilement obtenir cette valeur en masquant et en déplaçant .

Notre masque:

                  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' &opérateur logique garantit que seules les valeurs où le masque est 1 sont conservées. La dernière chose que nous devons maintenant faire est d'obtenir la valeur entière correcte en décalant tous ces bits vers la droite de 16 positions (décalage logique vers la droite) .

 green_value = masked_color >>> 16

Et voilà, nous avons l'entier représentant la quantité de vert dans la couleur du pixel:

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

Ceci est souvent utilisé pour l' encodage ou le décodage des formats d'image comme jpg, png, etc.


N'est-il pas plus facile de convertir votre original, disons cl_uint 32 bits, en quelque chose comme cl_uchar4 et d'accéder à l'octet que vous voulez directement en * .s2?
David H Parry

27

Un problème est que ce qui suit dépend de l'implémentation (selon la norme ANSI):

char x = -1;
x >> 1;

x peut désormais être 127 (01111111) ou encore -1 (11111111).

En pratique, c'est généralement ce dernier.


4
Si je me souviens bien, la norme ANSI C dit explicitement que cela dépend de l'implémentation, vous devez donc vérifier la documentation de votre compilateur pour voir comment il est implémenté si vous souhaitez décaler à droite les entiers signés sur votre code.
Joe Pineda

Oui, je voulais juste souligner que la norme ANSI elle-même le dit, ce n'est pas un cas où les fournisseurs ne respectent tout simplement pas la norme ou que la norme ne dit rien sur ce cas particulier.
Joe Pineda

22

J'écris uniquement des trucs et astuces. Il peut être utile dans les tests et les examens.

  1. n = n*2: n = n<<1
  2. n = n/2: n = n>>1
  3. Vérifier si n est une puissance de 2 (1,2,4,8, ...): vérifier !(n & (n-1))
  4. Obtenir le x ème bit de n:n |= (1 << x)
  5. Vérifier si x est pair ou impair: x&1 == 0(pair)
  6. Basculez le n ème bit de x:x ^ (1<<n)

Il doit y en avoir quelques autres que vous connaissez maintenant?
ryyker

@ryyker J'en ai ajouté quelques autres. Je vais essayer de continuer à le mettre à jour :)
Ravi Prakash

X et n 0 sont-ils indexés?
reggaeguitar

Annonce 5: Et si c'est un nombre négatif?
Peter Mortensen

alors, pouvons-nous conclure que 2 en binaire est comme 10 en décimal? et le décalage de bits, c'est comme ajouter ou soustraire un nombre de plus derrière un autre nombre en décimal?
Willy satrio nugroho

8

Notez que dans l'implémentation Java, le nombre de bits à décaler est modifié par la taille de la source.

Par exemple:

(long) 4 >> 65

est égal à 2. Vous pourriez vous attendre à décaler les bits vers la droite 65 fois, tout serait mis à zéro, mais c'est en fait l'équivalent de:

(long) 4 >> (65 % 64)

Cela est vrai pour <<, >> et >>>. Je ne l'ai pas essayé dans d'autres langues.


Huh, intéressant! En C, il s'agit d' un comportement techniquement indéfini . gcc 5.4.0donne un avertissement, mais donne 2pour 5 >> 65; ainsi que.
pizzapants184

2

Quelques opérations / manipulations de bits utiles en Python.

J'ai implémenté la réponse de Ravi Prakash en 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

Sachez que seule la version 32 bits de PHP est disponible sur la plate-forme Windows.

Ensuite, si vous décalez par exemple << ou >> de plus de 31 bits, les résultats sont inattendus. Habituellement, le numéro d'origine au lieu de zéros sera retourné, et cela peut être un bug vraiment délicat.

Bien sûr, si vous utilisez la version 64 bits de PHP (Unix), vous devez éviter de décaler de plus de 63 bits. Cependant, par exemple, MySQL utilise le BIGINT 64 bits, donc il ne devrait pas y avoir de problèmes de compatibilité.

MISE À JOUR: à partir de PHP 7 Windows, les versions PHP sont enfin capables d'utiliser des entiers 64 bits complets: la taille d'un entier dépend de la plate-forme, bien qu'une valeur maximale d'environ deux milliards soit la valeur habituelle (c'est-à-dire 32 bits signés). Les plates-formes 64 bits ont généralement une valeur maximale d'environ 9E18, sauf sur Windows avant PHP 7, où il était toujours de 32 bits.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.