¿Por qué agregar 0.1 múltiples veces permanece sin pérdidas?

152

Sé que el 0.1número decimal no se puede representar exactamente con un número binario finito ( explicación ), por double n = 0.1lo que perderá algo de precisión y no será exactamente0.1 . Por otro lado 0.5se puede representar exactamente porque lo es 0.5 = 1/2 = 0.1b.

Habiendo dicho eso, es comprensible que sumar 0.1 tres veces no dé exactamente, 0.3por lo que se imprime el siguiente código false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Pero entonces, ¿cómo es que sumar 0.1 cinco veces dará exactamente 0.5? Se imprime el siguiente código true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Si 0.1no se puede representar exactamente, ¿cómo es que sumarlo 5 veces da exactamente lo 0.5que se puede representar con precisión?

icza
fuente
77
Si realmente lo investigas, estoy seguro de que puedes resolverlo, pero el punto flotante está cargado de "sorpresas", y a veces es mejor mirar con asombro.
Hot Licks
3
Estás pensando en esto de una manera matemática. La aritmética de coma flotante no es matemática de ninguna manera.
Jakob
13
@HotLicks es una actitud muy equivocada.
hobbs
2
@RussellBorogove, incluso si se optimizara, solo sería una optimización válida si sumtuviera el mismo valor final que si el bucle se ejecutara realmente. En el estándar C ++, esto se denomina "regla as-if" o "mismo comportamiento observable".
hobbs
77
@Jakob no es cierto en absoluto. La aritmética de punto flotante se define rigurosamente, con un buen tratamiento matemático de los límites de error y tal. Es solo que muchos programadores no están dispuestos a seguir el análisis o creen erróneamente que "el punto flotante es inexacto" es todo lo que hay que saber y que no vale la pena molestarse en el análisis.
hobbs

Respuestas:

155

El error de redondeo no es aleatorio y la forma en que se implementa intenta minimizar el error. Esto significa que a veces el error no es visible, o no hay error.

Por ejemplo, 0.1no es exactamente 0.1es decir, new BigDecimal("0.1") < new BigDecimal(0.1)pero 0.5es exactamente1.0/2

Este programa le muestra los verdaderos valores involucrados.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

huellas dactilares

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Nota: eso 0.3está ligeramente desactivado, pero cuando llegas a 0.4los bits tienes que cambiar uno hacia abajo para que quepa en el límite de 53 bits y el error se descarta. Una vez más, un arrastra de error de vuelta en para 0.6y 0.7pero para 0.8que 1.0se descarta el error.

Agregarlo 5 veces debería acumular el error, no cancelarlo.

La razón por la que hay un error se debe a la precisión limitada. es decir, 53 bits. Esto significa que a medida que el número usa más bits a medida que se hace más grande, los bits deben eliminarse al final. Esto provoca un redondeo que en este caso está a su favor.
Puede obtener el efecto contrario al obtener un número menor, por ejemplo, 0.1-0.0999=>1.0000000000000286E-4 y ve más errores que antes.

Un ejemplo de esto es por qué en Java 6 ¿Por qué Math.round (0.49999999999999994) devuelve 1? En este caso, la pérdida de un bit en el cálculo resulta en una gran diferencia en la respuesta.

Peter Lawrey
fuente
1
¿Dónde se implementa esto?
EpicPandaForce
16
@Zhuinden La CPU sigue el estándar IEEE-754. Java le da acceso a las instrucciones subyacentes de la CPU y no se involucra. en.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey
10
@ PeterLawrey: No necesariamente la CPU. En una máquina sin coma flotante en la CPU (y sin FPU separada en uso), la aritmética IEEE se realizará mediante software. Y si la CPU host ha de coma flotante, pero no se ajusta a los requisitos de IEEE, creo que una aplicación Java para que la CPU estaría obligado a utilizar flotador suave también ...
R .. GitHub dejar de ayudar a ICE
1
@R .. en cuyo caso no sé qué pasaría si usaras strictfp Time para considerar enteros de punto fijo, creo. (o BigDecimal)
Peter Lawrey
2
@eugene el problema clave son los valores limitados que puede representar el punto flotante. Esta limitación puede provocar una pérdida de información y, a medida que el número crece, se produce una pérdida de error. Utiliza el redondeo, pero en este caso, redondea hacia abajo, por lo que lo que hubiera sido un número que es demasiado grande como 0.1 es demasiado grande, se convierte en el valor correcto. Exactamente 0.5
Peter Lawrey 01 de
47

El desbordamiento de restricción, en coma flotante, x + x + xes exactamente el número de coma flotante correctamente redondeado (es decir, el más cercano) al 3 * real x, x + x + x + xes exactamente 4 * xy x + x + x + x + xnuevamente es la aproximación de coma flotante correctamente redondeada para 5 * x.

El primer resultado, para x + x + x, deriva del hecho de que x + xes exacto. x + x + xes, por lo tanto, el resultado de un solo redondeo.

El segundo resultado es más difícil, una demostración de esto se discute aquí (y Stephen Canon alude a otra prueba por análisis de caso en los últimos 3 dígitos de x). Para resumir, 3 * xestá en la misma binada que 2 * xo está en la misma binada que 4 * x, y en cada caso es posible deducir que el error en la tercera adición cancela el error en la segunda adición (el La primera adición es exacta, como ya dijimos).

El tercer resultado, " x + x + x + x + xestá correctamente redondeado", deriva del segundo de la misma manera que el primero deriva de la exactitud de x + x.


El segundo resultado explica por qué 0.1 + 0.1 + 0.1 + 0.1es exactamente el número de coma flotante 0.4: los números racionales 1/10 y 4/10 se aproximan de la misma manera, con el mismo error relativo, cuando se convierten en coma flotante. Estos números de punto flotante tienen una relación de exactamente 4 entre ellos. El primero y tercer resultado muestran que 0.1 + 0.1 + 0.1y 0.1 + 0.1 + 0.1 + 0.1 + 0.1se puede esperar que tengan menor error que podría inferirse por análisis de error ingenuo, pero, en sí mismos, que sólo se refieren los resultados, respectivamente, 3 * 0.1y 5 * 0.1, lo que se puede esperar para estar cerca pero no necesariamente idéntica a 0.3y 0.5.

Si sigue sumando 0.1después de la cuarta adición, finalmente observará errores de redondeo que hacen que " 0.1agregado a sí mismo n veces" difiera en binario durante un tiempo. Después de eso, la absorción comenzaría a tener lugar y la curva se volvería plana.n * 0.1 , y diverja aún más de n / 10. Si tuviera que trazar los valores de "0.1 agregado a sí mismo n veces" en función de n, observaría líneas de pendiente constante por binadas (tan pronto como el resultado de la enésima suma esté destinado a caer en una binada particular, Se puede esperar que las propiedades de la adición sean similares a las adiciones anteriores que produjeron un resultado en la misma binada). Dentro de una misma binada, el error crecerá o disminuirá. Si observara la secuencia de las pendientes de binade a binade, reconocería los dígitos repetidos de0.1

Pascal Cuoq
fuente
1
En la primera línea estás diciendo que x + x + x es exactamente correcto, pero del ejemplo en la pregunta no lo es.
Alboz
2
@Alboz Digo que x + x + xes exactamente el número de punto flotante correctamente redondeado al 3 * real x. "Correctamente redondeado" significa "más cercano" en este contexto.
Pascal Cuoq
44
+1 Esta debería ser la respuesta aceptada. En realidad, ofrece explicaciones / pruebas de lo que está sucediendo en lugar de solo generalidades vagas.
R .. GitHub DEJA DE AYUDAR AL HIELO
1
@Alboz (todo lo que está previsto por la pregunta). Pero lo que explica esta respuesta es cómo los errores se cancelan fortuitamente en lugar de sumar en el peor de los casos.
hobbs
1
@chebus 0.1 es 0x1.999999999999999999999 ... p-4 en hexadecimal (una secuencia infinita de dígitos). Se aproxima en doble precisión como 0x1.99999ap-4. 0.2 es 0x1.999999999999999999999 ... p-3 en hexadecimal. Por la misma razón que 0.1 se aproxima a 0x1.99999ap-4, 0.2 se aproxima a 0x1.99999ap-3. Mientras tanto, 0x1.99999ap-3 también es exactamente 0x1.99999ap-4 + 0x1.99999ap-4.
Pascal Cuoq
-1

Los sistemas de punto flotante hacen varias magias, incluyendo tener algunos bits adicionales de precisión para redondear. Por lo tanto, el error muy pequeño debido a la representación inexacta de 0.1 termina siendo redondeado a 0.5.

Piense en el punto flotante como una excelente pero INEXACTA forma de representar números. No todos los números posibles se representan fácilmente en una computadora. Números irracionales como PI. O como SQRT (2). (Los sistemas matemáticos simbólicos pueden representarlos, pero dije "fácilmente").

El valor de coma flotante puede ser extremadamente cercano, pero no exacto. Puede estar tan cerca que podría navegar a Plutón y estar en milímetros. Pero aún no es exacto en un sentido matemático.

No use coma flotante cuando necesite ser exacto en lugar de aproximado. Por ejemplo, las aplicaciones de contabilidad desean realizar un seguimiento exacto de un cierto número de centavos en una cuenta. Los enteros son buenos para eso porque son exactos. El problema principal que debe tener en cuenta con los enteros es el desbordamiento.

Usar BigDecimal para la moneda funciona bien porque la representación subyacente es un número entero, aunque grande.

Reconociendo que los números de coma flotante son inexactos, todavía tienen muchos usos. Sistemas de coordenadas para navegación o coordenadas en sistemas gráficos. Valores astronómicos. Valores científicos (De todos modos, es probable que no se pueda conocer la masa exacta de una pelota de béisbol dentro de una masa de un electrón, por lo que la inexactitud realmente no importa).

Para contar aplicaciones (incluida la contabilidad) use integer. Para contar la cantidad de personas que pasan por una puerta, use int o long.

DannyB
fuente
2
La pregunta está etiquetada [java]. La definición del lenguaje Java no tiene provisión para "unos pocos bits adicionales de precisión", solo para unos pocos bits de exponente adicionales (y eso es solo si no se usa strictfp). El hecho de que haya renunciado a comprender algo no significa que sea insondable ni que otros deban renunciar a comprenderlo. Consulte stackoverflow.com/questions/18496560 como ejemplo de las longitudes a las que irán las implementaciones de Java para implementar la definición del lenguaje (que no incluye ninguna disposición para bits de precisión adicionales ni, con strictfp, para ningún bit de exp extra)
Pascal Cuoq