Pourquoi (a% 256) est-il différent de (a & 0xFF)?

145

J'ai toujours supposé qu'en faisant (a % 256)l'optimiseur utiliserait naturellement une opération efficace au niveau du bit, comme si j'écrivais (a & 0xFF).

Lors du test sur l'explorateur de compilateur gcc-6.2 (-O3):

// Type your code here, or load an example.
int mod(int num) {
    return num % 256;
}

mod(int):
    mov     edx, edi
    sar     edx, 31
    shr     edx, 24
    lea     eax, [rdi+rdx]
    movzx   eax, al
    sub     eax, edx
    ret

Et en essayant l'autre code:

// Type your code here, or load an example.
int mod(int num) {
    return num & 0xFF;
}

mod(int):
    movzx   eax, dil
    ret

Il semble que je manque complètement quelque chose. Des idées?

Elad Weiss
la source
64
0xFF est 255 et non 256.
Rishikesh Raje
186
@RishikeshRaje: Alors? %n'est pas non &plus.
usr2564301
27
@RishikeshRaje: Je suis sûr que l'OP en est très conscient. Ils sont utilisés avec différentes opérations.
Bravo et hth. - Alf
28
Par intérêt, obtenez-vous de meilleurs résultats si numoui unsigned?
Bathsheba
20
@RishikeshRaje Bitwise et 0xFF équivaut à modulo 2 ^ 8 pour les entiers non signés.
2501

Réponses:

230

Ce n'est pas la même chose. Essayez num = -79, et vous obtiendrez des résultats différents des deux opérations. (-79) % 256 = -79, tandis que (-79) & 0xffc'est un certain nombre positif.

En utilisant unsigned int, les opérations sont les mêmes et le code sera probablement le même.

PS - Quelqu'un a commenté

Ils ne devraient pas être les mêmes, a % best défini comme a - b * floor (a / b).

Ce n'est pas ainsi que cela est défini en C, C ++, Objective-C (c'est-à-dire tous les langages où le code de la question se compilerait).

gnasher729
la source
Les commentaires ne sont pas destinés à une discussion approfondie; cette conversation a été déplacée vers le chat .
Martijn Pieters
52

Réponse courte

-1 % 256cède -1et non 255ce qui est -1 & 0xFF. Par conséquent, l'optimisation serait incorrecte.

Longue réponse

C ++ a la convention que (a/b)*b + a%b == a, ce qui semble tout à fait naturel. a/brenvoie toujours le résultat arithmétique sans la partie fractionnaire (tronquée vers 0). Par conséquent, a%ba le même signe que aou vaut 0.

La division -1/256cède 0et -1%256doit donc être -1pour satisfaire la condition ci-dessus ( (-1%256)*256 + -1%256 == -1). C'est évidemment différent de -1&0xFFce qui est 0xFF. Par conséquent, le compilateur ne peut pas optimiser comme vous le souhaitez.

La section pertinente de la norme C ++ [expr.mul §4] à partir de N4606 déclare:

Pour les opérandes intégraux, l' /opérateur donne le quotient algébrique avec toute partie fractionnaire rejetée; si le quotient a/best représentable dans le type du résultat, (a/b)*b + a%best égal à a[...].

Activer l'optimisation

Cependant, en utilisant des unsignedtypes, l'optimisation serait complètement correcte , satisfaisant la convention ci-dessus:

unsigned(-1)%256 == 0xFF

Voyez aussi ceci .

Autres langues

Ceci est géré de manière très différente selon les langages de programmation, comme vous pouvez le rechercher sur Wikipedia .

Ralph Tandetzky
la source
50

Depuis C ++ 11, num % 256doit être non positif si numest négatif.

Ainsi, le modèle de bits dépendrait de l'implémentation des types signés sur votre système: pour un premier argument négatif, le résultat n'est pas l'extraction des 8 bits les moins significatifs.

Ce serait une autre question si numdans votre cas était unsigned: ces jours-ci, je m'attendrais presque à ce qu'un compilateur fasse l'optimisation que vous citez.

Bathsheba
la source
6
Presque mais pas tout à fait. Si numest négatif, alors num % 256est zéro ou négatif (c'est-à-dire non positif).
Nayuki
5
Quel IMO, est une erreur dans la norme: l'opération modulo mathématiquement doit prendre le signe du diviseur, 256 dans ce cas. Afin de comprendre pourquoi considérer cela (-250+256)%256==6, mais (-250%256)+(256%256)doit être, selon la norme, "non positif", et donc pas 6. Briser l'associativité comme celle-ci a des effets secondaires réels: par exemple, lors du calcul du "zoom arrière", le rendu en coordonnées entières, il faut décaler l'image pour que toutes les coordonnées soient non négatives.
Michael
2
@Michael Modulus n'a jamais été distributif par rapport à l'addition ("associatif" est le mauvais nom pour cette propriété!), Même si vous suivez la définition mathématique à la lettre. Par exemple, (128+128)%256==0mais (128%256)+(128%256)==256. Il y a peut-être une bonne objection au comportement spécifié, mais je ne suis pas sûr que ce soit celui que vous avez dit.
Daniel Wagner
1
@DanielWagner, vous avez raison, bien sûr, je me suis mal exprimé avec "associatif". Cependant, si l'on garde le signe du diviseur et calcule tout en arithmétique modulaire, la propriété distributive tient; dans votre exemple, vous auriez 256==0. La clé est d'avoir exactement Nles valeurs possibles en Narithmétique modulo , ce qui n'est possible que si tous les résultats sont dans la plage 0,...,(N-1), non -(N-1),...,(N-1).
Michael
6
@Michael: Sauf que% n'est pas un opérateur modulo, c'est un opérateur de reste .
Joren
11

Je n'ai pas d'aperçu télépathique du raisonnement du compilateur, mais dans le cas où %il y a la nécessité de traiter des valeurs négatives (et la division arrondit vers zéro), alors que &le résultat est toujours les 8 bits inférieurs.

L' sarinstruction me semble "décaler l'arithmétique vers la droite", remplissant les bits vides avec la valeur du bit de signe.

Bravo et hth. - Alf
la source
0

Mathématiquement parlant, modulo est défini comme suit:

a% b = a - b * plancher (a / b)

Ce droit ici devrait éclaircir pour vous. Nous pouvons éliminer le plancher pour les nombres entiers car la division entière est équivalente au plancher (a / b). Cependant, si le compilateur devait utiliser une astuce générale comme vous le suggérez, il devrait fonctionner pour tout a et tout b. Malheureusement, ce n'est tout simplement pas le cas. Mathématiquement parlant, votre astuce est correcte à 100% pour les entiers non signés (je vois une réponse indique que les entiers signés cassent mais je peux le confirmer ou le nier car -a% b devrait être positif). Cependant, pouvez-vous faire cette astuce pour tous les b? Probablement pas. C'est pourquoi le compilateur ne le fait pas. Après tout, si modulo était facilement écrit comme une opération au niveau du bit, alors nous ajouterions simplement un circuit modulo comme pour l'addition et les autres opérations.

user64742
la source
4
Je pense que vous confondez «plancher» avec «tronquer». Les premiers ordinateurs utilisaient la division tronquée car elle est souvent plus facile à calculer que la division par étage, même dans les cas où les choses se divisent uniformément. J'ai vu très peu de cas où la division tronquée était plus utile que la division au sol ne l'aurait été, mais de nombreux langages suivent l'exemple de FORTRAN consistant à utiliser la division tronquée.
supercat
@supercat Mathématiquement parlant, le plancher est tronqué. Ils ont tous deux le même effet. Ils peuvent ne pas être mis en œuvre de la même manière dans un ordinateur, mais ils font la même chose.
user64742
5
@TheGreatDuck: Ce n'est pas la même chose pour les nombres négatifs. Le plancher de -2.3est -3, tandis que si vous tronquez -2.3à un entier, vous obtenez -2. Voir en.wikipedia.org/wiki/Truncation . "pour les nombres négatifs, la troncature ne s'arrondit pas dans le même sens que la fonction de plancher". Et le comportement de %pour les nombres négatifs est précisément la raison pour laquelle l'OP voit le comportement décrit.
Mark Dickinson
@MarkDickinson Je suis à peu près sûr que modulo en C ++ donne des valeurs positives pour les diviseurs positifs, mais je ne vais pas discuter.
user64742
1
@TheGreatDuck - voir exemple: cpp.sh/3g7h (Notez que C ++ 98 n'a pas défini laquelle des deux variantes possibles était utilisée, mais que les normes plus récentes le font, il est donc possible que vous ayez utilisé une implémentation de C ++ dans le passé qui l'a fait différemment ...)
Periata Breatta