web-dev-qa-db-fra.com

Pourquoi la modification de l'ordre de somme renvoie-t-elle un résultat différent?

Pourquoi la modification de l'ordre de somme renvoie-t-elle un résultat différent?

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

Java et JavaScript retournent les mêmes résultats.

Je comprends que, en raison de la manière dont les nombres à virgule flottante sont représentés en binaire, certains nombres rationnels ( comme 1/3 - 0.333333 ...) ne peuvent pas être représentés avec précision.

Pourquoi le simple changement de l'ordre des éléments affecte-t-il le résultat?

292
Marlon Bernardes

Peut-être que cette question est stupide, mais pourquoi le simple changement de l'ordre des éléments affecte-t-il le résultat?

Cela modifiera les points auxquels les valeurs sont arrondies, en fonction de leur grandeur. Comme exemple du type que nous voyons, supposons qu'au lieu de virgule flottante binaire, nous utilisions un type à virgule flottante décimale avec 4 chiffres significatifs, où chaque addition est effectuée avec une précision "infinie" puis arrondie au nombre représentable le plus proche. Voici deux sommes:

1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
                = 1.000 + 0.6667 (no rounding needed!)
                = 1.667 (where 1.6667 is rounded to 1.667)

2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
                = 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
                = 1.666 (where 1.6663 is rounded to 1.666)

Nous n'avons même pas besoin de non-entiers pour que ce soit un problème:

10000 + 1 - 10000 = (10000 + 1) - 10000
                  = 10000 - 10000 (where 10001 is rounded to 10000)
                  = 0

10000 - 10000 + 1 = (10000 - 10000) + 1
                  = 0 + 1
                  = 1

Cela montre peut-être plus clairement que l’important est que nous ayons un nombre limité de chiffres significatifs - et non un nombre limité de décimales . Si nous pouvions toujours garder le même nombre de décimales, alors avec addition et soustraction au moins, tout irait bien (tant que les valeurs ne dépassaient pas). Le problème est que lorsque vous obtenez de plus grands nombres, des informations plus petites sont perdues - le 10001 étant arrondi à 10 000 dans ce cas. (C’est un exemple du problème que Eric Lippert a noté dans sa réponse .)

Il est important de noter que les valeurs de la première ligne du côté droit sont identiques dans tous les cas. Il est donc important de comprendre que vos nombres décimaux (23.53, 5.88, 17.64) ne seront pas représentés exactement comme double valeurs, c'est seulement un problème en raison des problèmes montrés ci-dessus.

275
Jon Skeet

Voici ce qui se passe en binaire. Comme nous le savons, certaines valeurs à virgule flottante ne peuvent pas être représentées exactement en binaire, même si elles peuvent l'être exactement en décimal. Ces 3 chiffres ne sont que des exemples de ce fait.

Avec ce programme, je produis les représentations hexadécimales de chaque nombre et les résultats de chaque addition.

public class Main{
   public static void main(String args[]) {
      double x = 23.53;   // Inexact representation
      double y = 5.88;    // Inexact representation
      double z = 17.64;   // Inexact representation
      double s = 47.05;   // What math tells us the sum should be; still inexact

      printValueAndInHex(x);
      printValueAndInHex(y);
      printValueAndInHex(z);
      printValueAndInHex(s);

      System.out.println("--------");

      double t1 = x + y;
      printValueAndInHex(t1);
      t1 = t1 + z;
      printValueAndInHex(t1);

      System.out.println("--------");

      double t2 = x + z;
      printValueAndInHex(t2);
      t2 = t2 + y;
      printValueAndInHex(t2);
   }

   private static void printValueAndInHex(double d)
   {
      System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
   }
}

La méthode printValueAndInHex est juste une aide d'imprimante hexadécimale.

La sortie est la suivante:

403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004

Les 4 premiers chiffres sont les représentations hexadécimales de x, y, z et s. Dans la représentation à virgule flottante IEEE, les bits 2 à 12 représentent l’exposant binaire , c’est-à-dire l’échelle du nombre. (Le premier bit est le bit de signe et les bits restants pour la mantisse .) L'exposant représenté est en réalité le nombre binaire moins 1023.

Les exposants des 4 premiers nombres sont extraits:

    sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5

Premier ensemble d'ajouts

Le deuxième nombre (y) est de moindre magnitude. Lorsque vous ajoutez ces deux nombres, vous obtenez x + y, les 2 derniers bits du second nombre (01) sont décalés hors de la plage et ne figurent pas dans le calcul.

Le deuxième ajout ajoute x + y et z et ajoute deux nombres de même échelle.

Deuxième série d'ajouts

Ici, x + z se produit en premier. Elles ont la même échelle, mais elles donnent un nombre plus élevé:

404 => 0|100 0000 0100| => 1028 - 1023 = 5

Le deuxième ajout ajoute x + z et y, et maintenant 3 bits sont supprimés de y pour ajouter les nombres (101). Ici, il doit y avoir un tour vers le haut, car le résultat est le prochain nombre à virgule flottante: 4047866666666666 pour le premier ensemble d’additions contre 4047866666666667 pour le deuxième ensemble d’ajouts. Cette erreur est suffisamment significative pour apparaître sur l’impression du total.

En conclusion, soyez prudent lorsque vous effectuez des opérations mathématiques sur des nombres IEEE. Certaines représentations sont inexactes et deviennent encore plus inexactes lorsque les échelles sont différentes. Ajoutez et soustrayez des nombres d’échelle similaire si vous le pouvez.

52
rgettman

La réponse de Jon est bien sûr correcte. Dans votre cas, l’erreur n’est pas supérieure à l’erreur que vous auriez accumulée lors d’une opération en virgule flottante simple. Vous avez un scénario dans lequel, dans un cas, vous obtenez zéro erreur et dans un autre, vous obtenez une erreur minuscule; ce n'est pas vraiment un scénario intéressant. Une bonne question est: existe-t-il des scénarios dans lesquels le changement d’ordre des calculs passe d’une erreur infime à une erreur (relativement) énorme? La réponse est sans équivoque: oui.

Considérons par exemple:

x1 = (a - b) + (c - d) + (e - f) + (g - h);

contre

x2 = (a + c + e + g) - (b + d + f + h);

contre

x3 = a - b + c - d + e - f + g - h;

Évidemment, en arithmétique exacte, ils seraient les mêmes. Il est amusant d'essayer de trouver des valeurs pour a, b, c, d, e, f, g, h telles que les valeurs de x1, x2 et x3 diffèrent par une grande quantité. Voyez si vous pouvez le faire!

44
Eric Lippert

En fait, cela couvre bien plus que Java et Javascript), et cela affecterait probablement tout langage de programmation utilisant des méthodes float ou double.

En mémoire, les points flottants utilisent un format spécial inspiré de la norme IEEE 754 (le convertisseur fournit une explication bien meilleure que ce que je peux).

Quoi qu'il en soit, voici le convertisseur de flotteur.

http://www.h-schmidt.net/FloatConverter/

La chose à propos de l'ordre des opérations est la "finesse" de l'opération.

Votre première ligne donne 29,41 des deux premières valeurs, ce qui nous donne 2 ^ 4 comme exposant.

Votre deuxième ligne donne 41,17, ce qui nous donne 2 ^ 5 comme exposant.

Nous perdons un chiffre significatif en augmentant l'exposant, ce qui est susceptible de changer le résultat.

Essayez de cocher le dernier bit à l'extrême droite pour 41,17 et vous pouvez voir que quelque chose d'aussi "insignifiant" que 1/2 ^ 23 de l'exposant serait suffisant pour causer cette différence en virgule flottante.

Edit: Pour ceux d'entre vous qui se souviennent de chiffres significatifs, cela tomberait dans cette catégorie. 10 ^ 4 + 4999 avec un chiffre significatif de 1 va être 10 ^ 4. Dans ce cas, le chiffre significatif est beaucoup plus petit, mais nous pouvons voir les résultats avec le .00000000004 qui y est associé.

10
Compass

Les nombres en virgule flottante sont représentés au format IEEE 754, qui fournit une taille de bits spécifique pour la mantisse (significande). Malheureusement, cela vous donne un nombre spécifique de "blocs de construction fractionnaires" avec lesquels jouer, et certaines valeurs fractionnaires ne peuvent pas être représentées avec précision.

Ce qui se passe dans votre cas, c'est que dans le deuxième cas, l'ajout pose probablement un problème de précision en raison de l'ordre dans lequel les ajouts sont évalués. Je n'ai pas calculé les valeurs, mais il se pourrait par exemple que 23,53 + 17,64 ne puissent pas être représentés avec précision, alors que 23,53 + 5,88 le peuvent.

Malheureusement, il s’agit d’un problème connu que vous devez régler.

9
jbx

Je crois que cela a à voir avec l'ordre d'évaulation. Alors que la somme est naturellement la même dans un monde mathématique, dans le monde binaire au lieu de A + B + C = D, c’est

A + B = E
E + C = D(1)

Il y a donc cette étape secondaire où les nombres en virgule flottante peuvent être supprimés.

Lorsque vous modifiez la commande,

A + C = F
F + B = D(2)
6
hotforfeature