java - una - Cuando se usan dobles, ¿por qué no es(x/(y*z)) lo mismo que(x/y/z)?
listas doblemente enlazadas java (5)
Ciertamente, el orden de las operaciones combinado con el hecho de que los dobles no son precisos :
450.00d / (7d * 60) --> a = 7d * 60 --> result = 450.00d / a
vs
450.00d / 7d / 60 --> a = 450.00d /7d --> result = a / 60
Esta pregunta ya tiene una respuesta aquí:
Esto es en parte académico, ya que para mis propósitos solo lo necesito redondeado a dos decimales; pero estoy ansioso por saber qué está sucediendo para producir dos resultados ligeramente diferentes.
Esta es la prueba que escribí para limitarla a la implementación más simple:
@Test
public void shouldEqual() {
double expected = 450.00d / (7d * 60); // 1.0714285714285714
double actual = 450.00d / 7d / 60; // 1.0714285714285716
assertThat(actual).isEqualTo(expected);
}
Pero falla con esta salida:
org.junit.ComparisonFailure:
Expected :1.0714285714285714
Actual :1.0714285714285716
¿Alguien puede explicar en detalle lo que sucede debajo del capó para que el valor en 1.000000000000000 X
sea diferente?
Algunos de los puntos que busco en una respuesta son: ¿Dónde se pierde la precisión? ¿Qué método se prefiere y por qué? ¿Cuál es realmente correcto? (En matemáticas puras, ambos no pueden estar bien. ¿Quizás ambos estén equivocados?) ¿Existe una mejor solución o método para estas operaciones aritméticas?
Eso es porque la división doble a menudo conduce a una pérdida de precisión. Dicha pérdida puede variar dependiendo del orden de las divisiones.
Cuando divides por 7d
, ya perdiste cierta precisión con el resultado real. Entonces solo divides un resultado erróneo por 60
.
Cuando divide por 7d * 60
, solo tiene que usar la división una vez, perdiendo precisión una vez.
Tenga en cuenta que la multiplicación doble a veces también puede fallar, pero eso es mucho menos común.
Simplifiquemos un poco las cosas. Lo que quiere saber es por qué 450d / 420
y 450d / 7 / 60
(específicamente) dan resultados diferentes.
Veamos cómo se realiza la división en formato de punto flotante de doble precisión IEE. Sin profundizar en los detalles de la implementación, es básicamente XOR
al bit de signo, restando el exponente del divisor del exponente del dividendo, dividiendo las mantisas y normalizando el resultado.
Primero, debemos representar nuestros números en el formato adecuado para el double
:
450 is 0 10000000111 1100001000000000000000000000000000000000000000000000
420 is 0 10000000111 1010010000000000000000000000000000000000000000000000
7 is 0 10000000001 1100000000000000000000000000000000000000000000000000
60 is 0 10000000100 1110000000000000000000000000000000000000000000000000
Primero dividamos 450
por 420
Primero viene el bit de signo, es 0
( 0 xor 0 == 0
).
Luego viene el exponente. 10000000111b - 10000000111b + 1023 == 10000000111b - 10000000111b + 01111111111b == 01111111111b
Luciendo bien, ahora la mantisa:
1.1100001000000000000000000000000000000000000000000000 / 1.1010010000000000000000000000000000000000000000000000 == 1.1100001 / 1.101001
. Hay un par de maneras diferentes de hacer esto, hablaré un poco sobre ellas más adelante. El resultado es 1.0(001)
(puede verificarlo here
).
Ahora deberíamos normalizar el resultado. Veamos los valores de bit de guardia, redondos y pegajosos:
0001001001001001001001001001001001001001001001001001 0 0 1
El bit de guardia es 0, no hacemos ningún redondeo. El resultado es, en binario:
0 01111111111 0001001001001001001001001001001001001001001001001001
Que se representa como 1.0714285714285714
en decimal.
Ahora dividamos 450
por 7
por analogía.
Bit de signo = 0
Exponente = 10000000111b - 10000000001b + 01111111111b == -01111111001b + 01111111111b + 01111111111b == 10000000101b
Mantisa = 1.1100001 / 1.11 == 1.00000(001)
Redondeo:
0000000100100100100100100100100100100100100100100100 1 0 0
Se establece el bit de guardia, los bits redondos y pegajosos no. Estamos redondeando al más cercano (modo predeterminado para IEEE), y estamos atrapados entre los dos valores posibles a los que podríamos redondear. Como el lsb es 0
, sumamos 1
. Esto nos da la mantisa redondeada:
0000000100100100100100100100100100100100100100100101
El resultado es
0 10000000101 0000000100100100100100100100100100100100100100100101
Lo que se representa como 64.28571428571429
en decimal.
Ahora tendremos que dividirlo por 60
... Pero ya sabes que hemos perdido algo de precisión. La división de 450
por 420
no requirió redondeo en absoluto, pero aquí, ya tuvimos que redondear el resultado al menos una vez . Pero, para completar, terminemos el trabajo:
Dividiendo 64.28571428571429
por 60
Bit de signo = 0
Exponente = 10000000101b - 10000000100b + 01111111111b == 01111111110b
Mantisa = 1.0000000100100100100100100100100100100100100100100101 / 1.111 == 0.10001001001001001001001001001001001001001001001001001100110011
Ronda y turno:
0.1000100100100100100100100100100100100100100100100100 1 1 0 0
1.0001001001001001001001001001001001001001001001001001 1 0 0
Al redondear al igual que en el caso anterior, obtenemos la mantisa: 0001001001001001001001001001001001001001001001001010
.
A medida que cambiamos en 1
, agregamos eso al exponente, obteniendo
Exponente = 01111111111b
Entonces, el resultado es:
0 01111111111 0001001001001001001001001001001001001001001001001010
Lo que se representa como 1.0714285714285716
en decimal.
Tl; dr :
La primera división nos dio:
0 01111111111 0001001001001001001001001001001001001001001001001001
Y la última división nos dio:
0 01111111111 0001001001001001001001001001001001001001001001001010
La diferencia está solo en los últimos 2 bits, pero podríamos haber perdido más. Después de todo, para obtener el segundo resultado, ¡tuvimos que redondear dos veces en lugar de ninguno!
Ahora, sobre la división de mantisa . La división de punto flotante se implementa de dos maneras principales.
La forma exigida por la división larga IEEE ( here hay algunos buenos ejemplos; es básicamente la división larga regular, pero con binario en lugar de decimal), y es bastante lenta. Eso es lo que hizo tu computadora.
También hay una opción más rápida pero de menos acumulación, multiplicación por inverso. Primero, se encuentra un recíproco del divisor, y luego se realiza la multiplicación.
Tiene que ver con cómo se implementa el tipo double
y el hecho de que los tipos de punto flotante no tienen las mismas garantías de precisión que otros tipos numéricos más simples. Aunque la siguiente respuesta es más específicamente sobre sumas, también responde a su pregunta explicando que no hay garantía de precisión infinita en operaciones matemáticas de punto flotante: ¿Por qué cambiar el orden de la suma devuelve un resultado diferente? . Esencialmente, nunca debe intentar determinar la igualdad de los valores de punto flotante sin especificar un margen de error aceptable. La biblioteca de guayaba de Google incluye DoubleMath.fuzzyEquals(double, double, double)
para determinar la igualdad de dos valores double
dentro de una cierta precisión. Si desea obtener información sobre los aspectos específicos de la igualdad de punto flotante, este sitio es bastante útil ; El mismo sitio también explica los errores de redondeo de punto flotante . En resumen: los valores esperados y reales de su cálculo difieren debido al redondeo que difiere entre los cálculos debido al orden de las operaciones.
Veo un montón de preguntas que te dicen cómo solucionar este problema, pero no una que realmente explique lo que está sucediendo, aparte de que "el error de redondeo de punto flotante es malo, ¿vale?" Así que déjame darle una oportunidad. Permítanme señalar primero que nada en esta respuesta es específico de Java . El error de redondeo es un problema inherente a cualquier representación de números de precisión fija, por lo que obtiene los mismos problemas en, por ejemplo, C.
Error de redondeo en un tipo de datos decimal
Como ejemplo simplificado, imagine que tenemos algún tipo de computadora que utiliza de forma nativa un tipo de datos decimales sin signo, llamémoslo float6d
. La longitud del tipo de datos es de 6 dígitos: 4 dedicados a la mantisa y 2 dedicados al exponente. Por ejemplo, el número 3.142 se puede expresar como
3.142 x 10^0
que se almacenaría en 6 dígitos como
503142
Los primeros dos dígitos son el exponente más 50, y los últimos cuatro son la mantisa. Este tipo de datos puede representar cualquier número desde 0.001 x 10^-50
hasta 9.999 x 10^+49
.
En realidad, eso no es cierto. No puede almacenar ningún número. ¿Qué pasa si quieres representar a 3.141592? O 3.1412034? O 3.141488906? Por suerte, el tipo de datos no puede almacenar más de cuatro dígitos de precisión, por lo que el compilador debe redondear cualquier cosa con más dígitos para ajustarse a las restricciones del tipo de datos. Si tú escribes
float6d x = 3.141592;
float6d y = 3.1412034;
float6d z = 3.141488906;
luego el compilador convierte cada uno de estos tres valores a la misma representación interna, 3.142 x 10^0
(que, recuerde, se almacena como 503142
), de modo que x == y == z
se mantendrá verdadero.
El punto es que hay toda una gama de números reales que se asignan a la misma secuencia subyacente de dígitos (o bits, en una computadora real). Específicamente, cualquier x
cumpla con 3.1415 <= x <= 3.1425
(suponiendo que el redondeo sea parejo) se convierte a la representación 503142
para el almacenamiento en la memoria.
Este redondeo ocurre cada vez que su programa almacena un valor de punto flotante en la memoria. La primera vez que sucede es cuando escribes una constante en tu código fuente, como hice anteriormente con x
, y
, y z
. Ocurre de nuevo cada vez que realiza una operación aritmética que aumenta el número de dígitos de precisión más allá de lo que puede representar el tipo de datos. Cualquiera de estos efectos se llama error de redondeo . Hay algunas maneras diferentes en que esto puede suceder:
Suma y resta: si uno de los valores que está agregando tiene un exponente diferente del otro, obtendrá dígitos adicionales de precisión, y si hay suficientes de ellos, será necesario eliminar los menos significativos. Por ejemplo, 2.718 y 121.0 son valores que pueden representarse exactamente en el tipo de datos
float6d
. Pero si intentas juntarlos:1.210 x 10^2 + 0.02718 x 10^2 ------------------- 1.23718 x 10^2
que se redondea a
1.237 x 10^2
, o 123.7, dejando caer dos dígitos de precisión.Multiplicación: el número de dígitos en el resultado es aproximadamente la suma del número de dígitos en los dos operandos. Esto producirá una cierta cantidad de error de redondeo, si sus operandos ya tienen muchos dígitos significativos. Por ejemplo, 121 x 2.718 te da
1.210 x 10^2 x 0.02718 x 10^2 ------------------- 3.28878 x 10^2
que se redondea a
3.289 x 10^2
, o 328.9, volviendo a caer dos dígitos de precisión.Sin embargo, es útil tener en cuenta que, si sus operandos son números "agradables", sin muchos dígitos significativos, el formato de punto flotante probablemente pueda representar el resultado exactamente, por lo que no tiene que lidiar con el error de redondeo. Por ejemplo, 2,3 x 140 da
1.40 x 10^2 x 0.23 x 10^2 ------------------- 3.22 x 10^2
Lo que no tiene problemas de redondeo.
División: aquí es donde las cosas se complican. La división casi siempre resultará en una cantidad de error de redondeo a menos que el número por el que se está dividiendo sea una potencia de la base (en cuyo caso la división es solo un desplazamiento de dígitos o un cambio de bit en binario). Como ejemplo, toma dos números muy simples, 3 y 7, divídelos y obtendrás
3. x 10^0 / 7. x 10^0 ---------------------------- 0.428571428571... x 10^0
El valor más cercano a este número que se puede representar como un
float6d
es4.286 x 10^-1
, o 0.4286, que difiere claramente del resultado exacto.
Como veremos en la siguiente sección, el error introducido al redondear aumenta con cada operación que realiza. Entonces, si está trabajando con números "buenos", como en su ejemplo, generalmente es mejor hacer las operaciones de la división lo más tarde posible, ya que esas son las operaciones que más probablemente introducen un error de redondeo en su programa donde no existía ninguno antes.
Análisis de error de redondeo.
En general, si no puede asumir que sus números son "buenos", el error de redondeo puede ser positivo o negativo, y es muy difícil predecir en qué dirección irá solo en función de la operación. Depende de los valores específicos involucrados. Mire esta gráfica del error de redondeo para 2.718 z
como una función de z
(aún utilizando el tipo de datos float6d
):
En la práctica, cuando trabaja con valores que utilizan la precisión total de su tipo de datos, a menudo es más fácil tratar el error de redondeo como un error aleatorio. Al observar la gráfica, es posible que pueda adivinar que la magnitud del error depende del orden de magnitud del resultado de la operación. En este caso particular, cuando z
es del orden 10 -1 , 2.718 z
también es del orden de 10 -1 , por lo que será un número de la forma 0.XXXX
. El error máximo de redondeo es entonces la mitad del último dígito de precisión; en este caso, por "el último dígito de precisión" quiero decir 0.0001, por lo que el error de redondeo varía entre -0.00005 y +0.00005. En el punto donde 2.718 z
salta hasta el siguiente orden de magnitud, que es 1 / 2.718 = 0.3679, puede ver que el error de redondeo también salta en un orden de magnitud.
Puede utilizar técnicas bien conocidas de análisis de errores para analizar cómo un error aleatorio (o impredecible) de cierta magnitud afecta su resultado. Específicamente, para la multiplicación o división, el error relativo "promedio" en su resultado se puede aproximar al agregar el error relativo en cada uno de los operandos en cuadratura , es decir, cuadrarlos, sumarlos y obtener la raíz cuadrada. Con nuestro tipo de datos float6d
, el error relativo varía entre 0.0005 (para un valor como 0.101) y 0.00005 (para un valor como 0.995).
Tomemos 0.0001 como un promedio aproximado del error relativo en los valores x
e y
. El error relativo en x * y
o x / y
es entonces dado por
sqrt(0.0001^2 + 0.0001^2) = 0.0001414
que es un factor de sqrt(2)
mayor que el error relativo en cada uno de los valores individuales.
Cuando se trata de combinar operaciones, puede aplicar esta fórmula varias veces, una vez por cada operación de punto flotante. Entonces, por ejemplo, para z / (x * y)
, el error relativo en x * y
es, en promedio, 0.0001414 (en este ejemplo decimal) y luego el error relativo en z / (x * y)
es
sqrt(0.0001^2 + 0.0001414^2) = 0.0001732
Observe que el error relativo promedio crece con cada operación, específicamente como la raíz cuadrada del número de multiplicaciones y divisiones que realiza.
De manera similar, para z / x * y
, el error relativo promedio en z / x
es 0.0001414, y el error relativo en z / x * y
es
sqrt(0.0001414^2 + 0.0001^2) = 0.0001732
Así, lo mismo, en este caso. Esto significa que para valores arbitrarios, en promedio, las dos expresiones introducen aproximadamente el mismo error . (En teoría, eso es. He visto que estas operaciones se comportan de manera muy diferente en la práctica, pero esa es otra historia).
Detalles sangrientos
Es posible que tenga curiosidad por el cálculo específico que presentó en la pregunta, no solo un promedio. Para ese análisis, vamos a pasar al mundo real de la aritmética binaria. Los números de punto flotante en la mayoría de los sistemas e idiomas se representan mediante el estándar 754 de IEEE . Para los números de 64 bits, el format especifica 52 bits dedicados a la mantisa, 11 al exponente y uno al signo. En otras palabras, cuando se escribe en la base 2, un número de punto flotante es un valor de la forma
1.1100000000000000000000000000000000000000000000000000 x 2^00000000010
52 bits 11 bits
El 1
inicial no se almacena explícitamente, y constituye un bit número 53. Además, debe tener en cuenta que los 11 bits almacenados para representar el exponente son en realidad el exponente real más 1023. Por ejemplo, este valor en particular es 7, que es 1.75 x 2 2 . La mantisa es 1.75 en binario, o 1.11
, y el exponente es 1023 + 2 = 1025 en binario, o 10000000001
, por lo que el contenido almacenado en la memoria es
01000000000111100000000000000000000000000000000000000000000000000
^ ^
exponent mantissa
pero eso realmente no importa.
Tu ejemplo también involucra 450,
1.1100001000000000000000000000000000000000000000000000 x 2^00000001000
y 60,
1.1110000000000000000000000000000000000000000000000000 x 2^00000000101
Puedes jugar con estos valores usando este convertidor o cualquiera de muchos otros en Internet.
Cuando calcula la primera expresión, 450/(7*60)
, el procesador primero realiza la multiplicación, obteniendo 420, o
1.1010010000000000000000000000000000000000000000000000 x 2^00000001000
Luego divide 450 por 420. Esto produce 15/14, que es
1.0001001001001001001001001001001001001001001001001001001001001001001001...
en binario. Ahora, la especificación del lenguaje Java dice que
Los resultados inexactos deben redondearse al valor representable más cercano al resultado infinitamente preciso; si los dos valores representables más cercanos están igualmente cerca, se elige el que tenga el bit cero menos significativo. Este es el modo de redondeo predeterminado del estándar IEEE 754 conocido como redondeo al más cercano.
y el valor representable más cercano a 15/14 en formato IEEE 754 de 64 bits es
1.0001001001001001001001001001001001001001001001001001 x 2^00000000000
que es aproximadamente 1.0714285714285714
en decimal. (Más precisamente, este es el valor decimal menos preciso que especifica de manera única esta representación binaria en particular).
Por otro lado, si calcula 450/7 primero, el resultado es 64.2857142857 ..., o en binario,
1000000.01001001001001001001001001001001001001001001001001001001001001001...
para el cual el valor representable más cercano es
1.0000000100100100100100100100100100100100100100100101 x 2^00000000110
que es 64.28571428571429180465 ... Observe el cambio en el último dígito de la mantisa binaria (en comparación con el valor exacto) debido a un error de redondeo. Dividiendo esto por 60 te da
1.000100100100100100100100100100100100100100100100100110011001100110011...
Mira el final: el patrón es diferente! Es 0011
que se repite, en lugar de 001
como en el otro caso. El valor representable más cercano es
1.0001001001001001001001001001001001001001001001001010 x 2^00000000000
que difiere del otro orden de operaciones en los últimos dos bits: son 10
lugar de 01
. El equivalente decimal es 1.0714285714285716.
El redondeo específico que causa esta diferencia debería quedar claro si observa los valores binarios exactos:
1.0001001001001001001001001001001001001001001001001001001001001001001001...
1.0001001001001001001001001001001001001001001001001001100110011001100110...
^ last bit of mantissa
Resulta en este caso que el resultado anterior, numéricamente 15/14, resulta ser la representación más precisa del valor exacto. Este es un ejemplo de cómo salir de la división hasta el final te beneficia. Pero, de nuevo, esta regla solo se mantiene mientras los valores con los que está trabajando no utilicen la precisión completa del tipo de datos. Una vez que comience a trabajar con valores inexactos (redondeados), ya no se protegerá de más errores de redondeo haciendo las multiplicaciones primero.