2010-05-27 14 views
9

Así que estoy tratando de averiguar por qué el operador de módulo está devolviendo un valor inusual tan grande.Aritmética de coma flotante - Operador de módulo en el tipo doble

Si tengo el código:

double result = 1.0d % 0.1d;

que dará un resultado de 0.09999999999999995. Yo esperaría un valor de 0

Nota: este problema no existe con el operador de división - double result = 1.0d/0.1d;

dará un resultado de 10.0, lo que significa que el resto debe ser 0.

Déjeme ser claro: no me sorprende que exista un error, me sorprende que el error sea tan grande en comparación con los números en juego. 0.0999 ~ = 0.1 y 0.1 tiene el mismo orden de magnitud que 0.1d y solo un orden de magnitud de 1.0d. No es como se puede comparar con un double.epsilon, o decir "es igual si es < 0,00001 diferencia".

He leído sobre este tema en StackOverflow, en las siguientes publicaciones onetwothree, entre otros.

¿Alguien puede sugerir que explique por qué este error es tan grande? Cualquier sugerencia para evitar tener problemas en el futuro (sé que podría usar decimal en su lugar, pero me preocupa el rendimiento de eso).

Editar: Debo señalar específicamente que sé que es un 0,1 infinitely repeating series of numbers in binary - ¿Tiene eso algo que ver con eso?

Respuesta

14

El error se produce porque un doble no puede representar exactamente 0.1 - lo más cercano que puede representar es algo así como 0.100000000000000005551115123126. Ahora, cuando divides 1.0 por eso, te da un número ligeramente inferior a 10, pero una vez más, un doble no puede representarlo exactamente, por lo que termina redondeando hasta 10. Pero cuando haces la modificación, puede darte un poco menos de 0.1 residuo.

desde 0 = 0.1 mod 0.1, el error real en el mod es 0.1 - 0.09999999 ... - muy pequeño.

Si agrega el resultado del operador% a 9 * 0.1, le dará 1.0 de nuevo.

Editar

Un poco más de detalle en las cosas redondeo - especialmente en lo que este problema es un buen ejemplo de los peligros de la precisión mixta.

La manera en que a % b se calcula para números flotantes es como a - (b * floor(a/b)). El problema es que puede hacerse todo de una vez con más precisión interna de la que obtendría con esas operaciones (y redondeando el resultado a un número fp en cada etapa), por lo que podría darle un resultado diferente. Un ejemplo que mucha gente ve es que el hardware Intel x86/x87 usa una precisión de 80 bits para cálculos intermedios y solo una precisión de 64 bits para los valores en la memoria. Entonces el valor en b en la ecuación anterior proviene de la memoria y es un número fp de 64 bits que no es exactamente 0.1 (gracias dan04 para el valor exacto), entonces cuando calcula 1.0/0.1 obtiene 9.99999999999999944488848768742172978818416595458984375 (redondeado a 80 bits) Ahora, si lo redondeas a 64 bits, sería 10.0, pero si mantienes los 80 bits internos y haces uso de la palabra, truncará a 9.0 y obtendrá así .0999999999999990000399638918679556809365749359130859375 como respuesta final.

En este caso, está viendo un gran error aparente porque está utilizando una función de paso no continuo (piso) lo que significa que una diferencia muy pequeña en un valor interno puede empujarlo sobre el paso. Pero dado que el mod es en sí mismo una función escalonada no continua, el error real aquí es 0.1-0.0999 ... ya que 0.1 es el punto discontinuo en el rango de la función mod.

+0

0.1000000000000000055511151231257827021181583404541015625, para ser exactos. – dan04

+0

Aprecio el recorrido del cálculo. Una aclaración: mencionas "desde 0 = 0.1 mod 0.1, el error real en el mod es 0.1 - 0.09999999 ... - muy pequeño". Estoy confundido porque mi resultado fue 0.099999, pero el resultado real es 0. ¿Cómo es el error real 0.1 - 0.099999? ¿Puede desglosar el cálculo de error de esta parte con un poco más de detalle? – CrimsonX

+0

para responder el comentario anterior para futuros lectores: la sección Editar lo responde. (breve explicación: el error es grande debido a la inestabilidad del mod (función de abolladuras) un ligero aumento en 'a' (por ejemplo, 10.0001) hará que el resultado caiga a cerca de 0) –

2

No es precisamente un "error" en el cálculo, pero el hecho de que nunca tuvo realmente 0.1 para empezar.

El problema es que 1.0 puede representarse exactamente en coma flotante binario pero 0.1 no puede, porque no puede construirse exactamente a partir de potencias negativas de dos. (Es 1/16 + 1/32 + ...)

Así que no está obteniendo realmente 1.0% 0.1, la máquina debe calcular 1.0% 0.1 + - 0.00 ... y luego informa honestamente obtuvo como resultado ...

Para tener un gran resto, supongo que el segundo operando de % debe haber sido ligeramente superior a 0.1, impidiendo la división final, y dando como resultado que casi el 0.1 es el resultado de la operacion.

+0

Pero ¿por qué sería del 1,0% al 0,1 espectáculo de este error y 1.0/0.1 no muestra el error? – CrimsonX

+0

También creo que quiere decir que "la máquina se deja calcular 1.0% 0.0999 ..." – CrimsonX

+0

La razón por la que 1.0/0.1 sale "bien" es porque 1.0/.0999 ... y 1.0/0.1 son casi iguales - el la función es continua. El cálculo se realiza con unos pocos bits adicionales, el resultado representable final es el mismo en ambos casos. Luego parece "correcto" en base a que esperamos que la respuesta a 1.0/exactamente_0.1 sea 10. Pero recuerde que el problema presentado a la máquina no era 1.0% 0.1 sino 1.0% 0.10000000001 o algo así y este cálculo realmente da como resultado un gran resto que no se puede redondear. – DigitalRoss

2

El hecho de que 0.1 no se puede representar exactamente en binario tiene todo que ver con eso.

Si 0,1 podría representarse como double, obtendría el doble representable más cercano (suponiendo el modo de redondeo "más cercano") al resultado real de la operación que desea calcular.

Como no puede hacerlo, obtiene el doble representable más cercano a una operación que es completamente diferente de la que estaba tratando de calcular.

También tenga en cuenta que/es una función principalmente continua (una pequeña diferencia en los argumentos generalmente significa una pequeña diferencia en el resultado, y mientras la derivada puede ser grande cerca pero en un mismo lado de cero, al menos precisión adicional para los argumentos ayudan). Por otro lado,% no es continuo: cualquiera que sea la precisión que elija, siempre habrá argumentos para los cuales un error de representación arbitrariamente pequeño en el primer argumento significa un gran error en el resultado.

La forma en que se especifica IEEE 754, solo obtiene garantías sobre la aproximación del resultado de una operación de punto flotante asumiendo que los argumentos son exactamente lo que quiere. Si los argumentos no son exactamente lo que quiere, debe cambiar a otras soluciones, como la aritmética de intervalos o el análisis de la buena condición de su programa (si usa% en números de coma flotante, es probable que no sea así). -acondicionado).

0

El error que ves es pequeño; solo se ve grande a primera vista. Su resultado (después del redondeo de la pantalla) fue 0.09999999999999995 == (0.1 - 5e-17) cuando esperaba 0 de una operación % 0.1. Pero recuerde que esto es casi 0.1, y 0.1 % 0.1 == 0.

Así que su error real aquí es -5e-17. Yo llamaría esto pequeño.

Dependiendo de lo que necesita el número de, tal vez sea mejor escribir:

double result = 1.0 % 0.1; result = result >= 0.1/2 ? result - 0.1 : result;

Cuestiones relacionadas