web-dev-qa-db-fra.com

Est-ce que ((a + (b & 255)) et 255) est identique à ((a + b) & 255)?

Je parcourais du code C++ et j'ai trouvé quelque chose comme:

(a + (b & 255)) & 255

Le double ET m'a agacé, alors j'ai pensé à:

(a + b) & 255

(a et b sont des entiers non signés 32 bits)

J'ai rapidement écrit un script de test (JS) pour confirmer ma théorie:

for (var i = 0; i < 100; i++) {
    var a = Math.ceil(Math.random() * 0xFFFF),
        b = Math.ceil(Math.random() * 0xFFFF);

    var expr1 = (a + (b & 255)) & 255,
        expr2 = (a + b) & 255;

    if (expr1 != expr2) {
        console.log("Numbers " + a + " and " + b + " mismatch!");
        break;
    }
}

Bien que le scénario ait confirmé mon hypothèse (les deux opérations sont égales), je n'y fais toujours pas confiance, car 1) aléatoire et 2) je ne suis pas mathématicien, je ne sais pas du tout que fais-je .

Aussi, désolé pour le titre LISP-y. N'hésitez pas à le modifier.

92
Martin

Ce sont les mêmes. Voici une preuve:

D'abord notez l'identité (A + B) mod C = (A mod C + B mod C) mod C

Reprenons le problème en considérant a & 255 comme remplaçant pour a % 256. Ceci est vrai puisque a n'est pas signé.

Alors (a + (b & 255)) & 255 est (a + (b % 256)) % 256

C'est pareil que (a % 256 + b % 256 % 256) % 256 _ (J'ai appliqué l'identité indiquée ci-dessus: notez que mod et % sont équivalents pour les types non signés.)

Ceci simplifie à (a % 256 + b % 256) % 256 qui devient (a + b) % 256 (réappliquant l'identité). Vous pouvez ensuite remettre l'opérateur au niveau du bit pour donner

(a + b) & 255

compléter la preuve.

77
Bathsheba

En addition de position, soustraction et multiplication de nombres non signés pour produire des résultats non signés, les chiffres les plus significatifs de l'entrée n'affectent pas les chiffres moins significatifs du résultat. Cela s'applique autant à l'arithmétique binaire qu'à l'arithmétique décimale. Cela s'applique également à l'arithmétique signée "complément à deux", mais pas à l'arithmétique signée de magnitude.

Cependant, nous devons faire attention lorsque nous prenons des règles d'arithmétique binaire et les appliquons au C (je pense que C++ a les mêmes règles que C sur ce sujet mais je ne suis pas sûr à 100%) car l'arithmétique C a des règles mystérieuses qui peuvent nous faire trébucher up. L'arithmétique non signée en C suit des règles enveloppantes binaires simples, mais le dépassement arithmétique signé est un comportement indéfini. Pire dans certaines circonstances, C "promouvra automatiquement" un type non signé en (signé) int.

Un comportement non défini en C peut être particulièrement insidieux. Un compilateur stupide (ou un compilateur dont le niveau d’optimisation est faible) fera probablement ce que vous attendez en fonction de votre compréhension de l’arithmétique binaire, tandis qu’un compilateur optimiseur risque de casser votre code de façon étrange.


Donc, pour revenir à la formule de la question, l'équivilence dépend des types d'opérandes.

S'il s'agit de nombres entiers non signés dont la taille est supérieure ou égale à celle de int, le comportement de débordement de l'opérateur d'addition est bien défini comme un enveloppement binaire simple. Que nous masquions ou non les 24 bits hauts d'un opérande avant l'opération d'addition n'a aucun impact sur les bits bas du résultat.

Si ce sont des entiers non signés dont la taille est inférieure à int, ils seront alors promus (signés) int. Le débordement d'entiers signés est un comportement non défini, mais au moins sur chaque plate-forme que j'ai rencontrée, la différence de taille entre différents types d'entiers est suffisamment importante pour qu'un simple ajout de deux valeurs promues ne provoque pas de débordement. Donc, encore une fois, nous pouvons revenir à l'argument simplement arithmétique binaire pour juger les déclarations équivalentes.

Si ce sont des entiers signés dont la taille est inférieure à int, là encore, le débordement ne peut pas se produire et sur les implémentations à deux compléments, nous pouvons nous fier à l'argument arithmétique binaire standard pour dire qu'ils sont équivalents. Sur le plan de la magnitude des signes ou de ceux qui complètent les implémentations, ils ne seraient pas équivalents.

OTOH si a et b étaient des entiers signés dont la taille était supérieure ou égale à la taille de int, alors même sur deux implémentations de complément, il y a des cas où une déclaration serait bien définie tandis que l'autre serait un comportement indéfini.

21
plugwash

Lemma: a & 255 == a % 256 Pour non signé a.

Unsigned a peut être réécrit comme m * 0x100 + b Certains m non signés, b, 0 <= b < 0xff, 0 <= m <= 0xffffff. Il résulte des deux définitions que a & 255 == b == a % 256.

De plus, nous avons besoin de:

  • la propriété distributive: (a + b) mod n = [(a mod n) + (b mod n)] mod n
  • la définition de l'addition non signée, mathématiquement: (a + b) ==> (a + b) % (2 ^ 32)

Ainsi:

(a + (b & 255)) & 255 = ((a + (b & 255)) % (2^32)) & 255      // def'n of addition
                      = ((a + (b % 256)) % (2^32)) % 256      // lemma
                      = (a + (b % 256)) % 256                 // because 256 divides (2^32)
                      = ((a % 256) + (b % 256 % 256)) % 256   // Distributive
                      = ((a % 256) + (b % 256)) % 256         // a mod n mod n = a mod n
                      = (a + b) % 256                         // Distributive again
                      = (a + b) & 255                         // lemma

Alors oui, c'est vrai. Pour les entiers non signés 32 bits.


Qu'en est-il des autres types entiers?

  • Pour les entiers non signés 64 bits, tout ce qui précède s'applique également, il suffit de remplacer 2^64 Par 2^32.
  • Pour les entiers non signés de 8 et 16 bits, l’addition implique une promotion vers int. Ce int ne débordera ni ne sera négatif dans aucune de ces opérations, elles resteront donc toutes valides.
  • Pour les entiers signés, si a+b Ou a+(b&255) overflow, il s'agit d'un comportement non défini. Donc l'égalité ne peut pas tenir - il y a des cas où (a+b)&255 Est un comportement indéfini alors que (a+(b&255))&255 Ne l'est pas.
20
Barry

Oui, (a + b) & 255 Va bien.

Rappelez-vous plus à l'école? Vous ajoutez des chiffres, chiffre par chiffre, et ajoutez une valeur de retenue à la colonne de chiffres suivante. Il n’ya aucun moyen pour une colonne de chiffres ultérieure (plus significative) d’influencer une colonne déjà traitée. Pour cette raison, cela ne fait aucune différence si vous mettez à zéro les chiffres uniquement dans le résultat, ou également en premier dans un argument.


Ce qui précède n'est pas toujours vrai, la norme C++ permet une implémentation qui résoudrait ce problème.

Une telle Deathstation 9000 :-) devrait utiliser un int à 33 bits, si l'OP signifiait unsigned short Avec "entiers non signés 32 bits". Si unsigned int Était utilisé, le DS9K devrait utiliser un int 32 bits et un unsigned int 32 bits avec un bit de remplissage. (Les entiers non signés doivent avoir la même taille que leurs équivalents signés, conformément au §3.9.1/3, et les bits de remplissage sont autorisés au §3.9.1/1.) D'autres combinaisons de tailles et de bits de remplissage fonctionnent également.

Autant que je sache, c’est le seul moyen de le casser, car:

  • La représentation entière doit utiliser un schéma de codage "purement binaire" (§3.9.1/7 et la note de bas de page), tous les bits sauf les bits de remplissage et le bit de signe doivent avoir une valeur de 2.n
  • la promotion int n'est autorisée que si int peut représenter toutes les valeurs du type source (§4.5/1), de sorte que int doit avoir au moins 32 bits contribuant à la valeur, plus un bit de signe .
  • le int ne peut pas avoir plus de bits de valeur (sans compter le bit de signe) que 32, car sinon une addition ne peut pas déborder.
17
alain

Vous avez déjà la réponse intelligente: l'arithmétique non signée est une arithmétique modulo et, par conséquent, les résultats sont valables, vous pouvez le prouver mathématiquement ...


Une chose intéressante à propos des ordinateurs, cependant, est que les ordinateurs sont rapides. En effet, ils sont si rapides qu’il est possible d’énumérer toutes les combinaisons valables de 32 bits dans un laps de temps raisonnable (n’essayez pas avec 64 bits).

Donc, dans votre cas, j'aime personnellement le lancer sur un ordinateur; il me faut moins de temps pour me convaincre que le programme est correct que pour me convaincre que la preuve mathématique est correcte et que je n'ai pas surveillée un détail dans le cahier des charges1:

#include <iostream>
#include <limits>

int main() {
    std::uint64_t const MAX = std::uint64_t(1) << 32;
    for (std::uint64_t i = 0; i < MAX; ++i) {
        for (std::uint64_t j = 0; j < MAX; ++j) {
            std::uint32_t const a = static_cast<std::uint32_t>(i);
            std::uint32_t const b = static_cast<std::uint32_t>(j);

            auto const champion = (a + (b & 255)) & 255;
            auto const challenger = (a + b) & 255;

            if (champion == challenger) { continue; }

            std::cout << "a: " << a << ", b: " << b << ", champion: " << champion << ", challenger: " << challenger << "\n";
            return 1;
        }
    }

    std::cout << "Equality holds\n";
    return 0;
}

Ceci énumère toutes les valeurs possibles de a et b dans l'espace 32 bits et vérifie si l'égalité est vérifiée ou non. Si ce n'est pas le cas, le cas qui n'a pas fonctionné est imprimé et peut être utilisé comme contrôle de cohérence.

Et, selon Clang : L'égalité est vraie .

De plus, étant donné que les règles arithmétiques sont indépendantes de la largeur en bits (au-dessus de int en largeur de bits), cette égalité est valable pour tout type d'entier non signé de 32 bits ou plus, y compris 64 bits et 128 bits.

Remarque: Comment un compilateur peut-il énumérer tous les modèles 64 bits dans un délai raisonnable? Ça ne peut pas. Les boucles ont été optimisées. Sinon, nous serions tous morts avant la fin de l'exécution.


Je l’ai initialement prouvé seulement pour des entiers non signés 16 bits; Malheureusement, C++ est un langage insensé dans lequel les petits entiers (dont la largeur de bits est inférieure à int) sont d'abord convertis en int.

#include <iostream>

int main() {
    unsigned const MAX = 65536;
    for (unsigned i = 0; i < MAX; ++i) {
        for (unsigned j = 0; j < MAX; ++j) {
            std::uint16_t const a = static_cast<std::uint16_t>(i);
            std::uint16_t const b = static_cast<std::uint16_t>(j);

            auto const champion = (a + (b & 255)) & 255;
            auto const challenger = (a + b) & 255;

            if (champion == challenger) { continue; }

            std::cout << "a: " << a << ", b: " << b << ", champion: "
                      << champion << ", challenger: " << challenger << "\n";
            return 1;
        }
    }

    std::cout << "Equality holds\n";
    return 0;
}

Et encore une fois, selon Clang : L'égalité est vraie .

Eh bien voilà


1  Bien sûr, si un programme déclenche par inadvertance un comportement non défini, il ne fera pas grand-chose.

14
Matthieu M.

La réponse rapide est: les deux expressions sont équivalentes

  • puisque a et b sont des entiers non signés 32 bits, le résultat est le même, même en cas de dépassement de capacité. L'arithmétique non signée garantit ce qui suit: un résultat qui ne peut pas être représenté par le type entier non signé résultant est réduit modulo le nombre qui est supérieur à la plus grande valeur pouvant être représentée par le type résultant.

La réponse longue est: il n’existait aucune plate-forme connue où ces expressions seraient différentes, mais la norme ne la garantit pas, en raison des règles de promotion intégrale.

  • Si le type de a et b (nombres entiers non signés sur 32 bits) a un rang supérieur à int, le calcul est exécuté sous la forme non signée, modulo 232et donne le même résultat défini pour les deux expressions pour toutes les valeurs de a et b.

  • Inversement, si le type de a et b est inférieur à int, ils sont tous deux promus en int et le calcul est effectué à l'aide d'une arithmétique signée, où débordement invoque un comportement indéfini.

    • Si int a au moins 33 bits de valeur, aucune des expressions ci-dessus ne peut déborder, le résultat est donc parfaitement défini et a la même valeur pour les deux expressions.

    • Si int a exactement 32 bits de valeur, le calcul peut déborder pour les deux expressions, par exemple valeurs a=0xFFFFFFFF et b=1 provoquerait un débordement dans les deux expressions. Pour éviter cela, vous devez écrire ((a & 255) + (b & 255)) & 255.

  • Les bonnes nouvelles sont qu'il n'y a pas de telles plates-formes1.


1 Plus précisément, il n’existe pas de telle plate-forme réelle, mais on pourrait configurer un DS9K afin qu’il présente un tel comportement tout en restant conforme à la norme C.

4
chqrlie

Identique en supposant qu'il n'y ait pas de dépassement de capacité. Aucune des deux versions n’est vraiment à l’abri des débordements, mais la version double et plus résistante. Je ne suis pas au courant d'un système où un débordement dans ce cas est un problème mais je peux voir l'auteur le faire s'il en existe un.

2
Loren Pechtel

Oui, vous pouvez le prouver avec de l'arithmétique, mais il existe une réponse plus intuitive.

Lors de l'ajout, chaque élément n'influence que ceux qui sont plus significatifs que lui-même; jamais ceux qui sont moins significatifs.

Par conséquent, quoi que vous fassiez avec les bits les plus hauts avant l'addition ne changera pas le résultat, tant que vous ne garderez que les bits moins significatifs que le bit le plus bas modifié.

2
Francesco Dondi

La preuve est triviale et laissée comme un exercice pour le lecteur

Mais pour légitimer cette réponse, votre première ligne de code indique de prendre les 8 derniers bits de b ** (tous les bits supérieurs de b définis sur zéro) et de les ajouter à a, puis ne prenez que les 8 derniers bits du résultat, en mettant tous les bits supérieurs à zéro.

La deuxième ligne dit add a et b et prend les 8 derniers bits avec tous les bits les plus élevés nuls.

Seuls les 8 derniers bits sont significatifs dans le résultat. Par conséquent, seuls les 8 derniers bits sont significatifs dans la ou les entrées.

** 8 derniers bits = 8 LSB

Il est également intéressant de noter que la sortie serait équivalente à

char a = something;
char b = something;
return (unsigned int)(a + b);

Comme ci-dessus, seuls les 8 LSB sont significatifs, mais le résultat est un unsigned int avec tous les autres bits à zéro. Le a + b débordera, produisant le résultat attendu.

0
user3728501