web-dev-qa-db-fra.com

applications pratiques des opérations au niveau du bit

  1. Pourquoi avez-vous utilisé des opérations au niveau du bit?
  2. pourquoi sont-ils si pratiques?
  3. quelqu'un peut-il s'il vous plaît recommander un tutoriel TRÈS simple?

Bien que tout le monde semble accroché à l'utilisation des drapeaux, ce n'est pas la seule application des opérateurs au niveau du bit (bien que probablement la plus courante). C # est également un langage de niveau suffisamment élevé pour que d'autres techniques soient probablement rarement utilisées, mais cela vaut la peine de les connaître. Voici à quoi je peux penser:


Le << et >> les opérateurs peuvent rapidement se multiplier par une puissance de 2. Bien sûr, l'optimiseur .NET JIT le fera probablement pour vous (et tout compilateur décent d'une autre langue également), mais si vous vous inquiétez vraiment à chaque microseconde, vous pourrait juste écrire ceci pour être sûr.

Une autre utilisation courante de ces opérateurs consiste à remplir deux entiers 16 bits en un entier 32 bits. Comme:

int Result = (shortIntA << 16 ) | shortIntB;

Ceci est courant pour l'interfaçage direct avec les fonctions Win32, qui utilisent parfois cette astuce pour des raisons héritées.

Et, bien sûr, ces opérateurs sont utiles lorsque vous voulez confondre les inexpérimentés, comme pour répondre à une question de devoirs. :)

Dans tout code réel, vous serez bien mieux en utilisant la multiplication à la place, car il a une bien meilleure lisibilité et le JIT l'optimise de toute façon pour les instructions shl et shr donc il n'y a pas de performances peine.


Pas mal de trucs curieux concernent le ^ opérateur (XOR). Il s'agit en fait d'un opérateur très puissant, en raison des propriétés suivantes:

  • A^B == B^A
  • A^B^A == B
  • Si tu sais A^B alors il est impossible de dire ce que sont A et B, mais si vous en connaissez un, vous pouvez calculer l'autre.
  • L'opérateur ne souffre d'aucun débordement comme la multiplication/division/addition/soustraction.

Quelques astuces que j'ai vues en utilisant cet opérateur:

Permutation de deux variables entières sans variable intermédiaire:

A = A^B // A is now XOR of A and B
B = A^B // B is now the original A
A = A^B // A is now the original B

Liste à double liaison avec une seule variable supplémentaire par élément. Cela aura peu d'utilité en C #, mais cela pourrait être utile pour la programmation de bas niveau des systèmes embarqués où chaque octet compte.

L'idée est que vous gardiez une trace du pointeur pour le premier élément; le pointeur pour le dernier élément; et pour chaque élément dont vous faites le suivi pointer_to_previous ^ pointer_to_next. De cette façon, vous pouvez parcourir la liste de chaque côté, mais le surcoût n'est que la moitié de celui d'une liste chaînée traditionnelle. Voici le code C++ pour la traversée:

ItemStruct *CurrentItem = FirstItem, *PreviousItem=NULL;
while (  CurrentItem != NULL )
{
    // Work with CurrentItem->Data

    ItemStruct *NextItem = CurrentItem->XorPointers ^ PreviousItem;
    PreviousItem = CurrentItem;
    CurrentItem = NextItem;
}

Pour parcourir à partir de la fin, il vous suffit de changer la toute première ligne de FirstItem à LastItem. C'est une autre économie de mémoire.

Un autre endroit où j'utilise le ^ L'opérateur sur une base régulière en C # est quand je dois calculer un HashCode pour mon type qui est un type composite. Comme:

class Person
{
    string FirstName;
    string LastName;
    int Age;

    public int override GetHashCode()
    {
        return (FirstName == null ? 0 : FirstName.GetHashCode()) ^
            (LastName == null ? 0 : LastName.GetHashCode()) ^
            Age.GetHashCode();
    }
}
77
Vilx-

J'utilise des opérateurs au niveau du bit pour la sécurité de mes applications. Je vais stocker les différents niveaux à l'intérieur d'un Enum:

[Flags]
public enum SecurityLevel
{
    User = 1, // 0001
    SuperUser = 2, // 0010
    QuestionAdmin = 4, // 0100
    AnswerAdmin = 8 // 1000
}

Et puis attribuez à un utilisateur leurs niveaux:

// Set User Permissions to 1010
//
//   0010
// | 1000
//   ----
//   1010
User.Permissions = SecurityLevel.SuperUser | SecurityLevel.AnswerAdmin;

Et puis vérifiez les autorisations dans l'action en cours:

// Check if the user has the required permission group
//
//   1010
// & 1000
//   ----
//   1000
if( (User.Permissions & SecurityLevel.AnswerAdmin) == SecurityLevel.AnswerAdmin )
{
    // Allowed
}
67
Justin Niessner

Je ne sais pas comment résoudre un sudoku que vous considérez comme pratique, mais supposons que ce soit le cas.

Imaginez que vous vouliez écrire un solveur sudoku ou même simplement un programme simple, qui vous montre le tableau et vous permet de résoudre le puzzle vous-même, mais vous assure que les mouvements sont légaux.

La carte elle-même sera très probablement représentée par un tableau à deux dimensions comme:

uint [, ] theBoard = new uint[9, 9];

Valeur 0 signifie que la cellule est toujours vide et que les valeurs de la plage [1u, 9u] sont les valeurs réelles de la carte.

Imaginez maintenant que vous vouliez vérifier si un déménagement est légal. Évidemment, vous pouvez le faire avec quelques boucles, mais les masques de bit vous permettent de faire les choses beaucoup plus rapidement. Dans un programme simple qui garantit simplement que les règles sont respectées, cela n'a pas d'importance, mais dans un solveur, cela pourrait.

Vous pouvez gérer des tableaux de masques de bits, qui stockent des informations sur les nombres qui sont déjà insérés dans chaque ligne, chaque colonne a et chaque boîte 3x3.

uint [] maskForNumbersSetInRow = new uint[9];

uint [] maskForNumbersSetInCol = new uint[9];

uint [, ] maskForNumbersSetInBox = new uint[3, 3];

Le mappage du nombre au modèle de bit, avec un bit correspondant à ce jeu de nombres, est très simple

1 -> 00000000 00000000 00000000 00000001
2 -> 00000000 00000000 00000000 00000010
3 -> 00000000 00000000 00000000 00000100
...
9 -> 00000000 00000000 00000001 00000000

En C #, vous pouvez calculer le bitpattern de cette façon (value est un uint):

uint bitpattern = 1u << (int)(value - 1u);

Dans la ligne ci-dessus 1u correspondant au bitpattern 00000000 00000000 00000000 00000001 est décalé vers la gauche de value - 1. Si, par exemple value == 5, vous obtenez

00000000 00000000 00000000 00010000

Au début, le masque de chaque ligne, colonne et case est 0. Chaque fois que vous mettez un nombre sur la carte, vous mettez à jour le masque, de sorte que le bit correspondant à la nouvelle valeur est défini.

Supposons que vous insériez la valeur 5 dans la ligne 3 (les lignes et les colonnes sont numérotées à partir de 0). Le masque de la ligne 3 est stocké dans maskForNumbersSetInRow[3]. Supposons également qu'avant l'insertion, il y avait déjà des nombres {1, 2, 4, 7, 9} dans la ligne 3. Le motif de bits dans le masque maskForNumbersSetInRow[3] ressemble à ça:

00000000 00000000 00000001 01001011
bits above correspond to:9  7  4 21

Le but est de régler le bit correspondant à la valeur 5 dans ce masque. Vous pouvez le faire en utilisant l'opérateur au niveau du bit ou (|). Vous créez d'abord un motif de bits correspondant à la valeur 5

uint bitpattern = 1u << 4; // 1u << (int)(value - 1u)

puis vous utilisez le operator | pour mettre le bit dans le masque maskForNumbersSetInRow[3]

maskForNumbersSetInRow[3] = maskForNumbersSetInRow[3] | bitpattern;

ou en utilisant une forme plus courte

maskForNumbersSetInRow[3] |= bitpattern;

00000000 00000000 00000001 01001011
                 |
00000000 00000000 00000000 00010000
                 =
00000000 00000000 00000001 01011011

Maintenant, votre masque indique qu'il existe des valeurs {1, 2, 4, 5, 7, 9} dans cette ligne (ligne 3).

Si vous voulez vérifier, si une valeur est dans la ligne, vous pouvez utiliser operator & pour vérifier si le bit correspondant est défini dans le masque. Si le résultat de cet opérateur appliqué au masque et un motif de bits, correspondant à cette valeur, est différent de zéro, la valeur est déjà dans la ligne. Si le résultat est 0, la valeur n'est pas dans la ligne.

Par exemple, si vous souhaitez vérifier si la valeur 3 est dans la ligne, vous pouvez le faire de cette façon:

uint bitpattern = 1u << 2; // 1u << (int)(value - 1u)
bool value3IsInRow = ((maskForNumbersSetInRow[3] & bitpattern) != 0);

00000000 00000000 00000001 01001011 // the mask
                 |
00000000 00000000 00000000 00000100 // bitpattern for the value 3
                 =
00000000 00000000 00000000 00000000 // the result is 0. value 3 is not in the row.

Vous trouverez ci-dessous des méthodes pour définir une nouvelle valeur dans la carte, maintenir à jour les masques de bit appropriés et vérifier si un mouvement est légal.

public void insertNewValue(int row, int col, uint value)
{

    if(!isMoveLegal(row, col, value))
        throw ...

    theBoard[row, col] = value;

    uint bitpattern = 1u << (int)(value - 1u);

    maskForNumbersSetInRow[row] |= bitpattern;

    maskForNumbersSetInCol[col] |= bitpattern;

    int boxRowNumber = row / 3;
    int boxColNumber = col / 3;

    maskForNumbersSetInBox[boxRowNumber, boxColNumber] |= bitpattern;

}

Ayant les masques, vous pouvez vérifier si le déménagement est légal comme ceci:

public bool isMoveLegal(int row, int col, uint value)
{

    uint bitpattern = 1u << (int)(value - 1u);

    int boxRowNumber = row / 3;
    int boxColNumber = col / 3;

    uint combinedMask = maskForNumbersSetInRow[row] | maskForNumbersSetInCol[col]
                        | maskForNumbersSetInBox[boxRowNumber, boxColNumber];

    return ((theBoard[row, col] == 0) && ((combinedMask & bitpattern) == 0u);
}
16
Maciej Hehl

Des dizaines d'exemples de twiddling de bits ici

Le code est en C, mais vous pouvez facilement l'adapter en C #

4
Thomas Levesque

Si jamais vous avez besoin de communiquer avec du matériel, vous devrez utiliser le bit twiddling à un moment donné.

Extraire les valeurs RVB d'une valeur de pixel.

Tant de choses

3
James

Ils peuvent être utilisés pour toute une série d'applications différentes, voici une question que j'ai déjà postée ici, qui utilise des opérations au niveau du bit:

Bitwise AND, Bitwise Inclusive OR question, en Java

Pour d'autres exemples, regardez (par exemple) les énumérations marquées.

Dans mon exemple, j'utilisais des opérations au niveau du bit pour changer la plage d'un nombre binaire de -128 ... 127 à 0..255 (en changeant sa représentation de signé à non signé).

l'article MSN ici ->

http://msdn.Microsoft.com/en-us/library/6a71f45d%28VS.71%29.aspx

est utile.

Et, bien que ce lien:

http://weblogs.asp.net/alessandro/archive/2007/10/02/bitwise-operators-in-c-or-xor-and-amp-amp-not.aspx

est très technique, il couvre tout.

HTH

2
Dave

Chaque fois que vous avez une option de 1 ou plus en combinaison d'éléments, le mode bit à bit est généralement une solution facile.

Quelques exemples incluent des bits de sécurité (en attente de l'échantillon de Justin ..), des jours de planification, etc.

2
NotMe

Je dois dire que l'une des utilisations les plus courantes consiste à modifier les champs de bits pour compresser les données. Vous le voyez principalement dans les programmes qui tentent d'être économiques avec les paquets.

Exemple de compression réseau utilisant des champs de bits

2
Greg Buehler

L'une des choses les plus fréquentes pour lesquelles je les utilise en C # est la production de codes de hachage. Il existe des méthodes de hachage assez bonnes qui les utilisent. Par exemple. pour une classe de coordonnées avec un X et un Y qui étaient les deux pouces, je pourrais utiliser:

public override int GetHashCode()
{
  return x ^ ((y << 16) | y >> 16);
}

Cela génère rapidement un nombre qui est garanti égal lorsqu'il est produit par un objet égal (en supposant que l'égalité signifie que les paramètres X et Y sont les mêmes dans les deux objets comparés) tout en ne produisant pas de modèles de conflit pour les objets de faible valeur (susceptibles d'être le plus courant dans la plupart des applications).

Un autre combine les énumérations d'indicateurs. Par exemple. RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase

Il y a quelques opérations de bas niveau qui ne sont généralement pas nécessaires lorsque vous codez contre un framework comme .NET (par exemple en C #, je n'aurai pas besoin d'écrire du code pour convertir UTF-8 en UTF-16, il est là pour moi dans le cadre), mais bien sûr, quelqu'un devait écrire ce code.

Il existe quelques techniques de torsion des bits, comme l'arrondi au nombre binaire le plus proche (par exemple, arrondir de 1010 à 10000):

        unchecked
        {
            --x;
            x |= (x >> 1);
            x |= (x >> 2);
            x |= (x >> 4);
            x |= (x >> 8);
            x |= (x >> 16);
            return ++x;
        }

Qui sont utiles lorsque vous en avez besoin, mais cela n'a pas tendance à être très courant.

Enfin, vous pouvez également les utiliser pour micro-optimiser des mathématiques telles que << 1 au lieu de * 2 mais j'inclus cela seulement pour dire que c'est généralement une mauvaise idée car elle cache l'intention du vrai code, n'économise presque rien dans les performances et peut cacher quelques bugs subtils.

2
Jon Hanna

Vous les utiliserez pour diverses raisons:

  • stocker (et vérifier!) les drapeaux d'options d'une manière efficace en mémoire
  • si vous faites de la programmation informatique, vous voudrez peut-être envisager d'optimiser certaines de vos opérations en utilisant des opérations au niveau du bit au lieu d'opérateurs mathématiques (méfiez-vous des effets secondaires)
  • Code gris !
  • création de valeurs énumérées

Je suis sûr que vous pouvez penser aux autres.

Cela étant dit, vous devez parfois vous demander: est-ce que l'augmentation de la mémoire et des performances en vaut la peine? Après avoir écrit ce type de code, laissez-le reposer un moment et revenez-y. Si vous rencontrez des difficultés, réécrivez avec un code plus facile à gérer.

D'un autre côté, il est parfois parfaitement logique d'utiliser des opérations au niveau du bit (pensez à la cryptographie).

Mieux encore, faites-le lire par quelqu'un d'autre et documentez abondamment.

1
haylem
  1. Ils peuvent être utilisés pour passer de nombreux arguments à une fonction via une variable de taille limitée.
  2. Les avantages sont une faible surcharge de mémoire ou un faible coût de mémoire: donc des performances accrues.
  3. Je ne peux pas écrire un tutoriel sur place, mais ils sont là, j'en suis sûr.
1
C Johnson

Tri binaire. Il y avait des problèmes où l'implémentation utilisait un opérateur de division au lieu d'un opérateur de décalage de bits. Cela a provoqué l'échec de BS après que la collection ait atteint des tailles supérieures à 10 000 000

1
Woot4Moo

Jeux!

À l'époque, je l'ai utilisé pour représenter les pièces d'un joueur Reversi. C'est 8X8 donc il m'a fallu un type long, et que, par exemple, si vous voulez savoir où sont toutes les pièces à bord - vous or les deux pièces des joueurs.
Si vous voulez toutes les étapes possibles d'un joueur, dites à droite - vous >> la représentation des pièces du joueur par une, et AND avec les pièces de l'adversaire pour vérifier s'il y a maintenant des 1 communs (cela signifie qu'il y a une pièce d'adversaire à votre droite). Ensuite, vous continuez à faire ça. si vous revenez à vos propres pièces - pas de mouvement. Si vous arrivez à un point clair - vous pouvez vous y déplacer et capturer toutes les pièces sur le chemin.
(Cette technique est largement utilisée, dans de nombreux types de jeux de société, y compris les échecs)

1
Oren A