(A + B + C) ≠ (A + C + B) et réorganisation du compilateur


108

L'ajout de deux entiers 32 bits peut entraîner un débordement d'entier:

uint64_t u64_z = u32_x + u32_y;

Ce débordement peut être évité si l'un des entiers 32 bits est d'abord converti ou ajouté à un entier 64 bits.

uint64_t u64_z = u32_x + u64_a + u32_y;

Cependant, si le compilateur décide de réorganiser l'ajout:

uint64_t u64_z = u32_x + u32_y + u64_a;

le dépassement d'entier peut encore se produire.

Les compilateurs sont-ils autorisés à effectuer une telle réorganisation ou pouvons-nous leur faire confiance pour remarquer l'incohérence du résultat et conserver l'ordre des expressions tel quel?


15
Vous n'affichez pas réellement de dépassement d'entier parce que vous semblez être des uint32_tvaleurs ajoutées - qui ne débordent pas, elles s'enroulent. Ce ne sont pas des comportements différents.
Martin Bonner soutient Monica le

5
Voir la section 1.9 des standards c ++, elle répond directement à votre question (il y a même un exemple qui est presque exactement le même que le vôtre).
Holt

3
@Tal: Comme d'autres l'ont déjà dit: il n'y a pas de débordement d'entiers. Les non-signés sont définis pour envelopper, pour les signatures, il s'agit d'un comportement non défini, donc toute implémentation fera l'affaire, y compris les démons nasaux.
trop honnête pour ce site

5
@Tal: absurde! Comme je l'ai déjà écrit: la norme est très claire et nécessite un wrapping, pas une saturation (ce serait possible avec signé, car c'est UB comme standard.
trop honnête pour ce site

15
@rustyx: Que vous appelez l'enveloppant ou débordement, il reste que les ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0résultats dans 0, alors que les (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)résultats dans 0x100000000, et ces deux valeurs ne sont pas égales. Il est donc important que le compilateur puisse ou non appliquer cette transformation. Mais oui, la norme n'utilise que le mot «débordement» pour les entiers signés, pas non signés.
Steve Jessop

Réponses:


84

Si l'optimiseur effectue une telle réorganisation, il est toujours lié à la spécification C, donc une telle réorganisation deviendrait:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Raisonnement:

Nous commençons par

uint64_t u64_z = u32_x + u64_a + u32_y;

L'addition est effectuée de gauche à droite.

Les règles de promotion d'entiers indiquent que lors du premier ajout dans l'expression d'origine, u32_xêtre promu en uint64_t. Dans le deuxième ajout, u32_ysera également promu à uint64_t.

Ainsi, afin d'être conforme à la spécification C, tout Optimiseur doit promouvoir u32_xet u32_yà 64 bits des valeurs non signées. Cela équivaut à ajouter une distribution. (L'optimisation proprement dite ne se fait pas au niveau C, mais j'utilise la notation C car c'est une notation que nous comprenons.)


N'est-ce pas associatif de gauche, alors (u32_x + u32_t) + u64_a?
Inutile du

12
@Useless: Klas a tout casté en 64 bits. Maintenant, la commande ne fait aucune différence. Le compilateur n'a pas besoin de suivre l'associativité, il doit juste produire exactement le même résultat que s'il le faisait.
gnasher729

2
Cela semble suggérer que le code d'OP serait évalué comme ça, ce qui n'est pas vrai.
Inutile du

@Klas - voulez-vous expliquer pourquoi c'est le cas et comment vous arrivez exactement à votre échantillon de code?
rustyx

1
@rustyx Il fallait une explication. Merci de m'avoir poussé à en ajouter un.
Klas Lindbäck

28

Un compilateur n'est autorisé à réorganiser que sous la règle comme si . Autrement dit, si la réorganisation donnera toujours le même résultat que la commande spécifiée, alors elle est autorisée. Sinon (comme dans votre exemple), non.

Par exemple, étant donné l'expression suivante

i32big1 - i32big2 + i32small

qui a été soigneusement construit pour soustraire les deux valeurs qui sont connues pour être grandes mais similaires, et ensuite seulement ajouter l'autre petite valeur (évitant ainsi tout débordement), le compilateur peut choisir de réorganiser en:

(i32small - i32big2) + i32big1

et comptez sur le fait que la plate-forme cible utilise l'arithmétique à deux compléments avec enveloppement pour éviter les problèmes. (Une telle réorganisation peut être judicieuse si le compilateur est pressé pour les registres, et se trouve déjà i32smalldans un registre).


L'exemple d'OP utilise des types non signés. i32big1 - i32big2 + i32smallimplique des types signés. Des préoccupations supplémentaires entrent en jeu.
chux - Réintégrer Monica le

@chux Absolument. Le point que j'essayais de faire valoir est que même si je ne pouvais pas écrire (i32small-i32big2) + i32big1, (car cela pourrait provoquer UB), le compilateur peut le réorganiser efficacement parce que le compilateur peut être sûr que le comportement sera correct.
Martin Bonner soutient Monica le

3
@chux: Des problèmes supplémentaires comme UB n'entrent pas en jeu, car nous parlons d'un réordonnancement du compilateur sous la règle as-if. Un compilateur particulier peut profiter de la connaissance de son propre comportement de débordement.
MSalters

16

Il existe la règle du «comme si» en C, C ++ et Objective-C: le compilateur peut faire ce qu'il veut tant qu'aucun programme conforme ne peut faire la différence.

Dans ces langages, a + b + c est défini comme étant identique à (a + b) + c. Si vous pouvez faire la différence entre ceci et par exemple a + (b + c), alors le compilateur ne peut pas changer l'ordre. Si vous ne pouvez pas faire la différence, le compilateur est libre de modifier l'ordre, mais c'est très bien, car vous ne pouvez pas faire la différence.

Dans votre exemple, avec b = 64 bits, a et c 32 bits, le compilateur serait autorisé à évaluer (b + a) + c ou même (b + c) + a, car vous ne pouviez pas faire la différence, mais pas (a + c) + b parce que vous pouvez faire la différence.

En d'autres termes, le compilateur n'est pas autorisé à faire quoi que ce soit qui fasse que votre code se comporte différemment de ce qu'il devrait. Il n'est pas nécessaire de produire le code que vous pensez qu'il produirait, ou que vous pensez qu'il devrait produire, mais le code va vous donner exactement les résultats qu'elle devrait.


Mais avec une grande mise en garde; le compilateur est libre d'assumer aucun comportement indéfini (dans ce cas un débordement). Ceci est similaire à la façon dont une vérification de dépassement de if (a + 1 < a)capacité peut être optimisée.
csiz

7
@csiz ... sur les variables signées . Les variables non signées ont une sémantique de débordement bien définie (wrap-around).
Gavin S.Yancey

7

Citant les normes :

[Note: Les opérateurs peuvent être regroupés selon les règles mathématiques habituelles uniquement lorsque les opérateurs sont réellement associatifs ou commutatifs.7 Par exemple, dans le fragment suivant int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

l'instruction d'expression se comporte exactement de la même manière que

a = (((a + 32760) + b) + 5);

en raison de l'associativité et de la priorité de ces opérateurs. Ainsi, le résultat de la somme (a + 32760) est ensuite ajouté à b, et ce résultat est ensuite ajouté à 5, ce qui donne la valeur attribuée à a. Sur une machine dans laquelle les débordements produisent une exception et dans laquelle la plage de valeurs représentables par un int est [-32768, + 32767], l'implémentation ne peut pas réécrire cette expression comme

a = ((a + b) + 32765);

puisque si les valeurs pour a et b étaient respectivement -32754 et -15, la somme a + b produirait une exception alors que l'expression originale ne le ferait pas; l'expression ne peut pas non plus être réécrite comme

a = ((a + 32765) + b);

ou

a = (a + (b + 32765));

puisque les valeurs de a et b auraient pu être respectivement 4 et -8 ou -17 et 12. Cependant sur une machine dans laquelle les débordements ne produisent pas d'exception et dans laquelle les résultats des débordements sont réversibles, l'instruction d'expression ci-dessus peut être réécrit par l'implémentation de l'une des manières ci-dessus car le même résultat se produira. - note de fin]


4

Les compilateurs sont-ils autorisés à effectuer une telle réorganisation ou pouvons-nous leur faire confiance pour remarquer l'incohérence du résultat et conserver l'ordre des expressions tel quel?

Le compilateur ne peut réorganiser que s'il donne le même résultat - ici, comme vous l'avez observé, ce n'est pas le cas.


Il est possible d'écrire un modèle de fonction, si vous en voulez un, qui promeut tous les arguments std::common_typeavant l'ajout - ce serait sûr, et ne dépendrait ni de l'ordre des arguments ni de la conversion manuelle, mais c'est assez maladroit.


Je sais que le casting explicite doit être utilisé, mais je souhaite connaître le comportement des compilateurs lorsque ce casting a été omis par erreur.
Tal

1
Comme je l'ai dit, sans casting explicite: l'ajout de gauche est effectué en premier, sans promotion intégrale, et donc soumis à un wrapping. Le résultat de cet ajout, éventuellement encapsulé, est ensuite promu en uint64_tpour l'ajout à la valeur la plus à droite.
Inutile

Votre explication sur la règle du «comme si» est totalement fausse. Le langage C par exemple spécifie quelles opérations doivent se produire sur une machine abstraite. La règle du «comme si» lui permet de faire absolument tout ce qu'il veut tant que personne ne peut faire la différence.
gnasher729

Ce qui signifie que le compilateur peut faire ce qu'il veut tant que le résultat est le même que celui déterminé par les règles d'associativité de gauche et de conversion arithmétique indiquées.
Inutile

1

Cela dépend de la largeur de bits de unsigned/int.

Les 2 ci-dessous ne sont pas les mêmes (lorsque les unsigned <= 32bits). u32_x + u32_ydevient 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Ce sont les mêmes (quand unsigned >= 34bits). Les promotions de nombres entiers ont provoqué l' u32_x + u32_yajout en maths 64 bits. L'ordre n'a pas d'importance.

C'est UB (quand unsigned == 33bits). Les promotions entières ont provoqué l'ajout au niveau des maths 33 bits signés et le débordement signé est UB.

Les compilateurs sont-ils autorisés à effectuer une telle réorganisation ...?

(Math 32 bits): Réorganiser oui, mais les mêmes résultats doivent se produire, donc pas ce que OP propose de réordonner. Ci-dessous sont les mêmes

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... pouvons-nous leur faire confiance pour remarquer l'incohérence du résultat et conserver l'ordre des expressions tel quel?

Faites confiance, mais l'objectif de codage d'OP n'est pas clair. Le u32_x + u32_ycarry devrait-il contribuer? Si OP veut cette contribution, le code doit être

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Mais non

uint64_t u64_z = u32_x + u32_y + u64_a;
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.