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

294

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

Les deux Java et JavaScript renvoient les mêmes résultats.

Je comprends que, en raison de la façon 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?

Marlon Bernardes
la source
28
La somme des nombres réels est associative et commutative. Les virgules flottantes ne sont pas de vrais nombres. En fait, vous venez de prouver que leurs opérations ne sont pas commutatives. Il est assez facile de montrer qu'ils ne sont pas trop associatifs (par exemple (2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1)). Par conséquent, oui: méfiez-vous lors du choix de l'ordre des sommes et des autres opérations. Certains langages proposent une fonction intégrée pour effectuer des sommes «de haute précision» (par exemple, les pythons math.fsum), vous pouvez donc envisager d'utiliser ces fonctions au lieu de l'algorithme de somme naïve.
Bakuriu
1
@RBerteig Cela peut être déterminé en examinant l'ordre des opérations du langage pour les expressions arithmétiques et, à moins que leur représentation des nombres à virgule flottante en mémoire soit différente, les résultats seront les mêmes si leurs règles de priorité d'opérateur sont les mêmes. Autre point à noter: je me demande combien de temps il a fallu aux développeurs qui développent des applications bancaires pour comprendre cela? Ces cents 0000000000004 supplémentaires s'additionnent vraiment!
Chris Cirefice
3
@ChrisCirefice: si vous avez 0,00000004 cents , vous vous trompez. Vous ne devez jamais utiliser un type à virgule flottante binaire pour les calculs financiers.
Daniel Pryden
2
@DanielPryden Ah hélas, c'était une blague ... juste jeter l'idée que les gens qui ont vraiment besoin de résoudre ce type de problème avaient l'un des emplois les plus importants que vous connaissez, détiennent le statut monétaire des gens et tout ce qui . J'étais très sarcastique ...
Chris Cirefice
6
Très sec (et vieux, mais toujours pertinent): ce que tout informaticien devrait savoir sur l'arithmétique
Brian

Réponses:

276

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

Il changera les points auxquels les valeurs sont arrondies, en fonction de leur ampleur. Comme exemple du genre de chose que nous voyons, supposons qu'au lieu de virgule flottante binaire, nous utilisions un type décimal à virgule flottante avec 4 chiffres significatifs, où chaque ajout est effectué avec une précision "infinie", puis arrondi à le 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 démontre peut-être plus clairement que la partie importante est que nous avons un nombre limité de chiffres significatifs - pas un nombre limité de décimales . Si nous pouvions toujours garder le même nombre de décimales, alors avec l'addition et la soustraction au moins, nous irions bien (tant que les valeurs ne débordaient pas). Le problème est que lorsque vous obtenez de plus grands nombres, des informations plus petites sont perdues - le 10001 étant arrondi à 10000 dans ce cas. (Ceci est un exemple du problème que Eric Lippert a noté dans sa réponse .)

Il est important de noter que les valeurs sur la première ligne du côté droit sont les mêmes dans tous les cas - donc bien qu'il soit important de comprendre que vos nombres décimaux (23,53, 5,88, 17,64) ne seront pas représentés exactement comme des doublevaleurs, c'est seulement un problème en raison des problèmes ci-dessus.

Jon Skeet
la source
10
May extend this later - out of time right now!l'attendant avec impatience @Jon
Prateek
3
quand je dis que je reviendrai à une réponse plus tard, la communauté est un peu moins gentille avec moi <entrez une sorte d'émoticône légère ici pour montrer que je plaisante et non un crétin> ... j'y reviendrai plus tard.
Grady Player
2
@ZongZhengLi: Bien qu'il soit certainement important de comprendre cela, ce n'est pas la cause première dans ce cas. Vous pouvez écrire un exemple similaire avec des valeurs qui sont représentées exactement en binaire et voir le même effet. Le problème ici est de maintenir à la fois des informations à grande échelle et des informations à petite échelle.
Jon Skeet
1
@Buksy: arrondi à 10000 - car nous avons affaire à un type de données qui ne peut stocker que 4 chiffres significatifs. (donc x.xxx * 10 ^ n)
Jon Skeet
3
@meteors: Non, cela ne provoque pas de débordement - et vous utilisez les mauvais numéros. C'est 10001 étant arrondi à 10000, et non 1001 étant arrondi à 1000. Pour être plus clair, 54321 serait arrondi à 54320 - parce que cela n'a que quatre chiffres significatifs. Il y a une grande différence entre "quatre chiffres significatifs" et "une valeur maximale de 9999". Comme je l'ai déjà dit, vous représentez essentiellement x.xxx * 10 ^ n, où pour 10000, x.xxx serait 1.000 et n serait 4. C'est exactement comme doubleet float, où pour les très grands nombres, les nombres représentables consécutifs sont plus de 1 à part.
Jon Skeet
52

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 être représentées 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);
   }
}

le printValueAndInHex méthode 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 x, y, zet sde » représentations hexadécimaux. Dans la représentation en 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 fait 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

Première série d'ajouts

Le deuxième nombre ( y) est de plus petite ampleur. Lors de l'ajout de ces deux nombres à obtenir x + y, les 2 derniers bits du deuxième nombre ( 01) sont décalés hors de la plage et ne figurent pas dans le calcul.

Le deuxième ajout ajoute x + yet zajoute deux nombres de la même échelle.

Deuxième série d'ajouts

Ici, x + zse produit en premier. Ils sont de la même échelle, mais ils donnent un nombre plus élevé dans l'échelle:

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

Le deuxième ajout ajoute x + zet y, et maintenant 3 bits sont supprimés ypour ajouter les nombres ( 101). Ici, il doit y avoir un arrondi vers le haut, car le résultat est le prochain nombre à virgule flottante vers le haut: 4047866666666666pour le premier ensemble d'additions par rapport à4047866666666667 au deuxième ensemble d'additions. Cette erreur est suffisamment importante pour apparaître dans 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 elles deviennent encore plus inexactes lorsque les échelles sont différentes. Ajoutez et soustrayez des nombres d'échelle similaire si vous le pouvez.

rgettman
la source
Les échelles étant différentes est la partie importante. Vous pouvez écrire (en décimal) les valeurs exactes qui sont représentées en binaire comme entrées, et avoir toujours le même problème.
Jon Skeet
@rgettman En tant que programmeur, j'aime mieux votre réponse =)+1 pour votre assistant d'imprimante hexadécimale ... c'est vraiment bien!
2013 ADTC
44

La réponse de Jon est bien sûr correcte. Dans votre cas, l'erreur n'est pas plus grande que l'erreur que vous accumulez en effectuant une simple opération à virgule flottante. Vous avez un scénario où, dans un cas, vous obtenez zéro erreur et dans un autre, vous obtenez une petite erreur; ce n'est pas vraiment un scénario intéressant. Une bonne question est: y a-t-il des scénarios où le changement de l'ordre des calculs passe d'une petite erreur à une erreur (relativement) énorme? La réponse est sans ambiguïté oui.

Considérez 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;

De toute évidence, 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 et x2 et x3 diffèrent considérablement. Voyez si vous pouvez le faire!

Eric Lippert
la source
Comment définissez-vous une grande quantité? Parle-t-on de l'ordre du 1000e? 100e? 1 ???
Cruncher
3
@Cruncher: calcule le résultat mathématique exact et les valeurs x1 et x2. Appelez la différence mathématique exacte entre les résultats réels et calculés e1 et e2. Il existe maintenant plusieurs façons de penser à la taille des erreurs. La première est: pouvez-vous trouver un scénario dans lequel | e1 / e2 | ou | e2 / e1 | sont grands? Par exemple, pouvez-vous faire l'erreur de dix fois celle de l'autre? Le plus intéressant est cependant de savoir si vous pouvez faire l'erreur d'une fraction significative de la taille de la bonne réponse.
Eric Lippert
1
Je me rends compte qu'il parle d'exécution, mais je me demande: si l'expression était une expression au moment de la compilation (par exemple, constexpr), les compilateurs sont-ils suffisamment intelligents pour minimiser l'erreur?
Kevin Hsu
@kevinhsu en général non, le compilateur n'est pas si intelligent. Bien sûr, le compilateur peut choisir de faire l'opération en arithmétique exacte s'il le souhaite, mais ce n'est généralement pas le cas.
Eric Lippert
8
@frozenkoi: Oui, l'erreur peut être infinie très facilement. Par exemple, considérons le C #: double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);- la sortie est Infinity puis 0.
Jon Skeet
10

Cela couvre en réalité bien plus que Java et Javascript, et affecterait probablement tout langage de programmation utilisant des flottants ou des doubles.

En mémoire, les virgules flottantes utilisent un format spécial du type IEEE 754 (le convertisseur fournit une bien meilleure explication que moi).

Quoi qu'il en soit, voici le convertisseur flottant.

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 à partir 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 et de désactiver 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 provoquer cette différence de virgule flottante.

Edit: Pour ceux d'entre vous qui se souviennent de chiffres importants, 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 attaché.

Boussole
la source
9

Les nombres à virgule flottante sont représentés au format IEEE 754, qui fournit une taille spécifique de bits 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 second cas, l'ajout rencontre 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, tandis que 23,53 + 5,88 le peuvent.

Malheureusement, c'est un problème connu que vous n'avez qu'à régler.

jbx
la source
6

Je crois que cela a à voir avec l'ordre de l'évaluation. 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 à virgule flottante peuvent descendre.

Lorsque vous modifiez la commande,

A + C = F
F + B = D(2)
hotforfeature
la source
4
Je pense que cette réponse évite la vraie raison. "il y a cette étape secondaire où les nombres à virgule flottante peuvent descendre". Clairement, c'est vrai, mais ce que nous voulons expliquer, c'est pourquoi .
Zong