Si je copie un flottant dans une autre variable, sera-t-il égal?

167

Je sais que l'utilisation ==pour vérifier l'égalité des variables à virgule flottante n'est pas un bon moyen. Mais je veux juste savoir cela avec les déclarations suivantes:

float x = ...

float y = x;

assert(y == x)

Puisque yest copié de x, l'affirmation sera-t-elle vraie?

Wei Li
la source
78
Permettez-moi de fournir une prime de 50 à quelqu'un qui prouve réellement l'inégalité par une démonstration avec du vrai code. Je veux voir la chose 80 vs 64 bits en action. Plus 50 autres pour une explication du code assembleur généré qui montre qu'une variable est dans un registre et l'autre non (ou quelle que soit la raison de l'inégalité, je voudrais qu'elle soit expliquée à un bas niveau).
Thomas Weller
1
@ThomasWeller le bogue GCC à ce sujet: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; cependant, je viens d'essayer de le reproduire sur un système x86-64 et ce n'est pas le cas, même avec -ffast-math. Je soupçonne que vous avez besoin d'un ancien GCC sur un système 32 bits.
pjc50
5
@ pjc50: En fait, vous avez besoin d'un système 80 bits pour reproduire le bogue 323; c'est le FPU 80x87 qui a causé le problème. x86-64 utilise le FPU SSE. Les bits supplémentaires causent le problème, car ils sont arrondis lors du déversement d'une valeur sur un flottant de 32 bits.
MSalters
4
Si la théorie de MSalters est correcte (et je soupçonne que c'est le cas), vous pouvez reprocher soit en compilant pour 32 bits ( -m32), soit en demandant à GCC d'utiliser le x87 FPU ( -mfpmath=387).
Cody Gray
4
Remplacez "48 bits" par "80 bits", puis vous pouvez supprimer l'adjectif "mythique", @Hot. C'est précisément ce dont on parlait juste avant votre commentaire. Le x87 (FPU pour l'architecture x86) utilise des registres 80 bits, un format de "précision étendue".
Cody Gray

Réponses:

125

Outre le assert(NaN==NaN);cas souligné par kmdreko, vous pouvez avoir des situations avec x87-math, lorsque des flottants 80 bits sont temporairement stockés en mémoire et plus tard comparés à des valeurs qui sont toujours stockées dans un registre.

Exemple minimal possible, qui échoue avec gcc9.2 lorsqu'il est compilé avec -O2 -m32:

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

Démo Godbolt: https://godbolt.org/z/X-Xt4R

Le volatilepeut probablement être omis, si vous parvenez à créer une pression de registre suffisante pour avoir ystocké et rechargé à partir de la mémoire (mais confondez suffisamment le compilateur, pour ne pas omettre la comparaison).

Voir la référence de la FAQ GCC:

chtz
la source
2
Il semble étrange que les bits supplémentaires soient pris en compte pour comparer une floatprécision standard à une précision supplémentaire.
Nat
13
@Nat Il est étrange; c'est un bug .
Courses de légèreté en orbite le
13
@ThomasWeller Non, c'est une récompense raisonnable. Bien que j'aimerais que la réponse indique qu'il s'agit d'un comportement non conforme
Courses de légèreté en orbite
4
Je peux étendre cette réponse, en soulignant ce qui se passe exactement dans le code d'assemblage, et que cela viole en fait la norme - bien que je ne m'appellerais pas avocat de la langue, donc je ne peux pas garantir qu'il n'y ait pas d'obscur clause qui autorise explicitement ce comportement. Je suppose que l'OP était plus intéressé par les complications pratiques sur les compilateurs réels, pas sur les compilateurs totalement sans bug et entièrement conformes (qui de facto n'existent pas, je suppose).
chtz
4
Il convient de mentionner que cela -ffloat-storesemble être le moyen d'empêcher cela.
OrangeDog
116

Ce ne sera pas vrai si xc'est le cas NaN, car les comparaisons sur NaNsont toujours fausses (oui, même NaN == NaN). Pour tous les autres cas (valeurs normales, valeurs sous-normales, infinis, zéros), cette affirmation sera vraie.

Les conseils pour éviter les ==flottants s'appliquent aux calculs car les nombres à virgule flottante sont incapables d'exprimer de nombreux résultats exactement lorsqu'ils sont utilisés dans des expressions arithmétiques. L'affectation n'est pas un calcul et il n'y a aucune raison pour que l'affectation produise une valeur différente de l'original.


L'évaluation à précision étendue ne devrait pas poser de problème si la norme est respectée. De <cfloat>hérité de C [5.2.4.2.2.8] ( soulignement le mien ):

À l'exception de l'affectation et de la conversion (qui supprime toute plage et précision supplémentaires) , les valeurs des opérations avec des opérandes flottants et les valeurs soumises aux conversions arithmétiques habituelles et des constantes flottantes sont évaluées dans un format dont la plage et la précision peuvent être supérieures à celles requises par le type.

Cependant, comme l'ont souligné les commentaires, certains cas avec certains compilateurs, options de build et cibles pourraient rendre cela paradoxalement faux.

kmdreko
la source
10
Que faire si xest calculé dans un registre de la première ligne, en gardant plus de précision que le minimum pour a float. Le y = xpeut être en mémoire, ne gardant que la floatprécision. Ensuite, le test d'égalité se ferait avec la mémoire par rapport au registre, avec des précisions différentes, et donc sans garantie.
David Schwartz
5
x+pow(b,2)==x+pow(a,3)pourrait différer de l' auto one=x+pow(b,2); auto two=y+pow(a,3); one==twoun car on pourrait comparer en utilisant plus de précision que l'autre (si un / deux sont des valeurs de 64 bits en RAM, alors que les valeurs intermédistes sont de 80ish bits sur fpu). La mission peut donc parfois faire quelque chose.
Yakk - Adam Nevraumont
22
@evg Bien sûr! Ma réponse suit simplement la norme. Tous les paris sont désactivés si vous dites à votre compilateur de ne pas se confesser, en particulier lors de l'activation des mathématiques rapides.
kmdreko
11
@Voo Voir la citation dans ma réponse. La valeur du RHS est affectée à la variable du LHS. Il n'y a aucune justification légale pour que la valeur résultante du LHS diffère de la valeur du RHS. J'apprécie que plusieurs compilateurs aient des bogues à cet égard. Mais si quelque chose est stocké dans un registre est censé n'y avoir rien à voir.
Courses de légèreté en orbite le
6
@Voo: Dans ISO C ++, l'arrondi à la largeur de type est censé se produire sur n'importe quelle affectation. Dans la plupart des compilateurs qui ciblent x87, cela ne se produit vraiment que lorsque le compilateur décide de se renverser / recharger. Vous pouvez le forcer avec gcc -ffloat-storepour une stricte conformité. Mais cette question concerne x=y; x==y; sans rien faire entre les deux. Si yest déjà arrondi pour tenir dans un flotteur, la conversion en double ou en double long et retour ne changera pas la valeur. ...
Peter Cordes
34

Oui, yprendra assurément la valeur de x:

[expr.ass]/2: En affectation simple (=), l'objet référencé par l'opérande gauche est modifié ([defns.access]) en remplaçant sa valeur par le résultat de l'opérande droit.

Il n'y a pas de latitude pour attribuer d'autres valeurs.

(D'autres ont déjà souligné qu'une comparaison d'équivalence ==sera néanmoins évaluée falsepour les valeurs de NaN.)

Le problème habituel avec la virgule flottante ==est qu'il est facile de ne pas avoir la valeur que vous pensez avoir. Ici, nous savons que les deux valeurs, quelles qu'elles soient, sont identiques.

Courses de légèreté en orbite
la source
7
@ThomasWeller C'est un bogue connu dans une implémentation par conséquent non conforme. Bon à mentionner cependant!
Courses de légèreté en orbite le
Au début, je pensais que le langage imposant la distinction entre «valeur» et «résultat» serait pervers, mais cette distinction n'est pas nécessairement sans différence par le langage de C2.2, 7.1.6; C3.3, 7.1.6; C4.2, 7.1.6 ou C5.3, 7.1.6 du projet de norme que vous citez.
Eric Towers
@EricTowers Désolé, pouvez-vous clarifier ces références? Je ne trouve pas ce que vous pointez
Courses de légèreté en orbite le
@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 et C5.3 .
Eric Towers le
@EricTowers Oui, toujours pas vous suivre. Votre premier lien va à l'index de l'annexe C (ne me dit rien). Vos quatre prochains liens vont tous vers [expr]. Si je dois ignorer les liens et me concentrer sur les citations, je me retrouve avec la confusion que, par exemple, C.5.3 ne semble pas aborder l'utilisation du terme "valeur" ou du terme "résultat" (bien qu'il le fasse utiliser "result" une fois dans son contexte anglais normal). Peut-être pourriez-vous décrire plus clairement où vous pensez que la norme fait une distinction et fournir une citation claire unique à ce qui se passe. Merci!
Courses de légèreté en orbite le
3

Oui, dans tous les cas (sans tenir compte des problèmes NaN et x87), cela sera vrai.

Si vous faites un memcmpsur eux, vous pourrez tester l'égalité tout en étant capable de comparer les NaN et les sNaN. Cela nécessitera également que le compilateur prenne l'adresse de la variable qui contraindra la valeur en 32 bits floatau lieu de 80 bits. Cela éliminera les problèmes de x87. La deuxième affirmation ici est destinée à ne pas montrer que ==les NaN ne seront pas comparés comme vrais:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

Notez que si les NaN ont une représentation interne différente (c'est-à-dire une mantisse différente), la valeur memcmpne sera pas vraie.

SS Anne
la source
1

Dans les cas habituels, il serait évalué comme vrai. (ou l'instruction assert ne fera rien)

Modifier :

Par «cas habituels», j'entends exclure les scénarios susmentionnés (tels que les valeurs NaN et les unités à virgule flottante 80x87), comme indiqué par d'autres utilisateurs.

Étant donné l'obsolescence des puces 8087 dans le contexte actuel, le problème est plutôt isolé et pour que la question soit applicable dans l'état actuel de l'architecture à virgule flottante utilisée, elle est vraie dans tous les cas, sauf pour les NaN.

(référence environ 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Félicitations à @chtz pour avoir reproduit un bon exemple et @kmdreko pour avoir mentionné les NaN - ne les connaissaient pas avant!

Anirban166
la source
1
Je pensais qu'il était tout à fait possible xd'être dans un registre à virgule flottante pendant le ychargement de la mémoire. La mémoire peut avoir moins de précision qu'un registre, ce qui entraîne l'échec de la comparaison.
David Schwartz
1
Cela pourrait être un cas pour un faux, je n'ai pas pensé jusque-là. (puisque le PO n'a fourni aucun cas particulier, je suppose qu'il n'y a pas de contraintes supplémentaires)
Anirban166
1
Je ne comprends pas vraiment ce que tu dis. Si je comprends bien la question, l'OP demande si la copie d'un flotteur puis le test d'égalité sont garantis pour réussir. Votre réponse semble dire "oui". Je demande pourquoi la réponse n'est pas non.
David Schwartz
6
La modification rend cette réponse incorrecte. La norme C ++ requiert que l'affectation convertisse la valeur en type de destination - une précision excessive peut être utilisée dans les évaluations d'expression mais ne peut pas être conservée par l'affectation. Peu importe que la valeur soit conservée dans un registre ou une mémoire; la norme C ++ exige que ce soit, comme le code est écrit, une floatvaleur sans précision supplémentaire.
Eric Postpischil
2
@AProgrammer Étant donné qu'un compilateur (extrêmement) buggé pourrait théoriquement provoquer int a=1; int b=a; assert( a==b );une assertion, je pense qu'il est logique de répondre à cette question en relation avec un compilateur fonctionnant correctement (tout en notant peut-être que certaines versions de certains compilateurs ont / ont -être connu-pour se tromper). En termes pratiques, si pour une raison quelconque, un compilateur ne supprime pas la précision supplémentaire du résultat d'une affectation stockée dans un registre, il doit le faire avant d' utiliser cette valeur.
TripeHound
-1

Oui, il retournera toujours True , sauf si c'est NaN . Si la valeur de la variable est NaN, elle renvoie toujours False !

Valentin Popescu
la source