69 votes

(.1f+.2f==.3f) != (.1f+.2f).Equals(.3f) Pourquoi ?

Ma question est la suivante no sur la précision flottante. Il s'agit de savoir pourquoi Equals() est différent de == .

Je comprends pourquoi .1f + .2f == .3f est false (alors que .1m + .2m == .3m est true ).
Je comprends. == est la référence et .Equals() est une comparaison de valeurs. ( Modifier : Je sais qu'il y a plus que ça).

Mais pourquoi (.1f + .2f).Equals(.3f) true alors que (.1d+.2d).Equals(.3d) est toujours false ?

 .1f + .2f == .3f;              // false
(.1f + .2f).Equals(.3f);        // true
(.1d + .2d).Equals(.3d);        // false

0 votes

Cette question fournit plus de détails sur les différences entre les types à virgule flottante et décimale.

0 votes

Pour mémoire, pas de véritable réponse : Math.Abs(.1d + .2d - .3d) < double.Epsilon Cela devrait être la meilleure méthode d'égalité.

10 votes

FYI == est no comparaison "de référence", et .Equals() est no comparaison "valeur". Leur mise en œuvre est spécifique au type.

135voto

Eric Lippert Points 300275

La question est formulée de manière confuse. Décomposons-la en plusieurs petites questions :

Pourquoi un dixième plus deux dixièmes ne sont pas toujours égaux à trois dixièmes dans l'arithmétique à virgule flottante ?

Laissez-moi vous donner une analogie. Supposons que nous ayons un système mathématique où tous les nombres sont arrondis à exactement cinq décimales. Supposons que vous disiez :

x = 1.00000 / 3.00000;

Vous vous attendez à ce que x soit égal à 0,33333, non ? Parce que c'est le le plus proche dans notre système au réel répondre. Maintenant, supposons que vous disiez

y = 2.00000 / 3.00000;

Tu t'attendrais à ce que y soit 0,66667, non ? Parce qu'encore une fois, c'est le le plus proche dans notre système au réel réponse. 0,66666 est plus loin des deux tiers que 0,66667.

Remarquez que dans le premier cas, nous avons arrondi vers le bas et dans le second cas, nous avons arrondi vers le haut.

Maintenant, quand nous disons

q = x + x + x + x;
r = y + x + x;
s = y + y;

qu'obtenons-nous ? Si nous faisions de l'arithmétique exacte, chacun d'entre eux serait évidemment égal à quatre tiers et ils seraient tous égaux. Mais ils ne sont pas égaux. Même si 1,33333 est le nombre le plus proche des quatre tiers dans notre système, seul r a cette valeur.

q est 1.33332 -- parce que x était un peu petit, chaque addition a accumulé cette erreur et le résultat final est un peu trop petit. De même, s est trop grand ; il vaut 1,33334, parce que y était un peu trop grand. r obtient la bonne réponse parce que la trop grande taille de y est annulée par la trop petite taille de x et le résultat est correct.

Le nombre de positions de précision a-t-il un effet sur la magnitude et la direction de l'erreur ?

Oui ; une plus grande précision rend l'ampleur de l'erreur plus faible, mais peut changer le fait qu'un calcul génère une perte ou un gain en raison de l'erreur. Par exemple :

b = 4.00000 / 7.00000;

b serait de 0,57143, qui s'arrondit à la valeur réelle de 0,571428571... Si nous étions allés jusqu'à huit chiffres, nous aurions obtenu 0,57142857, ce qui représente une erreur beaucoup plus petite, mais dans la direction opposée ; elle a été arrondie vers le bas.

Comme la modification de la précision peut changer le fait qu'une erreur soit un gain ou une perte dans chaque calcul individuel, cela peut changer le fait que les erreurs d'un calcul agrégé donné se renforcent ou s'annulent. Le résultat net est que, parfois, un calcul de précision inférieure est plus proche du "vrai" résultat qu'un calcul de précision supérieure parce que dans le calcul de précision inférieure vous avez de la chance et les erreurs sont dans des directions différentes.

On pourrait s'attendre à ce qu'un calcul effectué avec une plus grande précision donne toujours une réponse plus proche de la vraie réponse, mais cet argument montre le contraire. Cela explique pourquoi, parfois, un calcul en flottant donne la "bonne" réponse, mais qu'un calcul en double -- qui a une précision deux fois plus grande -- donne la "mauvaise" réponse, correct ?

Oui, c'est exactement ce qui se passe dans vos exemples, sauf qu'au lieu d'avoir cinq chiffres de précision décimale, nous avons un certain nombre de chiffres de binaire précision. De même qu'un tiers ne peut être représenté avec précision par cinq chiffres décimaux (ou tout autre nombre fini), 0,1, 0,2 et 0,3 ne peuvent être représentés avec précision par un nombre fini de chiffres binaires. Certains d'entre eux seront arrondis vers le haut, d'autres vers le bas, et que l'addition de ces chiffres soit ou non augmentation de l'erreur ou annuler l'erreur dépend des détails spécifiques de combien de chiffres binaires sont dans chaque système. C'est-à-dire que les changements dans précision peut modifier le réponse pour le meilleur et pour le pire. En général, plus la précision est élevée, plus la réponse est proche de la vraie réponse, mais pas toujours.

Comment puis-je alors obtenir des calculs arithmétiques décimaux précis, si float et double utilisent des chiffres binaires ?

Si vous avez besoin d'un calcul décimal précis, utilisez la fonction decimal Il utilise les fractions décimales, et non les fractions binaires. Le prix à payer est qu'il est considérablement plus grand et plus lent. Et bien sûr, comme nous l'avons déjà vu, des fractions comme un tiers ou quatre septièmes ne seront pas représentées avec précision. Cependant, toute fraction qui est en fait une fraction décimale sera représentée sans erreur, jusqu'à environ 29 chiffres significatifs.

OK, j'accepte que tous les systèmes à virgule flottante introduisent des inexactitudes dues à des erreurs de représentation, et que ces inexactitudes peuvent parfois s'accumuler ou s'annuler en fonction du nombre de bits de précision utilisés dans le calcul. Avons-nous au moins la garantie que ces inexactitudes seront cohérent ?

Non, vous n'avez pas cette garantie pour les flottants ou les doubles. Le compilateur et le runtime sont tous deux autorisés à effectuer des calculs en virgule flottante en plus haut précision que ce qui est requis par la spécification. En particulier, le compilateur et le runtime sont autorisés à faire de l'arithmétique en simple précision (32 bits). en 64 bits, 80 bits, 128 bits ou tout autre bit supérieur à 32 qu'ils souhaitent. .

Le compilateur et le runtime sont autorisés à le faire. comme ils l'entendent à ce moment-là . Il n'est pas nécessaire qu'elles soient cohérentes d'une machine à l'autre, d'une série à l'autre, etc. Puisque cela ne peut faire que des calculs plus précis ceci n'est pas considéré comme un bug. C'est une fonctionnalité. Une fonctionnalité qui rend incroyablement difficile l'écriture de programmes au comportement prévisible, mais une fonctionnalité néanmoins.

Cela signifie donc que les calculs effectués au moment de la compilation, comme les littéraux 0,1 + 0,2, peuvent donner des résultats différents de ceux du même calcul effectué au moment de l'exécution avec des variables ?

Ouaip.

Et si on comparait les résultats de 0.1 + 0.2 == 0.3 a (0.1 + 0.2).Equals(0.3) ?

Puisque le premier est calculé par le compilateur et le second par le runtime, et que je viens de dire qu'ils sont autorisés à utiliser arbitrairement plus de précision que celle requise par la spécification, oui, ils peuvent donner des résultats différents. Peut-être que l'un d'entre eux choisit de faire le calcul uniquement en précision 64 bits alors que l'autre choisit une précision de 80 ou 128 bits pour une partie ou la totalité du calcul et obtient une réponse différente.

Donc, attendez une minute ici. Vous êtes en train de dire que non seulement 0.1 + 0.2 == 0.3 peut être différent de (0.1 + 0.2).Equals(0.3) . Vous êtes en train de dire que 0.1 + 0.2 == 0.3 peut être calculé pour être vrai ou faux entièrement à la fantaisie du compilateur. Elle peut produire du vrai le mardi et du faux le jeudi, elle peut produire du vrai sur une machine et du faux sur une autre, elle peut produire du vrai et du faux si l'expression apparaît deux fois dans le même programme. Cette expression peut avoir l'une ou l'autre valeur pour n'importe quelle raison ; le compilateur est autorisé à être complètement peu fiable ici.

Correct.

La façon dont ce problème est généralement signalé à l'équipe chargée du compilateur C# est que quelqu'un a une expression qui produit vrai lorsqu'il compile en mode débogage et faux lorsqu'il compile en mode release. C'est la situation la plus courante, car la génération du code de débogage et de libération modifie les schémas d'allocation des registres. Mais le compilateur est autorisé de faire ce qu'il veut avec cette expression, tant qu'elle choisit vrai ou faux. (Il ne peut pas, par exemple, produire une erreur de compilation).

C'est de la folie.

Correct.

Qui dois-je blâmer pour ce gâchis ?

Pas moi, ça c'est sûr.

Intel a décidé de fabriquer une puce mathématique à virgule flottante dans laquelle il était beaucoup, beaucoup plus coûteux d'obtenir des résultats cohérents. De petits choix dans le compilateur concernant les opérations à enregistrer par rapport aux opérations à garder sur la pile peuvent entraîner de grandes différences dans les résultats.

Comment puis-je garantir des résultats constants ?

Utilisez le decimal type, comme je l'ai déjà dit. Ou faites tous vos calculs en nombres entiers.

Je dois utiliser des doubles ou des flottants ; puis-je le faire ? tout ce qui est pour encourager des résultats cohérents ?

Oui. Si vous enregistrez un résultat dans un champ statique tout champ d'instance d'une classe o élément du tableau de type float ou double, il est garanti d'être tronqué en précision 32 ou 64 bits. (Cette garantie est expressément no faites pour les magasins aux paramètres locaux ou formels). De plus, si vous faites un temps de fonctionnement pour (float) o (double) sur une expression qui est déjà de ce type, le compilateur émet un code spécial qui force le résultat à être tronqué comme s'il avait été assigné à un champ ou à un élément de tableau. (Les casts qui s'exécutent au moment de la compilation -- c'est-à-dire les casts sur des expressions constantes -- ne sont pas garantis).

Pour clarifier ce dernier point : est-ce que le C# spécification du langage faire ces garanties ?

Non. Le temps de fonctionnement garantit que les enregistrements dans un tableau ou un champ sont tronqués. La spécification C# ne garantit pas qu'un casting d'identité soit tronqué, mais l'implémentation Microsoft comporte des tests de régression qui garantissent que chaque nouvelle version du compilateur présente ce comportement.

Tout ce que la spécification du langage dit à ce sujet, c'est que les opérations en virgule flottante peuvent être effectuées avec une précision supérieure, à la discrétion de l'implémentation.

1 votes

Le problème se produit lorsque nous assignons bool result= 0.1f+0.2f==0.3f. Si nous ne stockons pas 0.1f+0.2f dans une variable, nous obtenons false. Si nous stockons 0,1f+0,2f dans une variable, nous obtenons true. Cela n'a pas grand-chose à voir avec l'arithmétique générale en virgule flottante, le cas échéant. La question principale est de savoir pourquoi le bool x=0,1f+0,2f==0,3f est faux, mais le float temp=0,1f+0,2f ; bool x=temp==0,3f est vrai, le reste est une question habituelle en virgule flottante.

0 votes

@ValentinKuzub : Il n'y a pas de variable temp dans == car le résultat est déterminé au moment de la compilation.

15 votes

Lorsque Eric Lippert répondu à la même question avec moi, je me sens toujours damn! my answer doesn't look logical anymore..

8voto

Soner Gönül Points 35739

Lorsque vous écrivez

double a = 0.1d;
double b = 0.2d;
double c = 0.3d;

En fait, ces derniers ne sont pas exactement 0.1 , 0.2 y 0.3 . Du code IL ;

  IL_0001:  ldc.r8     0.10000000000000001
  IL_000a:  stloc.0
  IL_000b:  ldc.r8     0.20000000000000001
  IL_0014:  stloc.1
  IL_0015:  ldc.r8     0.29999999999999999

Il y a beaucoup de questions dans SO qui pointent ce problème comme ( Différence entre décimal, flottant et double dans .NET ? y Traitement des erreurs en virgule flottante dans .NET ) mais je vous suggère de lire un article cool intitulé ;

What Every Computer Scientist Should Know About Floating-Point Arithmetic

Bien quel leppie a déclaré est plus logique. La situation réelle est ici, tout dépend sur compiler / computer o cpu .

Basé sur le code de leppie, ce code fonctionne sur mon Visual Studio 2010 y Linqpad En conséquence True / False mais quand je l'ai essayé sur ideone.com le résultat sera True / True

Vérifiez le DEMO .

Conseil : Quand j'ai écrit Console.WriteLine(.1f + .2f == .3f); Resharper m'avertit ;

Comparaison de nombres à virgule flottante avec l'opérateur d'égalité. Possible perte de précision lors de l'arrondi des valeurs.

enter image description here

0 votes

Il s'interroge sur le cas de précision unique. Il n'y a pas de problème avec le cas de double précision.

1 votes

Apparemment, il y a une différence entre le code qui sera exécuté et le compilateur aussi. 0.1f+0.2f==0.3f sera compilé à false en mode debug et release. Par conséquent, il sera faux pour l'opérateur d'égalité.

6voto

leppie Points 67289

Comme indiqué dans les commentaires, cela est dû au fait que le compilateur effectue la propagation des constantes et effectue le calcul avec une plus grande précision (je crois que cela dépend du CPU).

  var f1 = .1f + .2f;
  var f2 = .3f;
  Console.WriteLine(f1 == f2); // prints true (same as Equals)
  Console.WriteLine(.1f+.2f==.3f); // prints false (acts the same as double)

@Caramiriel fait également remarquer que .1f+.2f==.3f est émise en tant que false dans l'IL, donc le compilateur a fait le calcul au moment de la compilation.

Pour confirmer l'optimisation du compilateur par pliage/propagation des constantes

  const float f1 = .1f + .2f;
  const float f2 = .3f;
  Console.WriteLine(f1 == f2); // prints false

0 votes

Mais pourquoi ne fait-il pas la même optimisation dans le dernier cas ?

2 votes

@SonerGönül : Bientôt éclipsé par son altesse ;p Merci.

0 votes

Ok, laissez-moi le dire plus clairement, car je faisais référence au dernier cas du PO : Mais pourquoi n'effectue-t-il pas la même optimisation dans le cadre de la Equals cas ?

2voto

Valentin Kuzub Points 4349

Pour information, le test suivant est réussi

float x = 0.1f + 0.2f;
float result = 0.3f;
bool isTrue = x.Equals(result);
bool isTrue2 = x == result;
Assert.IsTrue(isTrue);
Assert.IsTrue(isTrue2);

Le problème se situe donc au niveau de cette ligne

0,1f + 0,2f==0,3f

Ce qui, comme indiqué, est probablement spécifique au compilateur et au PC.

La plupart des gens abordent cette question sous un mauvais angle, je pense.

UPDATE :

Un autre test curieux je pense

const float f1 = .1f + .2f;
const float f2 = .3f;
Assert.AreEqual(f1, f2); passes
Assert.IsTrue(f1==f2); doesnt pass

Mise en œuvre d'une égalité unique :

public bool Equals(float obj)
{
    return ((obj == this) || (IsNaN(obj) && IsNaN(this)));
}

0 votes

Je suis d'accord avec votre dernière déclaration :)

0 votes

@leppie a mis à jour ma réponse avec un nouveau test. Pouvez-vous me dire pourquoi le premier test passe et le second non ? Je ne comprends pas bien, étant donné l'implémentation de Equals

0voto

njzk2 Points 17085

== consiste à comparer des valeurs flottantes exactes.

Equals est une méthode booléenne qui peut renvoyer vrai ou faux. L'implémentation spécifique peut varier.

0 votes

Vérifier ma réponse pour la mise en œuvre de float Equals. La différence réelle est que equals est exécuté au moment de l'exécution, alors que == peut être exécuté au moment de la compilation, == est également une "méthode booléenne" (j'ai entendu parler de fonctions booléennes), pratiquement

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X