¿Por qué cambiar el orden de la suma arroja un resultado diferente?

Resuelto Marlon Bernardes asked hace 11 años • 8 respuestas

¿Por qué cambiar el orden de la suma arroja un resultado diferente?

23.53 + 5.88 + 17.64 = 47.05

23.53 + 17.64 + 5.88 = 47.050000000000004

Tanto Java como JavaScript devuelven los mismos resultados.

Entiendo que, debido a la forma en que se representan los números de coma flotante en binario, algunos números racionales ( como 1/3 - 0,333333... ) no se pueden representar con precisión.

¿Por qué el simple hecho de cambiar el orden de los elementos afecta el resultado?

Marlon Bernardes avatar Nov 07 '13 01:11 Marlon Bernardes
Aceptado

Quizás esta pregunta sea estúpida, pero ¿por qué el simple hecho de cambiar el orden de los elementos afecta el resultado?

Cambiará los puntos en los que se redondean los valores, según su magnitud. Como ejemplo del tipo de cosas que estamos viendo, supongamos que en lugar de coma flotante binaria, estuviéramos usando un tipo de coma flotante decimal con 4 dígitos significativos, donde cada suma se realiza con precisión "infinita" y luego se redondea a el número representable más cercano. Aquí hay dos sumas:

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)

Ni siquiera necesitamos números que no sean enteros para que esto sea un problema:

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

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

Esto demuestra posiblemente más claramente que lo importante es que tengamos un número limitado de dígitos significativos , no un número limitado de decimales . Si siempre pudiéramos mantener el mismo número de decimales, entonces, al menos con la suma y la resta, estaríamos bien (siempre que los valores no se desbordaran). El problema es que cuando se llega a números más grandes, se pierde información más pequeña; en este caso, el 10001 se redondea a 10000. (Este es un ejemplo del problema que Eric Lippert señaló en su respuesta ).

Es importante tener en cuenta que los valores en la primera línea del lado derecho son los mismos en todos los casos, por lo que, aunque es importante comprender que sus números decimales (23,53, 5,88, 17,64) no se representarán exactamente como valores, eso doublees Sólo es un problema debido a los problemas mostrados arriba.

Jon Skeet avatar Nov 06 '2013 18:11 Jon Skeet

Esto es lo que sucede en binario. Como sabemos, algunos valores de punto flotante no se pueden representar exactamente en binario, incluso si se pueden representar exactamente en decimal. Estos 3 números son sólo ejemplos de ese hecho.

Con este programa genero las representaciones hexadecimales de cada número y los resultados de cada suma.

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);
   }
}

El printValueAndInHexmétodo es sólo una ayuda de la impresora hexadecimal.

El resultado es el siguiente:

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

Los primeros 4 números son representaciones hexadecimales de x, y, zy . sEn la representación de punto flotante IEEE, los bits 2-12 representan el exponente binario , es decir, la escala del número. (El primer bit es el bit de signo y los bits restantes son la mantisa ). El exponente representado es en realidad el número binario menos 1023.

Se extraen los exponentes de los primeros 4 números:

    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

Primer conjunto de adiciones

El segundo número ( y) es de menor magnitud. Al sumar estos dos números para obtener x + y, los últimos 2 bits del segundo número ( 01) se desplazan fuera del rango y no figuran en el cálculo.

La segunda suma suma x + yy zy suma dos números de la misma escala.

Segundo conjunto de adiciones

Aquí x + zocurre primero. Son de la misma escala, pero dan como resultado un número que está más arriba en la escala:

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

La segunda suma suma x + zy y, y ahora se eliminan 3y bits para sumar los números ( 101). Aquí, debe haber una ronda hacia arriba, porque el resultado es el siguiente número de punto flotante hacia arriba: 4047866666666666para el primer conjunto de sumas versus 4047866666666667para el segundo conjunto de sumas. Ese error es lo suficientemente significativo como para aparecer en la impresión del total.

En conclusión, tenga cuidado al realizar operaciones matemáticas con números IEEE. Algunas representaciones son inexactas y se vuelven aún más inexactas cuando las escalas son diferentes. Suma y resta números de escala similar si puedes.

rgettman avatar Nov 06 '2013 19:11 rgettman

La respuesta de Jon es, por supuesto, correcta. En su caso, el error no es mayor que el error que acumularía al realizar cualquier operación simple de punto flotante. Tienes un escenario en el que en un caso obtienes cero errores y en otro obtienes un pequeño error; En realidad, ese no es un escenario tan interesante. Una buena pregunta es: ¿ existen escenarios en los que cambiar el orden de los cálculos pasa de un pequeño error a un error (relativamente) enorme? La respuesta es inequívocamente sí.

Considere por ejemplo:

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;

Obviamente en aritmética exacta serían lo mismo. Es entretenido intentar encontrar valores para a, b, c, d, e, f, g, h tales que los valores de x1, x2 y x3 difieran en gran cantidad. ¡Mira si puedes hacerlo!

Eric Lippert avatar Nov 06 '2013 19:11 Eric Lippert