Considere el siguiente código:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
¿Por qué ocurren estas imprecisiones?
Considere el siguiente código:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
¿Por qué ocurren estas imprecisiones?
Respuestas:
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.1
el binary64
formato estándar , la representación se puede escribir exactamente como
0.1000000000000000055511151231257827021181583404541015625
en decimal, o0x1.999999999999ap-4
en notación C99 hexfloat .En contraste, el número racional 0.1
, que es 1/10
, puede escribirse exactamente como
0.1
en decimal, o0x1.99999999999999...p-4
en un análogo de notación de flotación hexagonal C99, donde ...
representa una secuencia interminable de 9.Las constantes 0.2
y 0.3
en su programa también serán aproximaciones a sus valores verdaderos. Sucede que el más cercano double
a 0.2
es más grande que el número racional 0.2
pero que el más cercano double
a 0.3
es más pequeño que el número racional 0.3
. La suma 0.1
y 0.2
termina siendo mayor que el número racional 0.3
y, 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 abs
es el valor absoluto myToleranceValue
debe 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.
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.
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.
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.
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= 4
bits (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.
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.
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.
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.
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.
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.
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.
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.3
regresa false
en 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: 2550
centavos en lugar de 25.50
dólares.
1 Douglas Crockford: JavaScript: The Good Parts : Apéndice A - Awful Parts (página 105) .
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:
1
si el número es negativo, de lo 0
contrario 1 .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 - +0
y -0
se 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 1
s) 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.2
en 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.2
daría 0.3
: los últimos bits se redondean.
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.1
sería 1 x 10⁻¹
, 0.2
sería 2 x 10⁻¹
y 0.3
serí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.5
is 1 x 2⁻¹
y 0.25
is 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.1
es 3602879701896397 x 2⁻⁵⁵
y el número más cercano a 0.2
es 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.
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.
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
Aunque puede escribir 0.2
fácilmente, FLT_RADIX
y DBL_RADIX
es 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.
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%).
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.
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).
¿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 **
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.2
y 0.7
aquí está: 0.2 + 0.7 = 0.8999999999999999
.
Su resultado esperado fue 0.9
que 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 / 10
no 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.
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).
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 float
o double
de BigDecimal
Java.
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.
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 decimal
módulo de Python y la BigDecimal
clase 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 decimal
módulo de Python se basa en el estándar IEEE 854-1987 .
fractions
Módulo de Python y BigFraction
clase 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.
¿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)=true
menos que tenga infinito para agregar 0.333 ... a 0.333 ... así como con el (1/10+2/10)!==3/10
problema 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ó ...
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
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 xx
modo que la suma impresa por la función yy
sea 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 bc
imprimir la suma de términos generados por el programa principal. Uno puede insertar esa suma en Python repl
o 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 bc
que -3.14 también está perturbado. No olvides establecer un scale
factor 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 scale
factor en 15. Matemáticamente, con una precisión infinita, parece que es 1,000,000,000.
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.
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.00000000
exactamente 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.0001100
que 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.
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
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 ).
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,
Los números decimales como 0.1
, 0.2
y 0.3
no se representan exactamente en tipos de coma flotante codificados en binario. La suma de las aproximaciones para 0.1
y 0.2
difiere de la aproximación utilizada para 0.3
, de ahí la falsedad de 0.1 + 0.2 == 0.3
como 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
, _Decimal64
y los _Decimal128
tipos podrían estar disponibles en su sistema (por ejemplo, GCC les apoya en objetivos seleccionados , pero Clang no los admite en OS X ).
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
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
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)
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
o
Gracias a @a_guest por señalarme eso.
Una pregunta diferente ha sido nombrada como un duplicado de esta:
En C ++, ¿por qué el resultado es cout << x
diferente del valor que muestra un depurador x
?
El x
en la pregunta es una float
variable.
Un ejemplo sería
float x = 9.9F;
El depurador muestra 9.89999962
, la salida de la cout
operación es 9.9
.
La respuesta es que cout
la precisión predeterminada para float
6 es, por lo que se redondea a 6 dígitos decimales.
Ver aquí para referencia