Pourquoi l'ajout de 0,1 plusieurs fois reste-t-il sans perte?

152

Je sais que le 0.1nombre décimal ne peut pas être représenté exactement avec un nombre binaire fini ( explication ), donc double n = 0.1perdra une certaine précision et ne sera pas exactement 0.1. D'autre part 0.5peut être représenté exactement parce que c'est le cas 0.5 = 1/2 = 0.1b.

Cela dit, il est compréhensible que l'ajout de 0.1 trois fois ne donne pas exactement de 0.3sorte que le code suivant s'imprime false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Mais alors comment se fait-il que l'ajout de 0.1 cinq fois donne exactement 0.5? Le code suivant s'imprime true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Si 0.1on ne peut pas être représenté exactement, comment se fait-il que l'ajouter 5 fois donne exactement 0.5ce qui peut être représenté précisément?

icza
la source
7
Si vous faites vraiment des recherches, je suis sûr que vous pouvez le comprendre, mais la virgule flottante est chargée de "surprises", et parfois il vaut mieux regarder avec émerveillement.
Hot Licks
3
Vous pensez à cela d'une manière mathématique. L'aritmétique à virgule flottante n'est en aucun cas mathématique.
Jakob
13
@HotLicks c'est vraiment la mauvaise attitude à avoir.
hobbs
2
@RussellBorogove même s'il était optimisé à distance, ce ne serait une optimisation valide que s'il sumavait la même valeur finale que si la boucle était vraiment exécutée. Dans la norme C ++, cela s'appelle la "règle as-si" ou "même comportement observable".
hobbs
7
@Jakob n'est pas du tout vrai. L'arithmétique à virgule flottante est rigoureusement définie, avec un bon traitement mathématique des bornes d'erreur et autres. C'est juste que de nombreux programmeurs ne sont pas disposés à donner suite à l'analyse, ou qu'ils croient à tort que «la virgule flottante est inexacte» est tout ce qu'il y a à savoir et que l'analyse ne vaut pas la peine de s'embêter.
hobbs

Réponses:

155

L'erreur d'arrondi n'est pas aléatoire et la façon dont elle est mise en œuvre tente de minimiser l'erreur. Cela signifie que parfois l'erreur n'est pas visible ou qu'il n'y a pas d'erreur.

Par exemple 0.1n'est pas exactement 0.1ie new BigDecimal("0.1") < new BigDecimal(0.1)mais 0.5est exactement1.0/2

Ce programme vous montre les vraies valeurs impliquées.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

impressions

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Remarque: c'est 0.3légèrement désactivé, mais lorsque vous arrivez aux 0.4bits, vous devez en décaler un pour s'adapter à la limite de 53 bits et l'erreur est écartée. Encore une fois, une chair de poule d'erreur se reconnecter pour 0.6et 0.7mais 0.8à 1.0l'erreur est mis au rebut.

L'ajouter 5 fois devrait cumuler l'erreur, pas l'annuler.

La raison d'une erreur est due à une précision limitée. c'est-à-dire 53 bits. Cela signifie que comme le nombre utilise plus de bits à mesure qu'il augmente, les bits doivent être supprimés à la fin. Cela provoque un arrondi qui dans ce cas est en votre faveur.
Vous pouvez obtenir l'effet inverse en obtenant un nombre plus petit, par exemple 0.1-0.0999=> 1.0000000000000286E-4 et vous voyez plus d'erreur qu'auparavant.

Un exemple de ceci est pourquoi dans Java 6 Pourquoi Math.round (0.49999999999999994) renvoie 1 Dans ce cas, la perte d'un bit dans le calcul entraîne une grande différence dans la réponse.

Peter Lawrey
la source
1
Où cela est-il mis en œuvre?
EpicPandaForce
16
@Zhuinden Le CPU suit la norme IEEE-754. Java vous donne accès aux instructions CPU sous-jacentes et n'intervient pas. en.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey
10
@PeterLawrey: Pas nécessairement le CPU. Sur une machine sans virgule flottante dans le CPU (et sans FPU séparé en cours d'utilisation), l'arithmétique IEEE sera effectuée par logiciel. Et si le processeur hôte a une virgule flottante mais qu'il n'est pas conforme aux exigences IEEE, je pense qu'une implémentation Java pour ce processeur serait également obligée d'utiliser le flotteur doux ...
R .. GitHub ARRÊTEZ D'AIDER ICE
1
@R .. auquel cas je ne sais pas ce qui se passerait si vous utilisiez strictfp Time pour considérer les entiers à virgule fixe, je pense. (ou BigDecimal)
Peter Lawrey
2
@eugene le problème clé est les valeurs limitées que la virgule flottante peut représenter. Cette limitation peut entraîner une perte d'informations et à mesure que le nombre augmente, une perte d'erreur. Il utilise l'arrondi, mais dans ce cas, l'arrondit vers le bas pour que ce qui aurait été un nombre légèrement trop grand car 0,1 est légèrement trop grand, se transforme en valeur correcte. Exactement 0.5
Peter Lawrey
47

Le dépassement de capacité, en virgule flottante, x + x + xest exactement le nombre à virgule flottante correctement arrondi (c'est-à-dire le plus proche) du réel 3 * x, x + x + x + xest exactement 4 * x, et x + x + x + x + xest à nouveau l'approximation à virgule flottante correctement arrondie pour 5 * x.

Le premier résultat, car x + x + x, vient du fait que x + xc'est exact. x + x + xest donc le résultat d'un seul arrondi.

Le second résultat est plus difficile, une démonstration en est discutée ici (et Stephen Canon fait allusion à une autre analyse de preuve par cas sur les 3 derniers chiffres de x). Pour résumer, soit 3 * xest dans le même binade que 2 * xsoit il est dans le même binade que 4 * x, et dans chaque cas il est possible de déduire que l'erreur sur le troisième ajout annule l'erreur sur le deuxième addition (le premier ajout étant exact, comme nous l'avons déjà dit).

Le troisième résultat, « x + x + x + x + xest correctement arrondi», dérive du second de la même manière que le premier dérive de l'exactitude de x + x.


Le deuxième résultat explique pourquoi 0.1 + 0.1 + 0.1 + 0.1est exactement le nombre à virgule flottante 0.4: les nombres rationnels 1/10 et 4/10 sont approximés de la même manière, avec la même erreur relative, lorsqu'ils sont convertis en virgule flottante. Ces nombres à virgule flottante ont un rapport d'exactement 4 entre eux. Le premier et le troisième résultat montrent que 0.1 + 0.1 + 0.1et que l' 0.1 + 0.1 + 0.1 + 0.1 + 0.1on peut s'attendre à avoir moins d'erreur que ce qui pourrait être déduit par une analyse d'erreur naïve, mais, en eux-mêmes, ils ne relient que les résultats respectivement à 3 * 0.1et 5 * 0.1, dont on peut s'attendre à ce qu'ils soient proches mais pas nécessairement identiques à 0.3et 0.5.

Si vous continuez à ajouter 0.1après la quatrième addition, vous observerez finalement des erreurs d'arrondi qui font que « 0.1ajouté à lui-même n fois» diverge n * 0.1et diverge encore plus de n / 10. Si vous deviez tracer les valeurs de «0,1 ajouté à lui-même n fois» en fonction de n, vous observeriez des lignes de pente constante par binades (dès que le résultat de la nième addition est destiné à tomber dans un binade particulier, on peut s'attendre à ce que les propriétés de l'addition soient similaires aux ajouts précédents qui ont produit un résultat dans le même binade). Dans un même binade, l'erreur augmentera ou diminuera. Si vous regardiez la séquence des pentes de binade à binade, vous reconnaîtriez les chiffres répétés de0.1en binaire pendant un moment. Après cela, l'absorption commencerait à se produire et la courbe deviendrait plate.

Pascal Cuoq
la source
1
Sur la première ligne, vous dites que x + x + x est exactement correct, mais d'après l'exemple de la question, ce n'est pas le cas.
Alboz
2
@Alboz Je dis que x + x + xc'est exactement le nombre à virgule flottante correctement arrondi au vrai 3 * x. «Correctement arrondi» signifie «le plus proche» dans ce contexte.
Pascal Cuoq
4
+1 Cela devrait être la réponse acceptée. Il offre en fait des explications / preuves de ce qui se passe plutôt que de vagues généralités.
R .. GitHub STOP HELPING ICE
1
@Alboz (tout cela est envisagé par la question). Mais ce que cette réponse explique, c'est comment les erreurs s'annulent fortuitement plutôt que de s'additionner dans le pire des cas.
hobbs
1
@chebus 0.1 est 0x1.999999999999999999999… p-4 en hexadécimal (une séquence infinie de chiffres). Il est approximé en double précision comme 0x1.99999ap-4. 0,2 est 0x1.999999999999999999999… p-3 en hexadécimal. Pour la même raison que 0,1 correspond approximativement à 0x1.99999ap-4, 0,2 correspond à 0x1.99999ap-3. Pendant ce temps, 0x1.99999ap-3 est également exactement 0x1.99999ap-4 + 0x1.99999ap-4.
Pascal Cuoq
-1

Les systèmes à virgule flottante font diverses magies, y compris avoir quelques bits supplémentaires de précision pour l'arrondi. Ainsi, la très petite erreur due à la représentation inexacte de 0,1 finit par être arrondie à 0,5.

Pensez à la virgule flottante comme étant une manière excellente mais INEXACTE de représenter les nombres. Tous les nombres possibles ne sont pas facilement représentés dans un ordinateur. Des nombres irrationnels comme PI. Ou comme SQRT (2). (Les systèmes mathématiques symboliques peuvent les représenter, mais j'ai dit «facilement».)

La valeur en virgule flottante peut être extrêmement proche, mais pas exacte. Il est peut-être si proche que vous pourriez naviguer vers Pluton et vous éloigner de quelques millimètres. Mais toujours pas exact au sens mathématique.

N'utilisez pas de virgule flottante lorsque vous devez être exact plutôt qu'approximatif. Par exemple, les applications de comptabilité veulent garder une trace exacte d'un certain nombre de centimes dans un compte. Les nombres entiers sont bons pour cela car ils sont exacts. Le problème principal que vous devez surveiller avec les entiers est le dépassement de capacité.

L'utilisation de BigDecimal pour la devise fonctionne bien car la représentation sous-jacente est un entier, bien que gros.

Reconnaissant que les nombres à virgule flottante sont inexacts, ils ont encore de nombreuses utilisations. Systèmes de coordonnées pour la navigation ou coordonnées dans les systèmes graphiques. Valeurs astronomiques. Valeurs scientifiques. (De toute façon, vous ne pouvez probablement pas connaître la masse exacte d'une balle de baseball à une masse d'un électron, donc l'inexactitude n'a pas vraiment d'importance.)

Pour les applications de comptage (y compris la comptabilité), utilisez un entier. Pour compter le nombre de personnes qui franchissent une porte, utilisez int ou long.

DannyB
la source
2
La question est balisée [java]. La définition du langage Java ne prévoit pas de «quelques bits de précision supplémentaires», uniquement pour quelques bits d'exposant supplémentaires (et ce, uniquement si vous ne l'utilisez pas strictfp). Ce n'est pas parce que vous avez renoncé à comprendre quelque chose que cela ne signifie pas qu'il est insondable ni que les autres devraient renoncer à le comprendre. Voir stackoverflow.com/questions/18496560 comme exemple des longueurs auxquelles les implémentations Java iront pour implémenter la définition du langage (qui n'inclut aucune disposition pour les bits de précision supplémentaires ni, avec strictfp, pour tout bit d'exp supplémentaire)
Pascal Cuoq