ventajas una ordenada listas lista ligadas hacer enlazadas enlazada ejercicios ejemplos doblemente como circulares java double rounding double-precision order-of-evaluation

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

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 es 4.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.