web-dev-qa-db-fra.com

C # rotation au niveau du bit à gauche et rotation à droite

Quel est l'équivalent C # (.NET 2.0) de _rotl et _rotr de C++?

36
Prithis

C'est ce que vous essayez de faire?

Jon Skeet a répondu à cela sur un autre site

Fondamentalement, ce que vous voulez, c'est

(pour gauche)

(original << bits) | (original >> (32 - bits))

ou

(pour la droite)

(original >> bits) | (original << (32 - bits))

De plus, comme Mehrdad l'a déjà suggéré, cela ne fonctionne que pour uint, qui est l'exemple que Jon donne également.

44
Joseph

Il n'y a pas de fonctionnalité de langage intégrée pour la rotation des bits en C #, mais ces méthodes d'extension devraient faire le travail:

public static uint RotateLeft(this uint value, int count)
{
    return (value << count) | (value >> (32 - count))
}

public static uint RotateRight(this uint value, int count)
{
    return (value >> count) | (value << (32 - count))
}

Remarque: Comme Mehrdad le fait remarquer, décalage à droite (>>) pour les entiers signés est une particularité: il remplit les MSB avec un bit de signe plutôt que 0 comme pour les nombres non signés. J'ai maintenant changé les méthodes pour prendre et renvoyer uint (entier 32 bits non signé) à la place - cela est également plus conforme aux fonctions C++ rotl et rotr. Si vous voulez faire pivoter des entiers, cassez-les juste avant de passer, et encore une fois castez la valeur de retour, bien sûr.

Exemple d'utilisation:

int foo1 = 8.RotateRight(3); // foo1 = 1
int foo2 = int.MinValue.RotateLeft(3); // foo2 = 4

(Notez que int.MinValue est 111111111111111111111111 - 32 1s en binaire.)

24
Noldorin

Avec le dernier C # 7 , vous pouvez maintenant créer by-ref méthodes d'extension, afin que vous puissiez vous débarrasser de la tâche occupée de stocker constamment la valeur de retour de la fonction d'assistance dans la variable.

Cela rationalise bien les fonctions de rotation et élimine une classe commune de bogues où vous oubliez de réenregistrer la valeur de retour de la fonction, tout en introduisant éventuellement un nouveau type de bogue complètement différent - où ValueTypes obtient par inadvertance modifié in-situ quand vous ne vouliez pas ou ne vous attendiez pas à ce qu'ils soient.

public static void Rol(ref this ulong ul) => ul = (ul << 1) | (ul >> 63);

public static void Rol(ref this ulong ul, int N) => ul = (ul << N) | (ul >> (64 - N));

public static void Ror(ref this ulong ul) => ul = (ul << 63) | (ul >> 1);

public static void Ror(ref this ulong ul, int N) => ul = (ul << (64 - N)) | (ul >> N);
///   note: ---^        ^---^--- extension method can now use 'ref' for ByRef semantics

Habituellement, je serais sûr de mettre [MethodImpl(MethodImplOptions.AggressiveInlining)] sur de petites méthodes comme celles-ci, mais après quelques recherches (sur x64), j'ai découvert que ce n'était pas du tout nécessaire ici. Si le JIT détermine que la méthode est éligible (par exemple, si vous décochez la case du débogueur VisualStudio 'Suppression de l'optimisation JIT' , qui est activée par défaut), les méthodes seront intégrées indépendamment, et c'est le cas ici .

Dans le cas où le terme n'est pas familier, [~ # ~] jit [~ # ~] , ou "juste à temps" fait référence à la conversion unique des instructions IL en code natif réglé pour la plate-forme détectée lors de l'exécution, un processus qui se produit à la demande, par méthode en tant que . NET le programme s'exécute.

Pour démontrer l'utilisation d'une méthode d'extension by-ref, je me concentrerai uniquement sur la première méthode indiquée ci-dessus "tourner à gauche" et comparer la sortie JIT entre la méthode d'extension traditionnelle par valeur et la nouvelle by-ref approche. Voici les deux méthodes de test à comparer sur x64 Release in . NET 4.7 sous Windows 10. Comme indiqué ci-dessus, ce sera avec l'optimisation JIT "non supprimée", donc dans ces conditions de test comme vous le verrez, les fonctions disparaîtront complètement dans le code en ligne .

static ulong Rol_ByVal(this ulong ul) => (ul << 1) | (ul >> 63);

static void Rol_ByRef(ref this ulong ul) => ul = (ul << 1) | (ul >> 63);
//                 notice reassignment here ---^  (c̲a̲l̲l̲e̲e̲ doing it instead of caller)

Et voici le code C # pour chaque site d'appel correspondant. Étant donné que le code AMD64 entièrement optimisé pour JIT est si petit, je peux simplement l'inclure ici également. C'est le cas optimal:

static ulong x = 1;   // static so it won't be optimized away in this simple test

// ------- ByVal extension method; c̲a̲l̲l̲e̲r̲ must reassign 'x' with the result -------

                     x = x.Rol_ByVal();
// 00007FF969CC0481  mov         rax,qword ptr [7FF969BA4888h]  
// 00007FF969CC0487  rol         rax,1  
// 00007FF969CC048A  mov         qword ptr [7FF969BA4888h],rax  


// ------- New in C#7, ByRef extension method can directly alter 'x' in-situ -------

                     x.Rol_ByRef(); 
// 00007FF969CC0491  rol         qword ptr [7FF969BA4888h],1  

Sensationnel. Oui, ce n'est pas une blague. Dès le départ, nous pouvons voir que le manque flagrant d'une OpCodes.Rot - famille d'instructions dans ECMA CIL (.NET) intermédiaire la langue n'est pratiquement pas un problème; La gigue a pu voir à travers notre pile de code de contournement C # (ul << 1) | (ul >> 63) Pour deviner son intention essentielle, que dans les deux cas le JIT x64 implémente en émettant simplement une instruction native rol. De façon impressionnante, la version ByRef utilise une seule instruction pour effectuer la rotation directement sur l'adresse cible de la mémoire principale sans même la charger dans un registre.

Dans le cas ByVal, vous pouvez toujours voir une trace résiduelle de la copie excessive qui était nécessaire pour laisser la valeur d'origine de l'appelant inchangée, avant que la méthode appelée soit entièrement optimisée (comme c'est l'essence de la sémantique de type valeur). Pour la rotation d'entier ici, c'est juste une extraction/stockage supplémentaire de l'entier cible dans un registre 64 bits rax.

Pour clarifier cela, supprimons à nouveau les optimisations JIT dans la session de débogage. Cela fera revenir nos méthodes d'extension d'aide, avec des corps pleins et des cadres de pile pour mieux expliquer la première phrase du paragraphe précédent. Voyons d'abord les sites d'appel. Ici, nous pouvons voir l'effet de la sémantique traditionnelle de ValueType, qui se résume à ce qu'aucun cadre de pile inférieur ne puisse manipuler les copies ValueType d'un cadre parent:

par valeur:

                     x = x.Rol_ByVal();
// 00007FF969CE049C  mov         rcx,qword ptr [7FF969BC4888h]  
// 00007FF969CE04A3  call        00007FF969CE00A8  
// 00007FF969CE04A8  mov         qword ptr [rbp-8],rax  
// 00007FF969CE04AC  mov         rcx,qword ptr [rbp-8]  
// 00007FF969CE04B0  mov         qword ptr [7FF969BC4888h],rcx  

par référence

                     x.Rol_ByRef();
// 00007FF969CE04B7  mov         rcx,7FF969BC4888h  
// 00007FF969CE04C1  call        00007FF969CE00B0
//             ...all done, nothing to do here; the callee did everything in-place for us

Comme nous pouvons nous y attendre du code C # associé à chacun de ces deux fragments, nous voyons que le by-val l'appelant a un tas de travail à faire après le retour de l'appel. Il s'agit du processus de remplacement de la copie parente de la valeur ulong 'x' par la valeur ulong complètement indépendante qui est retournée dans le registre rax.

Examinons maintenant le code des fonctions cibles appelées. Les voir nécessite de forcer le JIT à "supprimer" les optimisations. Ce qui suit est le code natif que le JIT Release x64 émet pour les fonctions Rol_ByVal Et Rol_ByRef.

Afin de me concentrer sur la différence minuscule mais cruciale entre les deux, j'ai enlevé une partie du passe-partout administratif. (J'ai quitté la configuration et le démontage du cadre de pile pour le contexte et pour montrer comment dans cet exemple, ces éléments auxiliaires éclipsent à peu près les instructions réelles.) Pouvez-vous voir l'indirection du ByRef au travail? Eh bien, ça aide que je l'ai souligné: - /

                 static ulong Rol_ByVal(this ulong ul) => (ul << 1) | (ul >> 63);
// 00007FF969CD0760  Push        rbp  
// 00007FF969CD0761  sub         rsp,20h  
// 00007FF969CD0765  lea         rbp,[rsp+20h]  
// ...
// 00007FF969CE0E4C  mov         rax,qword ptr [rbp+10h]  
// 00007FF969CE0E50  rol         rax,1  
// 00007FF969CD0798  lea         rsp,[rbp]  
// 00007FF969CD079C  pop         rbp  
// 00007FF969CD079D  ret  

                 static void Rol_ByRef(ref this ulong ul) => ul = (ul << 1) | (ul >> 63);
// 00007FF969CD0760  Push        rbp  
// 00007FF969CD0761  sub         rsp,20h  
// 00007FF969CD0765  lea         rbp,[rsp+20h]  
// ...
// 00007FF969CE0E8C  mov         rax,qword ptr [rbp+10h]  
// 00007FF969CE0E90  rol         qword ptr [rax],1              <--- !
// 00007FF969CD0798  lea         rsp,[rbp]  
// 00007FF969CD079C  pop         rbp  
// 00007FF969CD079D  ret  

Vous remarquerez peut-être que les deux appels transmettent en fait l'instance parent de la valeur ulong par référence - les deux exemples sont identiques à cet égard. Le parent indique l'adresse où sa copie privée de ul réside dans le cadre de pile supérieur. Il s'avère qu'il n'est pas nécessaire d'isoler les callees de - lecture des cas où elles se trouvent, tant que nous pouvons être sûrs qu'elles n'écrivent jamais sur ces pointeurs. Il s'agit d'une approche "paresseuse" ou différée qui attribue à chaque trame de pile inférieure (enfant) la responsabilité de préserver la sémantique ValueType de ses appelants supérieurs. Il n'est pas nécessaire qu'un appelant copie proactivement tout ValueType transmis à un cadre enfant si l'enfant ne finit jamais par l'écraser; pour éviter autant que possible les copies inutiles, seul l'enfant peut prendre la décision la plus récente possible.

Il est également intéressant de noter que nous pourrions avoir ici une explication pour l'utilisation maladroite de rax dans le premier exemple "ByVal" que j'ai montré. Après que la méthode par valeur ait été complètement réduite via l'inline, pourquoi la rotation devait-elle encore se produire dans un registre?

Eh bien, dans ces deux dernières versions de corps de méthode complet, il est clair que la première méthode renvoie ulong et la seconde est void. Puisqu'une valeur de retour est passée dans rax, la méthode ByVal doit la récupérer dans ce registre de toute façon, il est donc évident de la faire pivoter là aussi. Étant donné que la méthode ByRef n'a pas besoin de renvoyer de valeur, elle n'a pas besoin de coller quoi que ce soit pour son appelant, et encore moins dans rax. Il semble probable que "ne pas avoir à se soucier de rax" libère le code ByRef pour cibler l'instance ulong que son parent a partagée "où il se trouve", en utilisant la fantaisie qword ptr à indirect dans la mémoire de trame de pile du parent, au lieu d'utiliser un registre. Voilà donc mon explication spéculative, mais peut-être crédible, pour le mystère "résiduel rax" que nous avons vu plus tôt.

13
Glenn Slayden

La version naïve du décalage ne fonctionnera pas. La raison en est que les nombres signés décalés vers la droite rempliront les bits de gauche avec bit de signe, pas:

Vous pouvez vérifier ce fait avec:

Console.WriteLine(-1 >> 1);

La bonne façon est:

public static int RotateLeft(this int value, int count)
{
    uint val = (uint)value;
    return (int)((val << count) | (val >> (32 - count)));
}

public static int RotateRight(this int value, int count)
{
    uint val = (uint)value;
    return (int)((value >> count) | (value << (32 - count)));
}
9
Mehrdad Afshari

Notez que si vous souhaitez créer des surcharges qui fonctionnent sur des valeurs intégrales plus courtes, vous devez ajouter une étape supplémentaire, comme indiqué dans:

public static byte RotateLeft(
    this byte value,
    int count )
{
    // Unlike the RotateLeft( uint, int ) and RotateLeft( ulong, int ) 
    // overloads, we need to mask out the required bits of count 
    // manually, as the shift operaters will promote a byte to uint, 
    // and will not mask out the correct number of count bits.
    count &= 0x07;
    return (byte)((value << count) | (value >> (8 - count)));
}

L'opération de masquage n'est pas nécessaire pour les surcharges 32 bits et 64 bits, car les opérateurs de changement eux-mêmes s'en occupent pour ces tailles d'opérandes gauches.

2
John Beyer