¿Por qué cambiar el orden de la suma arroja un resultado diferente?
¿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?
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 double
es Sólo es un problema debido a los problemas mostrados arriba.
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 printValueAndInHex
mé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
, z
y . s
En 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 + y
y z
y suma dos números de la misma escala.
Segundo conjunto de adiciones
Aquí x + z
ocurre 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 + z
y 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: 4047866666666666
para el primer conjunto de sumas versus 4047866666666667
para 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.
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!