¿Se han roto las matemáticas de coma flotante?

2983

Considere el siguiente código:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

¿Por qué ocurren estas imprecisiones?

Cato Johnston
fuente
127
Las variables de punto flotante suelen tener este comportamiento. Es causado por cómo se almacenan en el hardware. Para obtener más información, consulte el artículo de Wikipedia sobre números de punto flotante .
Ben S
6262
JavaScript trata los decimales como números de coma flotante , lo que significa que operaciones como la suma pueden estar sujetas a errores de redondeo. Es posible que desee leer este artículo: Lo que todo informático debe saber sobre la aritmética de coma flotante
mate b
44
Solo para información, TODOS los tipos numéricos en javascript son IEEE-754 Dobles.
Gary Willoughby
66
Como JavaScript utiliza el estándar IEEE 754 para matemáticas, utiliza números flotantes de 64 bits . Esto causa errores de precisión al hacer cálculos de coma flotante (decimal), en resumen, debido a que las computadoras trabajan en Base 2 mientras que decimal es Base 10 .
Pardeep Jain

Respuestas:

2253

La matemática de punto flotante binario es así. En la mayoría de los lenguajes de programación, se basa en el estándar IEEE 754 . El quid del problema es que los números se representan en este formato como un número entero multiplicado por una potencia de dos; los números racionales (como 0.1, cuál es 1/10) cuyo denominador no es una potencia de dos no pueden representarse exactamente.

En 0.1el binary64formato estándar , la representación se puede escribir exactamente como

  • 0.1000000000000000055511151231257827021181583404541015625 en decimal, o
  • 0x1.999999999999ap-4en notación C99 hexfloat .

En contraste, el número racional 0.1, que es 1/10, puede escribirse exactamente como

  • 0.1 en decimal, o
  • 0x1.99999999999999...p-4en un análogo de notación de flotación hexagonal C99, donde ...representa una secuencia interminable de 9.

Las constantes 0.2y 0.3en su programa también serán aproximaciones a sus valores verdaderos. Sucede que el más cercano doublea 0.2es más grande que el número racional 0.2pero que el más cercano doublea 0.3es más pequeño que el número racional 0.3. La suma 0.1y 0.2termina siendo mayor que el número racional 0.3y, por lo tanto, no está de acuerdo con la constante en su código.

Un tratamiento bastante completo de los problemas aritméticos de punto flotante es lo que todo informático debe saber sobre la aritmética de punto flotante . Para una explicación más fácil de digerir, vea floating-point-gui.de .

Nota al margen: todos los sistemas de números posicionales (base-N) comparten este problema con precisión

Los números decimales antiguos simples (base 10) tienen los mismos problemas, por lo que números como 1/3 terminan en 0.333333333 ...

Acaba de toparse con un número (3/10) que resulta fácil de representar con el sistema decimal, pero que no se ajusta al sistema binario. También va en ambos sentidos (en algún grado pequeño): 1/16 es un número feo en decimal (0.0625), pero en binario se ve tan ordenado como un 10,000 en decimal (0.0001) ** - si estuviéramos en Con el hábito de usar un sistema de números de base 2 en nuestra vida diaria, incluso miraría ese número y comprendería instintivamente que podría llegar allí reduciendo a la mitad algo, reduciéndolo a la mitad una y otra vez.

** Por supuesto, esa no es exactamente la forma en que los números de punto flotante se almacenan en la memoria (usan una forma de notación científica). Sin embargo, ilustra el punto de que los errores de precisión de punto flotante binario tienden a surgir porque los números del "mundo real" con los que generalmente estamos interesados ​​en trabajar son a menudo potencias de diez, pero solo porque usamos un día de sistema de números decimales. hoy. Esta es también la razón por la que diremos cosas como 71% en lugar de "5 de cada 7" (71% es una aproximación, ya que 5/7 no se puede representar exactamente con ningún número decimal).

Entonces no: los números binarios de coma flotante no están rotos, simplemente resultan ser tan imperfectos como cualquier otro sistema de números base-N :)

Nota lateral: Trabajar con flotadores en la programación

En la práctica, este problema de precisión significa que necesita usar funciones de redondeo para redondear sus números de coma flotante a la cantidad de decimales que le interese antes de mostrarlos.

También debe reemplazar las pruebas de igualdad con comparaciones que permitan cierta tolerancia, lo que significa:

No no hacerif (x == y) { ... }

En cambio hazlo if (abs(x - y) < myToleranceValue) { ... }.

donde abses el valor absoluto myToleranceValuedebe elegirse para su aplicación en particular, y tendrá mucho que ver con la cantidad de "margen de maniobra" que está dispuesto a permitir, y cuál puede ser el mayor número que va a comparar (debido a problemas de pérdida de precisión ) Tenga cuidado con las constantes de estilo "epsilon" en su idioma de elección. Estos son no para ser utilizados como valores de tolerancia.

Daniel Scott
fuente
181
Creo que "alguna constante de error" es más correcta que "The Epsilon" porque no hay "The Epsilon" que pueda usarse en todos los casos. Se deben usar diferentes épsilons en diferentes situaciones. Y la máquina épsilon casi nunca es una buena constante de usar.
Rotsor
34
No es del todo cierto que todas las matemáticas de punto flotante se basen en el estándar IEEE [754]. Todavía hay algunos sistemas en uso que tienen el antiguo FP hexadecimal de IBM, por ejemplo, y todavía hay tarjetas gráficas que no son compatibles con la aritmética IEEE-754. Sin embargo, es fiel a una aproximación razonable.
Stephen Canon
19
Cray abandonó el cumplimiento de IEEE-754 para la velocidad. Java también aflojó su adhesión como una optimización.
Art Taylor
28
Creo que debería agregar algo a esta respuesta sobre cómo los cálculos sobre el dinero siempre deben hacerse, siempre con la aritmética de punto fijo en los enteros , porque el dinero está cuantificado. (Puede tener sentido hacer cálculos contables internos en pequeñas fracciones de centavo, o sea cual sea su unidad monetaria más pequeña; esto a menudo ayuda, por ejemplo, a reducir el error de redondeo al convertir "$ 29.99 al mes" a una tasa diaria, pero debería sigue siendo aritmética de punto fijo.)
zwol
18
Hecho interesante: este 0.1 no exactamente representado en coma flotante binaria causó un infame error de software de misiles Patriot que resultó en 28 personas muertas durante la primera guerra de Irak.
hdl
603

La perspectiva de un diseñador de hardware

Creo que debería agregar la perspectiva de un diseñador de hardware a esto ya que diseño y construyo hardware de punto flotante. Conocer el origen del error puede ayudar a comprender lo que está sucediendo en el software y, en última instancia, espero que esto ayude a explicar las razones por las cuales los errores de coma flotante ocurren y parecen acumularse con el tiempo.

1. Información general

Desde una perspectiva de ingeniería, la mayoría de las operaciones de punto flotante tendrán algún elemento de error, ya que el hardware que realiza los cálculos de punto flotante solo debe tener un error de menos de la mitad de una unidad en el último lugar. Por lo tanto, gran parte del hardware se detendrá con una precisión que solo es necesaria para generar un error de menos de la mitad de una unidad en el último lugar para una sola operación, lo cual es especialmente problemático en la división de coma flotante. Lo que constituye una sola operación depende de cuántos operandos tome la unidad. Para la mayoría, son dos, pero algunas unidades toman 3 o más operandos. Debido a esto, no hay garantía de que las operaciones repetidas den como resultado un error deseable ya que los errores se acumulan con el tiempo.

2. Normas

La mayoría de los procesadores siguen el estándar IEEE-754 , pero algunos usan estándares desnormalizados o diferentes. Por ejemplo, hay un modo desnormalizado en IEEE-754 que permite la representación de números de coma flotante muy pequeños a expensas de la precisión. Sin embargo, lo siguiente cubrirá el modo normalizado de IEEE-754, que es el modo de operación típico.

En el estándar IEEE-754, a los diseñadores de hardware se les permite cualquier valor de error / épsilon siempre que sea menos de la mitad de una unidad en el último lugar, y el resultado solo debe ser menos de la mitad de una unidad en el último Lugar para una operación. Esto explica por qué cuando hay operaciones repetidas, los errores se suman. Para la precisión doble IEEE-754, este es el 54 ° bit, ya que 53 bits se utilizan para representar la parte numérica (normalizada), también llamada mantisa, del número de coma flotante (por ejemplo, el 5.3 en 5.3e5). Las siguientes secciones dan más detalles sobre las causas del error de hardware en varias operaciones de coma flotante.

3. Causa del error de redondeo en la división

La causa principal del error en la división de coma flotante son los algoritmos de división utilizados para calcular el cociente. La mayoría de los sistemas informáticos calculan la división usando la multiplicación por un inverso, principalmente en Z=X/Y,Z = X * (1/Y). Una división se calcula iterativamente, es decir, cada ciclo calcula algunos bits del cociente hasta que se alcanza la precisión deseada, lo que para IEEE-754 es cualquier cosa con un error de menos de una unidad en el último lugar. La tabla de recíprocos de Y (1 / Y) se conoce como la tabla de selección de cociente (QST) en la división lenta, y el tamaño en bits de la tabla de selección de cociente suele ser el ancho de la raíz, o un número de bits de El cociente calculado en cada iteración, más algunos bits de protección. Para el estándar IEEE-754, doble precisión (64 bits), sería el tamaño de la raíz del divisor, más unos pocos bits de protección k, donde k>=2. Entonces, por ejemplo, una tabla de selección de cociente típica para un divisor que calcula 2 bits del cociente a la vez (radix 4) sería 2+2= 4bits (más algunos bits opcionales).

3.1 Error de redondeo de división: aproximación de recíproco

Los recíprocos en la tabla de selección de cociente dependen del método de división : división lenta como la división SRT o división rápida como la división Goldschmidt; cada entrada se modifica de acuerdo con el algoritmo de división en un intento de producir el error más bajo posible. En cualquier caso, sin embargo, todos los recíprocos son aproximaciones.del recíproco real e introducir algún elemento de error. Los métodos de división lenta y división rápida calculan el cociente de forma iterativa, es decir, se calcula un cierto número de bits del cociente en cada paso, luego el resultado se resta del dividendo y el divisor repite los pasos hasta que el error sea inferior a la mitad de uno Unidad en último lugar. Los métodos de división lenta calculan un número fijo de dígitos del cociente en cada paso y generalmente son menos costosos de construir, y los métodos de división rápida calculan un número variable de dígitos por paso y generalmente son más costosos de construir. La parte más importante de los métodos de división es que la mayoría de ellos se basan en la multiplicación repetida por una aproximación de un recíproco, por lo que son propensos a errores.

4. Errores de redondeo en otras operaciones: truncamiento

Otra causa de los errores de redondeo en todas las operaciones son los diferentes modos de truncamiento de la respuesta final que permite IEEE-754. Hay truncar, redondear hacia cero, redondear al más cercano (predeterminado), redondear hacia abajo y redondear hacia arriba. Todos los métodos introducen un elemento de error de menos de una unidad en el último lugar para una sola operación. Con el tiempo y las operaciones repetidas, el truncamiento también se suma acumulativamente al error resultante. Este error de truncamiento es especialmente problemático en la exponenciación, que implica alguna forma de multiplicación repetida.

5. Operaciones repetidas

Dado que el hardware que realiza los cálculos de coma flotante solo necesita obtener un resultado con un error de menos de la mitad de una unidad en el último lugar para una sola operación, el error crecerá en operaciones repetidas si no se observa. Esta es la razón por la que en los cálculos que requieren un error acotado, los matemáticos usan métodos como el uso del dígito par redondeado al más cercano en el último lugar de IEEE-754, porque, con el tiempo, es más probable que los errores se cancelen entre sí. out, y Aritmética de intervalo combinada con variaciones de los modos de redondeo IEEE 754para predecir errores de redondeo y corregirlos. Debido a su bajo error relativo en comparación con otros modos de redondeo, redondear al dígito par más cercano (en último lugar), es el modo de redondeo predeterminado de IEEE-754.

Tenga en cuenta que el modo de redondeo predeterminado, redondear al dígito par más cercano en el último lugar , garantiza un error de menos de la mitad de una unidad en el último lugar para una operación. El uso del truncamiento, redondeo y redondeo solo puede dar como resultado un error que es mayor que la mitad de una unidad en el último lugar, pero menor que una unidad en el último lugar, por lo que estos modos no se recomiendan a menos que sean utilizado en aritmética de intervalos.

6. Resumen

En resumen, la razón fundamental de los errores en las operaciones de coma flotante es una combinación del truncamiento en el hardware y el truncamiento de un recíproco en el caso de la división. Dado que el estándar IEEE-754 solo requiere un error de menos de la mitad de una unidad en el último lugar para una sola operación, los errores de coma flotante sobre operaciones repetidas se sumarán a menos que se corrijan.

KernelPanik
fuente
8
(3) está mal. El error de redondeo en una división no es menos de una unidad en el último lugar, sino como máximo media unidad en el último lugar.
gnasher729
66
@ gnasher729 Buena captura. La mayoría de las operaciones básicas también tienen un error de menos de la mitad de una unidad en el último lugar usando el modo de redondeo IEEE predeterminado. Editó la explicación y también notó que el error puede ser mayor que 1/2 de una ulp pero menor que 1 ulp si el usuario anula el modo de redondeo predeterminado (esto es especialmente cierto en los sistemas integrados).
KernelPanik
39
(1) Los números de coma flotante no tienen error. Cada valor de coma flotante es exactamente lo que es. La mayoría (pero no todas) las operaciones de coma flotante dan resultados inexactos. Por ejemplo, no hay un valor de punto flotante binario que sea exactamente igual a 1.0 / 10.0. Algunas operaciones (por ejemplo, 1,0 + 1,0) no dan resultados exactos en el otro lado.
Solomon Slow
19
"La causa principal del error en la división de coma flotante, son los algoritmos de división utilizados para calcular el cociente" es algo muy engañoso que decir. Para una división conforme a IEEE-754, la única causa de error en la división de punto flotante es la incapacidad del resultado para representarse exactamente en el formato de resultado; Se calcula el mismo resultado independientemente del algoritmo utilizado.
Stephen Canon
66
@ Matt Perdón por la respuesta tardía. Básicamente se debe a problemas de recursos / tiempo y compensaciones. Hay una manera de hacer una división larga / más división 'normal', se llama División SRT con radix dos. Sin embargo, esto cambia y resta repetidamente el divisor del dividendo y toma muchos ciclos de reloj, ya que solo calcula un bit del cociente por ciclo de reloj. Utilizamos tablas de recíprocos para poder calcular más bits del cociente por ciclo y realizar compensaciones efectivas de rendimiento / velocidad.
KernelPanik
463

Cuando convierte .1 o 1/10 a base 2 (binario) obtiene un patrón repetitivo después del punto decimal, al igual que tratar de representar 1/3 en base 10. El valor no es exacto y, por lo tanto, no puede hacerlo matemática exacta con él utilizando métodos normales de coma flotante.

Joel Coehoorn
fuente
133
Gran y corta respuesta. La repetición del patrón parece 0.00011001100110011001100110011001100110011001100110011 ...
Konstantin Chernov
44
Esto no explica por qué no se utiliza un mejor algoritmo que no se convierta en binarios en primer lugar.
Dmitri Zaitsev
12
Porque el rendimiento. Usar binario es unos miles de veces más rápido, porque es nativo para la máquina.
Joel Coehoorn
77
Hay métodos que producen valores decimales exactos. BCD (decimal codificado en binario) u otras formas de número decimal. Sin embargo, estos son más lentos (MUCHO más lento) y requieren más almacenamiento que el uso de punto flotante binario. (como ejemplo, el BCD empaquetado almacena 2 dígitos decimales en un byte. Esos son 100 valores posibles en un byte que en realidad puede almacenar 256 valores posibles, o 100/256, que desperdicia aproximadamente el 60% de los valores posibles de un byte)
Duncan C
16
@Jacksonkr todavía estás pensando en base-10. Las computadoras son de base 2.
Joel Coehoorn
307

La mayoría de las respuestas aquí abordan esta pregunta en términos técnicos muy secos. Me gustaría abordar esto en términos que los seres humanos normales puedan entender.

Imagine que está tratando de cortar pizzas. Tiene un cortador de pizza robótico que puede cortar rebanadas de pizza exactamente por la mitad. Puede reducir a la mitad una pizza entera, o puede reducir a la mitad una rebanada existente, pero en cualquier caso, la reducción a la mitad siempre es exacta.

Ese cortador de pizza tiene movimientos muy finos, y si comienza con una pizza completa, luego divida en dos y continúe dividiendo la porción más pequeña cada vez, puede hacer la reducción a la mitad 53 veces antes de que la porción sea demasiado pequeña incluso para sus habilidades de alta precisión . En ese punto, ya no puede reducir a la mitad ese corte muy delgado, sino que debe incluirlo o excluirlo tal como está.

Ahora, ¿cómo dividiría todas las rebanadas de tal manera que sumen una décima parte (0.1) o una quinta parte (0.2) de una pizza? Realmente piense en ello e intente resolverlo. Incluso puede intentar usar una pizza real, si tiene a mano un cortador de pizza de precisión mítica. :-)


Los programadores más experimentados, por supuesto, conocen la respuesta real, que es que no hay forma de juntar una décima o quinta parte exacta de la pizza usando esas rebanadas, sin importar cuán finamente las corte. Puede hacer una aproximación bastante buena, y si suma la aproximación de 0.1 con la aproximación de 0.2, obtendrá una aproximación bastante buena de 0.3, pero sigue siendo solo eso, una aproximación.

Para los números de doble precisión (que es la precisión que le permite reducir a la mitad su pizza 53 veces), los números inmediatamente menores y mayores que 0.1 son 0.09999999999999999167332731531132594682276248931884765625 y 0.1000000000000000055511151231257827021181583404541015625. El último está bastante más cerca de 0.1 que el primero, por lo que un analizador numérico, con una entrada de 0.1, favorecerá al último.

(La diferencia entre esos dos números es el "segmento más pequeño" que debemos decidir incluir, que introduce un sesgo hacia arriba, o excluir, que introduce un sesgo hacia abajo. El término técnico para ese segmento más pequeño es un ulp ).

En el caso de 0.2, los números son todos iguales, solo aumentados por un factor de 2. Nuevamente, favorecemos el valor que es ligeramente superior a 0.2.

Observe que en ambos casos, las aproximaciones para 0.1 y 0.2 tienen un ligero sesgo hacia arriba. Si agregamos suficiente de estos sesgos, empujarán el número cada vez más lejos de lo que queremos, y de hecho, en el caso de 0.1 + 0.2, el sesgo es lo suficientemente alto como para que el número resultante ya no sea el número más cercano a 0.3.

En gran medida, la


PD: algunos lenguajes de programación también proporcionan cortadores de pizza que pueden dividir rebanadas en décimas exactas . Aunque tales cortadores de pizza son poco comunes, si tiene acceso a uno, debe usarlo cuando sea importante poder obtener exactamente una décima parte o un quinto de una rebanada.

(Publicado originalmente en Quora).

Chris Jester-Young
fuente
3
Tenga en cuenta que hay algunos idiomas que incluyen matemáticas exactas. Un ejemplo es Scheme, por ejemplo a través de GNU Guile. Ver draketo.de/english/exact-math-to-the-rescue : estos mantienen las matemáticas como fracciones y solo se cortan al final.
Arne Babenhauserheide
55
@FloatingRock En realidad, muy pocos lenguajes de programación convencionales tienen números racionales incorporados. Arne es un Schemer, como yo lo soy, así que estas son cosas que nos miman.
Chris Jester-Young
55
@ArneBabenhauserheide Creo que vale la pena agregar que esto solo funcionará con números racionales. Entonces, si estás haciendo algunas matemáticas con números irracionales como pi, deberías almacenarlo como un múltiplo de pi. Por supuesto, cualquier cálculo que implique pi no puede representarse como un número decimal exacto.
Aidiakapi
13
@connexo Ok. ¿Cómo programarías tu rotador de pizza para obtener 36 grados? ¿Qué es 36 grados? (Sugerencia: si puede definir esto de manera exacta, también tiene un cortador de pizza en rodajas, una décima parte exacta). En otras palabras, no puede tener 1/360 (un grado) o 1 / 10 (36 grados) con solo punto flotante binario.
Chris Jester-Young
12
@connexo Además, "cada idiota" no puede rotar una pizza exactamente 36 grados. Los humanos son demasiado propensos a errores para hacer algo tan preciso.
Chris Jester-Young
212

Errores de redondeo de punto flotante. 0.1 no se puede representar con tanta precisión en la base 2 como en la base 10 debido al factor primo faltante de 5. Al igual que 1/3 toma un número infinito de dígitos para representar en decimal, pero es "0.1" en la base 3, 0.1 toma un número infinito de dígitos en base-2 donde no lo hace en base-10. Y las computadoras no tienen una cantidad infinita de memoria.

Devin Jeanpierre
fuente
133
las computadoras no necesitan una cantidad infinita de memoria para obtener 0.1 + 0.2 = 0.3 correcto
Pacerier
23
@Pacerier Claro, podrían usar dos enteros de precisión ilimitada para representar una fracción, o podrían usar la notación de comillas. Es la noción específica de "binario" o "decimal" lo que hace que esto sea imposible: la idea de que tiene una secuencia de dígitos binarios / decimales y, en algún lugar, un punto de raíz. Para obtener resultados racionales precisos necesitaríamos un mejor formato.
Devin Jeanpierre
15
@Pacerier: Ni el punto flotante binario ni el decimal pueden almacenar con precisión 1/3 o 1/13. Los tipos decimales de coma flotante pueden representar con precisión valores de la forma M / 10 ^ E, pero son menos precisos que los números binarios de coma flotante de tamaño similar cuando se trata de representar la mayoría de las otras fracciones . En muchas aplicaciones, es más útil tener mayor precisión con fracciones arbitrarias que tener una precisión perfecta con unas pocas "especiales".
supercat
13
@Pacerier Lo hacen si están almacenando los números como flotantes binarios, que era el punto de la respuesta.
Mark Amery
3
@chux: La diferencia de precisión entre los tipos binarios y decimales no es enorme, pero la diferencia de 10: 1 en la precisión del mejor caso frente al peor de los tipos decimales es mucho mayor que la diferencia 2: 1 con los tipos binarios. Tengo curiosidad por saber si alguien ha construido hardware o software escrito para operar de manera eficiente en cualquiera de los tipos decimales, ya que ninguno de ellos parecería adecuado para una implementación eficiente en hardware o software.
supercat
121

Además de las otras respuestas correctas, es posible que desee considerar escalar sus valores para evitar problemas con la aritmética de punto flotante.

Por ejemplo:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... en lugar de:

var result = 0.1 + 0.2;     // result === 0.3 returns false

La expresión 0.1 + 0.2 === 0.3regresa falseen JavaScript, pero afortunadamente la aritmética de enteros en coma flotante es exacta, por lo que los errores de representación decimal pueden evitarse mediante el escalado.

Como ejemplo práctico, para evitar problemas de punto flotante donde la precisión es primordial, se recomienda 1 manejar el dinero como un número entero que representa el número de centavos: 2550centavos en lugar de 25.50dólares.


1 Douglas Crockford: JavaScript: The Good Parts : Apéndice A - Awful Parts (página 105) .

Daniel Vassallo
fuente
3
El problema es que la conversión en sí es inexacta. 16.08 * 100 = 1607.9999999999998. ¿Tenemos que recurrir a dividir el número y convertir por separado (como en 16 * 100 + 08 = 1608)?
Jason
38
La solución aquí es hacer todos sus cálculos en números enteros y luego dividirlos por su proporción (100 en este caso) y redondear solo cuando presente los datos. Eso asegurará que sus cálculos siempre sean precisos.
David Granado
16
Solo para señalar un poco: la aritmética de enteros solo es exacta en coma flotante hasta un punto (juego de palabras). Si el número es mayor que 0x1p53 (para usar la notación de punto flotante hexadecimal de Java 7, = 9007199254740992), entonces el ulp es 2 en ese punto y entonces 0x1p53 + 1 se redondea a 0x1p53 (y 0x1p53 + 3 se redondea a 0x1p53 + 4, debido a redondeo a par). :-D Pero ciertamente, si su número es menor a 9 billones, debería estar bien. :-P
Chris Jester-Young
2
Jason, deberías redondear el resultado (int) (16.08 * 100 + 0.5)
Mikhail Semenov
@CodyBugstein " Entonces, ¿cómo se obtiene .1 + .2 para mostrar .3? " Escriba una función de impresión personalizada para colocar el decimal donde lo desee.
RonJohn
113

Mi respuesta es bastante larga, así que la he dividido en tres secciones. Como la pregunta es acerca de las matemáticas de coma flotante, he puesto énfasis en lo que la máquina realmente hace. También lo hice específico para la precisión doble (64 bits), pero el argumento se aplica igualmente a cualquier aritmética de coma flotante.

Preámbulo

Un número de formato de punto flotante binario de doble precisión IEEE 754 (binary64) representa un número de la forma

valor = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023

en 64 bits:

  • El primer bit es el bit de signo : 1si el número es negativo, de lo 0contrario 1 .
  • Los siguientes 11 bits son el exponente , que se compensa con 1023. En otras palabras, después de leer los bits de exponente de un número de doble precisión, se debe restar 1023 para obtener la potencia de dos.
  • Los 52 bits restantes son el significado (o mantisa). En la mantisa, un 'implícito' 1.siempre se omite 2 ya que el bit más significativo de cualquier valor binario es 1.

1 - IEEE 754 permite el concepto de un cero con signo - +0y -0se tratan de manera diferente: 1 / (+0)es infinito positivo; 1 / (-0)Es infinito negativo. Para valores cero, los bits de mantisa y exponente son todos cero. Nota: los valores cero (+0 y -0) no se clasifican explícitamente como denormal 2 .

2 - Este no es el caso para los números denormales , que tienen un exponente de compensación de cero (y un implícito 0.). El rango de números de precisión doble denormal es d min ≤ | x | ≤ d max , donde d min (el número distinto de cero representable más pequeño) es 2 -1023-51 (≈ 4.94 * 10 -324 ) y d max (el número denormal más grande, para el cual la mantisa consiste completamente de 1s) es 2 -1023 + 1 - 2 - 1023 - 51 (≈ 2.225 * 10 - 308 ).


Convertir un número de doble precisión en binario

Existen muchos convertidores en línea para convertir un número de coma flotante de doble precisión a binario (por ejemplo, en binaryconvert.com ), pero aquí hay un código C # de muestra para obtener la representación IEEE 754 para un número de doble precisión (separo las tres partes con dos puntos ( :) :

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

Llegando al punto: la pregunta original

(Salte al final para la versión TL; DR)

Cato Johnston (el que pregunta) preguntó por qué 0.1 + 0.2! = 0.3.

Escritas en binario (con dos puntos que separan las tres partes), las representaciones IEEE 754 de los valores son:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Tenga en cuenta que la mantisa se compone de dígitos recurrentes de 0011. Esta es la clave de por qué hay algún error en los cálculos: 0.1, 0.2 y 0.3 no se pueden representar en binario precisamente en un número finito de bits binarios, no más de 1/9, 1/3 o 1/7 se pueden representar con precisión en dígitos decimales .

También tenga en cuenta que podemos disminuir la potencia en el exponente en 52 y desplazar el punto en la representación binaria a la derecha en 52 lugares (al igual que 10-3 * 1.23 == 10-5 * 123). Esto nos permite representar la representación binaria como el valor exacto que representa en la forma a * 2 p . donde 'a' es un número entero.

Convirtiendo los exponentes a decimal, eliminando el desplazamiento y volviendo a agregar el implícito 1(entre corchetes), 0.1 y 0.2 son:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

Para sumar dos números, el exponente debe ser el mismo, es decir:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

Como la suma no es de la forma 2 n * 1. {bbb} aumentamos el exponente en uno y desplazamos el punto decimal ( binario ) para obtener:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

Ahora hay 53 bits en la mantisa (la 53 está entre corchetes en la línea de arriba). El modo de redondeo predeterminado para IEEE 754 es ' Redondear al más cercano ', es decir, si un número x cae entre dos valores a y b , se elige el valor donde el bit menos significativo es cero.

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

Tenga en cuenta que una y B difieren sólo en el último bit; ...0011+ 1= ...0100. En este caso, el valor con el bit menos significativo de cero es b , entonces la suma es:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

mientras que la representación binaria de 0.3 es:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

que solo difiere de la representación binaria de la suma de 0.1 y 0.2 por 2 -54 .

La representación binaria de 0.1 y 0.2 son las representaciones más precisas de los números permitidos por IEEE 754. La adición de estas representaciones, debido al modo de redondeo predeterminado, da como resultado un valor que difiere solo en el bit menos significativo.

TL; DR

Escribiendo 0.1 + 0.2en una representación binaria IEEE 754 (con dos puntos que separan las tres partes) y comparándolo 0.3, esto es (he puesto los distintos bits entre corchetes):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Convertidos de nuevo a decimal, estos valores son:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

La diferencia es exactamente 2 -54 , que es ~ 5.5511151231258 × 10 -17 - insignificante (para muchas aplicaciones) cuando se compara con los valores originales.

Comparar los últimos bits de un número de coma flotante es inherentemente peligroso, como lo sabrá cualquiera que lea el famoso " Lo que todo informático debe saber sobre la aritmética de coma flotante " (que cubre todas las partes principales de esta respuesta).

La mayoría de las calculadoras usan dígitos de protección adicionales para solucionar este problema, que es cómo 0.1 + 0.2daría 0.3: los últimos bits se redondean.

Wai Ha Lee
fuente
14
Mi respuesta fue rechazada poco después de publicarla. Desde entonces he realizado muchos cambios (incluida la observación explícita de los bits recurrentes al escribir 0.1 y 0.2 en binario, que había omitido en el original). En caso de que el votante negativo vea esto, ¿podría darme algún comentario para que pueda mejorar mi respuesta? Siento que mi respuesta agrega algo nuevo ya que el tratamiento de la suma en IEEE 754 no está cubierto de la misma manera en otras respuestas. Si bien "Lo que todo informático debería saber ..." cubre el mismo material, mi respuesta trata específicamente del caso de 0.1 + 0.2.
Wai Ha Lee
57

Los números de coma flotante almacenados en la computadora constan de dos partes, un número entero y un exponente al que se toma la base y se multiplica por la parte entera.

Si la computadora funcionara en la base 10, 0.1sería 1 x 10⁻¹, 0.2sería 2 x 10⁻¹y 0.3sería 3 x 10⁻¹. Operaciones aritméticas con enteros es fácil y exacta, por lo que añadir 0.1 + 0.2, obviamente, tendrá como resultado 0.3.

Las computadoras no suelen funcionar en la base 10, funcionan en la base 2. Aún puede obtener resultados exactos para algunos valores, por ejemplo, 0.5is 1 x 2⁻¹y 0.25is 1 x 2⁻², y al agregarlos se obtiene en 3 x 2⁻², o 0.75. Exactamente.

El problema viene con números que se pueden representar exactamente en la base 10, pero no en la base 2. Esos números deben redondearse a su equivalente más cercano. Suponiendo el muy común formato de coma flotante IEEE de 64 bits, el número más cercano a 0.1es 3602879701896397 x 2⁻⁵⁵y el número más cercano a 0.2es 7205759403792794 x 2⁻⁵⁵; sumarlos juntos da como resultado 10808639105689191 x 2⁻⁵⁵, o un valor decimal exacto de 0.3000000000000000444089209850062616169452667236328125. Los números de punto flotante generalmente se redondean para su visualización.

Mark Ransom
fuente
2
@ Mark Gracias por esta explicación clara, pero surge la pregunta de por qué 0.1 + 0.4 se suma exactamente a 0.5 (al menos en Python 3). Además, ¿cuál es la mejor manera de verificar la igualdad al usar flotantes en Python 3?
pchegoor
2
@ user2417881 Las operaciones de punto flotante IEEE tienen reglas de redondeo para cada operación, y a veces el redondeo puede producir una respuesta exacta incluso cuando los dos números están un poco apagados. Los detalles son demasiado largos para un comentario y de todos modos no soy un experto en ellos. Como puede ver en esta respuesta, 0.5 es uno de los pocos decimales que se pueden representar en binario, pero eso es solo una coincidencia. Para pruebas de igualdad, consulte stackoverflow.com/questions/5595425/… .
Mark Ransom
1
@ user2417881 su pregunta me intrigó, así que la convertí en una pregunta y respuesta completas: stackoverflow.com/q/48374522/5987
Mark Ransom
47

Error de redondeo de punto flotante. De lo que todo informático debe saber sobre la aritmética de coma flotante :

Exprimir infinitos números reales en un número finito de bits requiere una representación aproximada. Aunque hay infinitos números enteros, en la mayoría de los programas el resultado de los cálculos enteros se puede almacenar en 32 bits. Por el contrario, dado cualquier número fijo de bits, la mayoría de los cálculos con números reales producirán cantidades que no pueden representarse exactamente usando esa cantidad de bits. Por lo tanto, el resultado de un cálculo de punto flotante a menudo se debe redondear para que encaje en su representación finita. Este error de redondeo es el rasgo característico del cálculo de punto flotante.

Brett Daniel
fuente
33

Mi solución alternativa:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

La precisión se refiere al número de dígitos que desea conservar después del punto decimal durante la suma.

Justineo
fuente
30

Se han publicado muchas buenas respuestas, pero me gustaría agregar una más.

No todos los números pueden representarse mediante flotantes / dobles. Por ejemplo, el número "0.2" se representará como "0.200000003" en precisión simple en el estándar de coma flotante IEEE754.

El modelo para los números reales de la tienda debajo del capó representa los números flotantes como

ingrese la descripción de la imagen aquí

Aunque puede escribir 0.2fácilmente, FLT_RADIXy DBL_RADIXes 2; no 10 para una computadora con FPU que utiliza el "Estándar IEEE para aritmética de punto flotante binario (ISO / IEEE Std 754-1985)".

Por lo tanto, es un poco difícil representar tales números exactamente. Incluso si especifica esta variable explícitamente sin ningún cálculo intermedio.

bruziuz
fuente
28

Algunas estadísticas relacionadas con esta famosa pregunta de doble precisión.

Al agregar todos los valores ( a + b ) usando un paso de 0.1 (de 0.1 a 100) tenemos ~ 15% de probabilidad de error de precisión . Tenga en cuenta que el error podría generar valores ligeramente mayores o menores. Aquí hay unos ejemplos:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

Al restar todos los valores ( a - b donde a> b ) usando un paso de 0.1 (de 100 a 0.1) tenemos ~ 34% de probabilidad de error de precisión . Aquí hay unos ejemplos:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* 15% y 34% son realmente enormes, así que siempre use BigDecimal cuando la precisión es de gran importancia. Con 2 dígitos decimales (paso 0.01) la situación empeora un poco más (18% y 36%).

Kostas Chalkias
fuente
28

No, no está roto, pero la mayoría de las fracciones decimales deben ser aproximadas

Resumen

La aritmética de coma flotante es exacta, desafortunadamente, no coincide bien con nuestra representación habitual de números de base 10, por lo que resulta que a menudo le damos una entrada ligeramente diferente de lo que escribimos.

Incluso los números simples como 0.01, 0.02, 0.03, 0.04 ... 0.24 no son representables exactamente como fracciones binarias. Si cuenta hasta 0.01, .02, .03 ..., no hasta que llegue a 0.25 obtendrá la primera fracción representable en la base 2 . Si lo intentaste con FP, tu 0.01 habría estado ligeramente apagado, por lo que la única forma de sumar 25 de ellos a un buen 0.25 exacto habría requerido una larga cadena de causalidad que involucra bits de guarda y redondeo. Es difícil de predecir, así que levantamos las manos y decimos "FP no es exacto", pero eso no es realmente cierto.

Constantemente le damos al hardware FP algo que parece simple en la base 10 pero que es una fracción repetitiva en la base 2.

¿Cómo pasó esto?

Cuando escribimos en decimal, cada fracción (específicamente, cada decimal final) es un número racional de la forma

           a / (2 n x 5 m )

En binario, solo obtenemos el término 2 n , es decir:

           a / 2 n

Así que en decimal, no podemos representar 1 / 3 . Como la base 10 incluye 2 como factor primo, cada número que podemos escribir como fracción binaria también se puede escribir como una fracción base 10. Sin embargo, casi nada de lo que escribimos como una fracción base 10 es representable en binario. En el rango de 0.01, 0.02, 0.03 ... 0.99, solo se pueden representar tres números en nuestro formato FP: 0.25, 0.50 y 0.75, porque son 1/4, 1/2 y 3/4, todos los números con un factor primo usando solo el término 2 n .

En la base 10 que no podemos representar 1 / 3 . Pero en binario, no podemos hacer 1 / 10 o 1 / 3 .

Entonces, si bien cada fracción binaria se puede escribir en decimal, lo contrario no es cierto. Y, de hecho, la mayoría de las fracciones decimales se repiten en binario.

Lidiando con eso

Los desarrolladores generalmente reciben instrucciones de hacer < comparaciones epsilon , un mejor consejo podría ser redondear a valores integrales (en la biblioteca C: round () y roundf (), es decir, permanecer en el formato FP) y luego comparar. El redondeo a una longitud de fracción decimal específica resuelve la mayoría de los problemas con la salida.

Además, en los problemas de cálculo de números reales (los problemas para los que se inventó la FP en computadoras antiguas y terriblemente caras), las constantes físicas del universo y todas las demás mediciones solo son conocidas por un número relativamente pequeño de cifras significativas, por lo que todo el espacio del problema fue "inexacto" de todos modos. La "precisión" de FP no es un problema en este tipo de aplicación.

Todo el problema surge realmente cuando las personas intentan usar FP para el conteo de frijoles. Funciona para eso, pero solo si te apegas a los valores integrales, lo que de alguna manera frustra el punto de usarlo. Es por eso que tenemos todas esas bibliotecas de software de fracción decimal.

Me encanta la respuesta de Chris de Pizza , porque describe el problema real, no solo el saludo habitual sobre la "inexactitud". Si la PF fuera simplemente "inexacta", podríamos arreglar eso y lo habríamos hecho hace décadas. La razón por la que no lo hemos hecho es porque el formato FP es compacto y rápido y es la mejor manera de calcular muchos números. Además, es un legado de la era espacial y la carrera armamentista y los primeros intentos de resolver grandes problemas con computadoras muy lentas que usan sistemas de memoria pequeños. (A veces, núcleos magnéticos individuales para almacenamiento de 1 bit, pero esa es otra historia ) .

Conclusión

Si solo está contando frijoles en un banco, las soluciones de software que utilizan representaciones de cadenas decimales en primer lugar funcionan perfectamente bien. Pero no se puede hacer la cromodinámica cuántica o la aerodinámica de esa manera.

DigitalRoss
fuente
Redondear al entero más cercano no es una forma segura de resolver el problema de comparación en todos los casos. 0.4999998 y 0.500001 redondean a enteros diferentes, por lo que hay una "zona de peligro" alrededor de cada punto de corte de redondeo. (Sé que esas cadenas decimales probablemente no son exactamente representables como flotadores binarios IEEE.)
Peter Cordes
1
Además, aunque el punto flotante es un formato "heredado", está muy bien diseñado. No sé de nada que alguien cambiaría si lo rediseñara ahora. Cuanto más aprendo al respecto, más creo que está realmente bien diseñado. por ejemplo, el exponente sesgado significa que los flotantes binarios consecutivos tienen representaciones enteras consecutivas, por lo que puede implementar nextafter()con un incremento o decremento entero en la representación binaria de un flotante IEEE. Además, puede comparar flotantes como enteros y obtener la respuesta correcta, excepto cuando ambos son negativos (debido a la magnitud del signo frente al complemento de 2).
Peter Cordes
No estoy de acuerdo, los flotadores deben almacenarse como decimales y no binarios y todos los problemas están resueltos.
Ronen Festinger
¿No debería " x / (2 ^ n + 5 ^ n) " ser " x / (2 ^ n * 5 ^ n) "?
Wai Ha Lee
@RonenFestinger: ¿qué pasa con 1/3?
Stephen C
19

¿Probaste la solución de cinta adhesiva?

Trate de determinar cuándo ocurren los errores y corríjalos con sentencias if cortas, no es bonito pero para algunos problemas es la única solución y esta es una de ellas.

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

Tuve el mismo problema en un proyecto de simulación científica en C #, y puedo decirte que si ignoras el efecto mariposa, se convertirá en un gran dragón gordo y te morderá en el a **

flujo de trabajo
fuente
19

Para ofrecer la mejor solución que puedo decir, descubrí el siguiente método:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

Déjame explicarte por qué es la mejor solución. Como otros mencionaron en las respuestas anteriores, es una buena idea usar la función JavaScript toFixed () lista para usar para resolver el problema. Pero lo más probable es que te encuentres con algunos problemas.

Imagínese que usted va a sumar dos números flotantes como 0.2y 0.7aquí está: 0.2 + 0.7 = 0.8999999999999999.

Su resultado esperado fue 0.9que significa que necesita un resultado con precisión de 1 dígito en este caso. Entonces debería haber usado, (0.2 + 0.7).tofixed(1) pero no puede simplemente dar un cierto parámetro a toFixed () ya que depende del número dado, por ejemplo

`0.22 + 0.7 = 0.9199999999999999`

En este ejemplo, necesita una precisión de 2 dígitos, por lo que debería ser toFixed(2), ¿cuál debería ser el parámetro para adaptarse a cada número flotante?

Podría decir que sea 10 en cada situación:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

¡Maldición! ¿Qué vas a hacer con esos ceros no deseados después de las 9? Es el momento de convertirlo en flotante para hacerlo como desee:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

Ahora que ha encontrado la solución, es mejor ofrecerla como una función como esta:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }

Probémoslo tú mismo:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

Puedes usarlo de esta manera:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

Como W3SCHOOLS sugiere que también hay otra solución, puede multiplicar y dividir para resolver el problema anterior:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

¡Tenga en cuenta que (0.2 + 0.1) * 10 / 10no funcionará en absoluto aunque parezca lo mismo! Prefiero la primera solución, ya que puedo aplicarla como una función que convierte la entrada flotante en salida precisa.

Mohammad Musavi
fuente
Esto me hizo un verdadero dolor de cabeza. Sumo 12 números flotantes, luego muestro la suma y el promedio si esos números. El uso de toFixed () podría corregir la suma de 2 números, pero cuando suma varios números, el salto es significativo.
Nuryagdy Mustapayev
@Nuryagdy Mustapayev No obtuve su intención, ya que probé antes de que pueda sumar 12 números flotantes, luego use la función floatify () en el resultado, luego haga lo que quiera, no observé ningún problema al usarlo.
Mohammad Musavi
Solo digo en mi situación donde tengo alrededor de 20 parámetros y 20 fórmulas donde el resultado de cada fórmula depende de otros, esta solución no ayudó.
Nuryagdy Mustapayev
16

Esos números extraños aparecen porque las computadoras usan el sistema de números binarios (base 2) para fines de cálculo, mientras que nosotros usamos el decimal (base 10).

Hay una mayoría de números fraccionarios que no se pueden representar con precisión ni en binario ni en decimal ni en ambos. Resultado: resultados de un número redondeado (pero preciso).

Piyush S528
fuente
No entiendo tu segundo párrafo en absoluto.
Nae
1
@Nae Yo traduciría el segundo párrafo como "La mayoría de las fracciones no se pueden representar exactamente en decimal o binario. Por lo tanto, la mayoría de los resultados se redondearán, aunque seguirán siendo precisos para el número de bits / dígitos inherentes a la representación siendo utilizado."
Steve Summit
15

Muchos de los numerosos duplicados de esta pregunta se refieren a los efectos del redondeo de coma flotante en números específicos. En la práctica, es más fácil tener una idea de cómo funciona mirando los resultados exactos de los cálculos de interés en lugar de simplemente leer sobre ello. Algunos lenguajes proporcionan maneras de hacerlo - como la conversión de una floato doublede BigDecimalJava.

Dado que esta es una pregunta independiente del idioma, necesita herramientas independientes del idioma, como un convertidor de decimal a coma flotante .

Aplicandolo a los números en la pregunta, tratados como dobles:

0.1 se convierte en 0.1000000000000000055511151231257827021181583404541015625,

0.2 se convierte en 0.200000000000000011102230246251565404236316680908203125,

0.3 se convierte en 0.299999999999999988897769753748434595763683319091796875, y

0.30000000000000004 se convierte a 0.3000000000000000444089209850062616169452667236328125.

Agregar los primeros dos números manualmente o en una calculadora decimal, como la Calculadora de precisión completa , muestra que la suma exacta de las entradas reales es 0.3000000000000000166533453693773481063544750213623046875.

Si se redondeara al equivalente de 0.3, el error de redondeo sería 0.0000000000000000277555756156289135105907917022705078125. Redondear al equivalente de 0.30000000000000004 también da un error de redondeo 0.0000000000000000277555756156289135105907917022705078125. Se aplica el disyuntor redondo a par.

Volviendo al convertidor de coma flotante, el hexadecimal bruto para 0.30000000000000004 es 3fd3333333333334, que termina en un dígito par y, por lo tanto, es el resultado correcto.

Patricia Shanahan
fuente
2
Para la persona cuya edición acabo de revertir: considero que las comillas de código son apropiadas para citar código. Esta respuesta, siendo neutral en cuanto al lenguaje, no contiene ningún código citado. Los números se pueden usar en oraciones en inglés y eso no los convierte en código.
Patricia Shanahan
Esta es probablemente la razón por la cual alguien formateó sus números como código, no para formatear, sino para facilitar la lectura.
Wai Ha Lee
... también, la ronda hasta se refiere a la representación binaria , no a la representación decimal . Vea esto o, por ejemplo, esto .
Wai Ha Lee
@WaiHaLee No apliqué la prueba impar / par a ningún número decimal, solo hexadecimal. Un dígito hexadecimal es incluso si, y solo si, el bit menos significativo de su expansión binaria es cero.
Patricia Shanahan
14

Dado que nadie ha mencionado esto ...

Algunos lenguajes de alto nivel, como Python y Java, vienen con herramientas para superar las limitaciones de punto flotante binario. Por ejemplo:

  • El decimalmódulo de Python y la BigDecimalclase de Java , que representan números internamente con notación decimal (en oposición a la notación binaria). Ambos tienen una precisión limitada, por lo que siguen siendo propensos a errores, sin embargo, resuelven los problemas más comunes con la aritmética de coma flotante binaria.

    Los decimales son muy buenos cuando se trata de dinero: diez centavos más veinte centavos son siempre exactamente treinta centavos:

    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True
    

    El decimalmódulo de Python se basa en el estándar IEEE 854-1987 .

  • fractionsMódulo de Python y BigFractionclase de Apache Common . Ambos representan números racionales como (numerator, denominator)pares y pueden dar resultados más precisos que la aritmética de coma flotante decimal.

Ninguna de estas soluciones es perfecta (especialmente si observamos las actuaciones, o si requerimos una precisión muy alta), pero aún así resuelven una gran cantidad de problemas con la aritmética de coma flotante binaria.

Andrea Corbellini
fuente
14

¿Puedo agregar? la gente siempre asume que se trata de un problema informático, pero si cuenta con las manos (base 10), no puede obtener a (1/3+1/3=2/3)=truemenos que tenga infinito para agregar 0.333 ... a 0.333 ... así como con el (1/10+2/10)!==3/10problema en la base 2, lo trunca a 0.333 + 0.333 = 0.666 y probablemente lo redondea a 0.667, lo que también sería técnicamente inexacto.

Cuente en ternario, y los tercios no son un problema, sin embargo, tal vez una raza con 15 dedos en cada mano pregunte por qué su matemática decimal se rompió ...


fuente
Como los humanos usan números decimales, no veo una buena razón por la cual los flotadores no se representan como un decimal por defecto, por lo que tenemos resultados precisos.
Ronen Festinger
Los seres humanos utilizan muchas bases que no sean de base 10 (decimales), binario es el que usamos para el cálculo de la mayoría .. la 'buena razón' es que simplemente no puedes representa cada fracción en cada base ..
La aritmética binaria @RonenFestinger es fácil de implementar en las computadoras porque solo requiere ocho operaciones básicas con dígitos: digamos $ a $, $ b $ en $ 0,1 $ todo lo que necesita saber es $ \ operatorname {xor} (a, b) $ y $ \ operatorname {cb} (a, b) $, donde xor es exclusivo o y cb es el "bit de acarreo" que es $ 0 $ en todos los casos excepto cuando $ a = 1 = b $, en cuyo caso tenemos uno (de hecho, la conmutatividad de todas las operaciones le ahorra $ 2 $ casos y todo lo que necesita son reglas de $ 6 $). La expansión decimal necesita $ 10 \ times 11 $ (en notación decimal) para ser almacenada y $ 10 $ diferentes estados para cada bit y desperdicia almacenamiento en el carry.
Oskar Limka
@RonenFestinger - Decimal NO es más preciso. Eso es lo que dice esta respuesta. Para cualquier base que elija, habrá números racionales (fracciones) que dan secuencias de dígitos que se repiten infinitamente. Para el registro, algunos de los primeros ordenadores hicieron uso de la base 10 representaciones de números, pero los diseñadores de hardware pioneras pronto llegó a la conclusión de que la base 2 era mucho más fácil y más eficiente de implementar.
Stephen C
9

El tipo de matemática de punto flotante que se puede implementar en una computadora digital necesariamente usa una aproximación de los números reales y las operaciones en ellos. (La versión estándar tiene más de cincuenta páginas de documentación y tiene un comité para encargarse de sus erratas y mejoras adicionales).

Esta aproximación es una mezcla de aproximaciones de diferentes tipos, cada una de las cuales puede ignorarse o explicarse cuidadosamente debido a su forma específica de desviación de la exactitud. También implica una serie de casos excepcionales explícitos, tanto a nivel de hardware como de software, que la mayoría de las personas pasan por delante mientras fingen no darse cuenta.

Si necesita una precisión infinita (usando el número π, por ejemplo, en lugar de uno de sus muchos sustitutos más cortos), debe escribir o usar un programa matemático simbólico.

Pero si está de acuerdo con la idea de que a veces las matemáticas de punto flotante tienen un valor difuso y la lógica y los errores pueden acumularse rápidamente, y puede escribir sus requisitos y pruebas para permitir eso, entonces su código puede pasar con frecuencia con lo que está en su FPU

Blair Houghton
fuente
9

Solo por diversión, jugué con la representación de flotadores, siguiendo las definiciones del Estándar C99 y escribí el código a continuación.

El código imprime la representación binaria de flotantes en 3 grupos separados.

SIGN EXPONENT FRACTION

y después de eso imprime una suma que, cuando se suma con suficiente precisión, mostrará el valor que realmente existe en el hardware.

Entonces, cuando escribe float x = 999..., el compilador transformará ese número en una representación de bits impresa por la función de xxmodo que la suma impresa por la función yysea ​​igual al número dado.

En realidad, esta suma es solo una aproximación. Para el número 999,999,999, el compilador insertará en representación de bits del flotador el número 1,000,000,000

Después del código, adjunto una sesión de consola, en la que calculo la suma de términos para ambas constantes (menos PI y 999999999) que realmente existe en el hardware, insertada allí por el compilador.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Aquí hay una sesión de consola en la que calculo el valor real del flotante que existe en el hardware. Solía bcimprimir la suma de términos generados por el programa principal. Uno puede insertar esa suma en Python replo algo similar también.

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

Eso es. El valor de 999999999 es de hecho

999999999.999999446351872

También puede verificar bcque -3.14 también está perturbado. No olvides establecer un scalefactor bc.

La suma mostrada es lo que hay dentro del hardware. El valor que obtienes al calcularlo depende de la escala que establezcas. Establecí el scalefactor en 15. Matemáticamente, con una precisión infinita, parece que es 1,000,000,000.

alinsoar
fuente
5

Otra forma de ver esto: se usan 64 bits para representar números. Como consecuencia, no hay más de 2 ** 64 = 18,446,744,073,709,551,616 números diferentes que se pueden representar con precisión.

Sin embargo, Math dice que ya hay infinitos decimales entre 0 y 1. IEE 754 define una codificación para usar estos 64 bits de manera eficiente para un espacio numérico mucho mayor más NaN y +/- Infinito, por lo que hay espacios entre los números representados con precisión llenos de números solo aproximados.

Lamentablemente 0.3 se encuentra en un hueco.

Torsten Becker
fuente
4

Imagine trabajar en base diez con, digamos, 8 dígitos de precisión. Usted verifica si

1/3 + 2 / 3 == 1

y aprende que esto vuelve false. ¿Por qué? Bueno, como números reales tenemos

1/3 = 0.333 .... y 2/3 = 0.666 ....

Truncando en ocho decimales, obtenemos

0.33333333 + 0.66666666 = 0.99999999

que es, por supuesto, diferente de 1.00000000exactamente 0.00000001.


La situación para los números binarios con un número fijo de bits es exactamente análoga. Como números reales, tenemos

1/10 = 0.0001100110011001100 ... (base 2)

y

1/5 = 0.0011001100110011001 ... (base 2)

Si los truncamos a, digamos, siete bits, obtendríamos

0.0001100 + 0.0011001 = 0.0100101

mientras que por otro lado,

3/10 = 0.01001100110011 ... (base 2)

que, truncado a siete bits, es 0.0100110, y estos difieren exactamente 0.0000001.


La situación exacta es un poco más sutil porque estos números generalmente se almacenan en notación científica. Entonces, por ejemplo, en lugar de almacenar 1/10 ya 0.0001100que podemos almacenarlo como algo así 1.10011 * 2^-4, dependiendo de cuántos bits hayamos asignado para el exponente y la mantisa. Esto afecta cuántos dígitos de precisión obtienes para tus cálculos.

El resultado es que, debido a estos errores de redondeo, esencialmente nunca desea usar == en números de punto flotante. En cambio, puede verificar si el valor absoluto de su diferencia es menor que algún número pequeño fijo.

Daniel McLaury
fuente
4

Desde Python 3.5 puede usar la math.isclose()función para probar la igualdad aproximada:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
nauer
fuente
3

Dado que este hilo se ramificó un poco en una discusión general sobre las implementaciones actuales de coma flotante, agregaría que hay proyectos para solucionar sus problemas.

Eche un vistazo a https://posithub.org/ por ejemplo, que muestra un tipo de número llamado posit (y su predecesor unum) que promete ofrecer una mejor precisión con menos bits. Si mi comprensión es correcta, también soluciona el tipo de problemas en la pregunta. Proyecto bastante interesante, la persona detrás de él es un matemático que es el Dr. John Gustafson . Todo es de código abierto, con muchas implementaciones reales en C / C ++, Python, Julia y C # ( https://hastlayer.com/arithmetics ).

Piedone
fuente
3

En realidad es bastante simple. Cuando tienes un sistema de base 10 (como el nuestro), solo puede expresar fracciones que usan un factor primo de la base. Los factores primos de 10 son 2 y 5. Por lo tanto, 1/2, 1/4, 1/5, 1/8 y 1/10 se pueden expresar limpiamente porque todos los denominadores usan factores primos de 10. En contraste, 1 / 3, 1/6 y 1/7 son decimales repetidos porque sus denominadores usan un factor primo de 3 o 7. En binario (o base 2), el único factor primo es 2. Por lo tanto, solo puede expresar fracciones limpiamente que solo contiene 2 como factor primo. En binario, 1/2, 1/4, 1/8 se expresarían limpiamente como decimales. Mientras, 1/5 o 1/10 estarían repitiendo decimales. Entonces, 0.1 y 0.2 (1/10 y 1/5), mientras que los decimales limpios en un sistema base 10, son decimales repetidos en el sistema base 2 en el que está operando la computadora. Cuando hace cálculos matemáticos en estos decimales repetitivos,

Desde https://0.30000000000000004.com/

Vlad Agurets
fuente
3

Los números decimales como 0.1, 0.2y 0.3no se representan exactamente en tipos de coma flotante codificados en binario. La suma de las aproximaciones para 0.1y 0.2difiere de la aproximación utilizada para 0.3, de ahí la falsedad de 0.1 + 0.2 == 0.3como se puede ver más claramente aquí:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

Salida:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

Para que estos cálculos se evalúen de manera más confiable, necesitaría usar una representación basada en decimales para los valores de coma flotante. El Estándar C no especifica dichos tipos por defecto, sino como una extensión descrita en un Informe técnico .

Las _Decimal32, _Decimal64y los _Decimal128tipos podrían estar disponibles en su sistema (por ejemplo, GCC les apoya en objetivos seleccionados , pero Clang no los admite en OS X ).

chqrlie
fuente
1

Math.sum (javascript) ... tipo de reemplazo del operador

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

la idea es usar operadores matemáticos en su lugar para evitar errores de flotación

Math.sum detecta automáticamente la precisión para usar

Math.sum acepta cualquier número de argumentos

bortunac
fuente
1
Sin embargo, no estoy seguro de que hayas respondido la pregunta " ¿Por qué ocurren estas inexactitudes? ".
Wai Ha Lee
de manera que tiene razón pero vine aquí de un comportamiento extraño Javascript respecto a este tema ... sólo quiero compartir tipo de solución
bortunac
Sin embargo, todavía no estás respondiendo la pregunta.
Wai Ha Lee
k tiene un problema con esto ... dígame dónde moverlo o si insiste, puedo eliminarlo
bortunac
0

Acabo de ver este interesante tema en torno a los puntos flotantes:

Considere los siguientes resultados:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

Podemos ver claramente un punto de interrupción cuando 2**53+1: todo funciona bien hasta 2**53.

>>> (2**53) - int(float(2**53))
0

Ingrese la descripción de la imagen aquí

Esto sucede debido al binario de doble precisión: formato de punto flotante binario de doble precisión IEEE 754: binario64

Desde la página de Wikipedia para el formato de coma flotante de precisión doble :

El punto flotante binario de doble precisión es un formato comúnmente utilizado en las PC, debido a su rango más amplio sobre el punto flotante de precisión simple, a pesar de su rendimiento y costo de ancho de banda. Al igual que con el formato de punto flotante de precisión simple, carece de precisión en los números enteros en comparación con un formato entero del mismo tamaño. Es comúnmente conocido simplemente como doble. El estándar IEEE 754 especifica un binario64 que tiene:

  • Bit de señal: 1 bit
  • Exponente: 11 bits
  • Precisión significativa: 53 bits (52 almacenados explícitamente)

Ingrese la descripción de la imagen aquí

El valor real asumido por un determinado dato de doble precisión de 64 bits con un exponente sesgado dado y una fracción de 52 bits es

Ingrese la descripción de la imagen aquí

o

Ingrese la descripción de la imagen aquí

Gracias a @a_guest por señalarme eso.

costargc
fuente
-1

Una pregunta diferente ha sido nombrada como un duplicado de esta:

En C ++, ¿por qué el resultado es cout << xdiferente del valor que muestra un depurador x?

El xen la pregunta es una floatvariable.

Un ejemplo sería

float x = 9.9F;

El depurador muestra 9.89999962, la salida de la coutoperación es 9.9.

La respuesta es que coutla precisión predeterminada para float6 es, por lo que se redondea a 6 dígitos decimales.

Ver aquí para referencia


fuente
1
OMI: publicar esto aquí fue un enfoque equivocado. Sé que es frustrante, pero las personas que necesitan una respuesta a la pregunta original (¡aparentemente ahora eliminada!) No la encontrarán aquí. Si realmente siente que su trabajo merece ser salvado, sugeriría: 1) buscar otra Q que realmente responda, 2) crear una pregunta que se responda a sí misma.
Stephen C