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.
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.0 votes
Je crois que la raison en est la propagation constante avec le
==
en utilisant une plus grande précision pour calculer le résultat. Le résultat est donc le même que celui obtenu en utilisant la double précision.0 votes
L'idée clé ici est que
float + float == float
odouble + double == double
podría être fausse lorsqu'elle apparaît pour être vrai.0 votes
@AustinSalonen : uniquement lorsqu'il s'agit de constantes.
15 votes
Juste pour clarifier : la différence est que dans le premier cas
0.1 + 0.2 == 0.3
qui est un expression constante qui peut être entièrement calculée au moment de la compilation. Dans(0.1 + 0.2).Equals(0.3)
le site0.1 + 0.2
y el0.3
sont toutes des expressions constantes mais l'égalité est calculée par le runtime, pas par le compilateur. Est-ce clair ?8 votes
De plus, pour être pointilleux, les différences qui font que le calcul doit être effectué en plus haute précision ne doivent pas être "environnementales" ; le compilateur et le moteur d'exécution sont tous deux autorisés à utiliser une plus haute précision. pour quelque raison que ce soit indépendamment de tout détail environnemental. En pratique, la décision d'utiliser une précision supérieure ou inférieure dépend généralement de la disponibilité des registres ; les expressions qui sont enregistrées sont plus précises.