308 votes

Pourquoi le fait de passer de la somme à la commande renvoie un résultat différent?

Pourquoi le fait de passer de la somme à la commande renvoie 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 le JavaScript 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ées de manière précise.

Pourquoi est-ce tout simplement de changer l'ordre des éléments affecte le résultat?

281voto

Jon Skeet Points 692016

Peut-être que cette question est stupide, mais pourquoi est-ce tout simplement de changer l'ordre des éléments affecte le résultat?

Il va changer les points où les valeurs sont arrondies, en fonction de leur ampleur. Comme un exemple du genre de chose que nous observons, imaginons qu'au lieu de binaire à virgule flottante, nous avons été à l'aide d'un décimal à virgule flottante type avec 4 chiffres significatifs, où chaque addition est effectuée à "l'infini", de précision et puis arrondie au nombre représentable. 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 ce 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 l'important, c'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 chiffres après la virgule, puis avec l'addition et la soustraction au moins, nous serions très bien (à condition que les valeurs n'ont pas de débordement). Le problème est que lorsque vous obtenez au plus grand nombre, la plus petite de l'information est perdue - le 10001 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 de la première ligne du côté droit sont les mêmes dans tous les cas, - si bien qu'il est important de comprendre que vos nombres décimaux (23.53, 5.88, 17.64) ne sera pas exactement comme double valeurs, c'est seulement un problème à cause des problèmes indiqués ci-dessus.

52voto

rgettman Points 74908

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

Avec ce programme, j'ai sortie les représentations hexadécimales de chaque numéro 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);
   }
}

L' printValueAndInHex méthode n'est qu'un hex-imprimante helper.

La sortie est comme suit:

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 numéros sont x, y, z, et s's des représentations hexadécimales. En virgule flottante IEEE de la représentation, les bits 2 à 12 représentent le binaire de l'exposant, qui est, à l'échelle de la nombre. (Le premier bit est le bit de signe, et le reste des bits pour la mantisse.) L'exposant représenté est en fait le nombre binaire moins 1023.

Les exposants pour les 4 premiers chiffres 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 pour obtenir de l' x + y, les 2 derniers bits du deuxième nombre (01) sont décalés hors de portée et ne figure pas dans le calcul.

La deuxième plus ajoute x + y et z et ajoute les deux nombres de la même échelle.

Deuxième série d'ajouts

Ici, x + z se produit en premier. Ils sont de la même échelle, mais ils donnent un nombre qui est plus haut dans l'échelle:

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

La deuxième plus ajoute x + z et y, et maintenant 3 bits sont passé de y la somme des nombres (101). Ici, il doit y avoir un tour vers le haut, parce que le résultat est le prochain nombre à virgule flottante: 4047866666666666 de la première série d'ajouts vs 4047866666666667 pour la deuxième série d'ajouts. Cette erreur est assez importante pour montrer à l'impression du total.

En conclusion, être prudent lors de l'exécution d'opérations mathématiques sur les numéros IEEE. Certaines représentations sont inexactes, et ils deviennent encore plus inexact lorsque les échelles sont différentes. Ajouter et soustraire des nombres d'envergure comparable, si vous le pouvez.

44voto

Eric Lippert Points 300275

Jon la réponse est, bien sûr, correct. Dans votre cas, l'erreur n'est pas plus grande que l'erreur serait de s'accumuler faire une simple opération de virgule flottante. Vous avez un scénario où dans un cas, vous ne bénéficiez d'aucune erreur et dans l'autre, vous obtenez une petite erreur; ce n'est pas réellement intéressant d'un scénario. Une bonne question est: existe-il des scénarios où la modification de l'ordre des calculs de passe à partir d'un minuscule erreur, à une (relativement) énorme erreur? La réponse est clairement oui.

Considérons par exemple:

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

vs

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

vs

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 les valeurs de a, b, c, d, e, f, g, h à ce que les valeurs de x1 et x2 et x3 distinguent par une grande quantité. Voyez si vous pouvez le faire!

10voto

Compass Points 2117

Cela couvre beaucoup plus que Java et Javascript, et serait susceptible d'affecter n'importe quel langage de programmation à l'aide de flotteurs ou doubles.

Dans la mémoire, floating points d'utiliser un format particulier le long de la lignes de la norme IEEE 754 (le convertisseur fournit une bien meilleure explication que je peux).

De toute façon, voici le flotteur convertisseur.

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 de rendements 29.41 à partir de la première des deux valeurs, ce qui nous donne 2^4 que l'exposant.

Votre deuxième ligne de rendements 41.17 qui nous donne 2^5 que l'exposant.

Nous sommes en train de perdre une figure significative par l'augmentation de la puissance, qui est de nature à modifier le résultat.

Essayez cochant le dernier bit à l'extrême droite sur et en dehors pour 41.17 et vous pouvez voir que quelque chose d'aussi insignifiant", 1/2^23 de l'exposant serait suffisant pour provoquer ce point flottant différence.

Edit: Pour ceux d'entre vous qui se souviennent de chiffres significatifs, ce serait tomber dans cette catégorie. 10^4 + 4999 avec une importante figure 1 est 10^4. Dans ce cas, le chiffre significatif est beaucoup plus petit, mais nous pouvons voir les résultats avec le .00000000004 attaché à elle.

9voto

jbx Points 4063

Les nombres à virgule flottante sont représentés en utilisant la norme IEEE 754, ce qui offre une taille spécifique de bits pour la mantisse (significande). Malheureusement, cela vous donne un certain nombre de fractions de blocs de construction pour jouer avec, et de certaines fractions de valeurs ne peuvent pas être représentés de manière précise.

Ce qui se passe dans votre cas est que dans le second cas, l'addition est probablement un peu de précision à cause de l'ordre, les ajouts sont évalués. Je n'ai pas calculé les valeurs, mais il se pourrait par exemple que 23.53 + 17.64 ne peuvent pas être précisément représentées, tandis que 23.53 + 5.88 peut.

Malheureusement, c'est un problème connu, vous avez juste à composer avec.

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