math - rotar - rotacion en el plano cartesiano
¿Es rota la matemática de punto flotante? (27)
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 que ocurren los errores de punto flotante y que se acumulen 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 producir 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 punto 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 resulten en 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 no normalizados o diferentes. Por ejemplo, hay un modo desnormalizado en IEEE-754 que permite la representación de números de punto 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 y cuando sea menos de la mitad de una unidad en el último lugar, y el resultado solo tiene que 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 bit 54, ya que se usan 53 bits para representar la parte numérica (normalizada), también llamada mantisa, del número de punto flotante (por ejemplo, 5.3 en 5.3e5). Las siguientes secciones detallan las causas del error de hardware en varias operaciones de punto flotante.
3. Causa del error de redondeo en la división
La principal causa del error en la división de punto flotante son los algoritmos de división utilizados para calcular el cociente. La mayoría de los sistemas informáticos calculan la división utilizando la multiplicación por una inversa, principalmente en Z=X/Y
, Z = X * (1/Y)
. Una división se calcula de forma iterativa, 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 es generalmente el ancho del radix, o un número de bits de el cociente calculado en cada iteración, más unos pocos bits de guarda. Para el estándar IEEE-754, doble precisión (64 bits), sería el tamaño de la base del divisor, más algunos bits de guarda k, donde k>=2
. Así, por ejemplo, una Tabla de selección de cociente típica para un divisor que calcula 2 bits del cociente a la vez (raíz 4) sería 2+2= 4
bits (más unos pocos bits opcionales).
3.1 Error de redondeo de división: aproximación de recíproco
Los recíprocos de la tabla de selección de cocientes 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 introducen algún elemento de error. Tanto los métodos de división lenta como los de división rápida calculan el cociente de manera iterativa, es decir, se calcula una cantidad de bits del cociente en cada paso, luego el resultado se resta del dividendo y el divisor repite los pasos hasta que el error es menor que la mitad de uno. Unidad en el último lugar. Los métodos de división lenta calculan un número fijo de dígitos del cociente en cada paso y suelen ser 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 caros 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 recíproca, por lo que son propensos a errores.
4. Redondeo de errores 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 truncado, redondeado hacia cero, redondeo al más cercano (predeterminado), redondeo hacia abajo y redondeo 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 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 punto flotante solo necesita producir un resultado con un error de menos de la mitad de una unidad en el último lugar para una sola operación, el error aumentará con las 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 utilizan métodos como el uso del dígito par redondeado 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 la aritmética de intervalos combinada con variaciones de los modos de redondeo IEEE 754 para 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 el último lugar), es el modo de redondeo predeterminado de IEEE-754.
Tenga en cuenta que el modo de redondeo predeterminado, el dígito par redondeado 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. Usar el truncamiento, redondear y redondear solo puede dar como resultado un error que sea 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 punto 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 punto flotante sobre las operaciones repetidas se acumularán a menos que se corrijan.
Considere el siguiente código:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
¿Por qué ocurren estas inexactitudes?
No, no roto, pero la mayoría de las fracciones decimales deben ser aproximadas
Resumen
La aritmética de punto flotante es exacta, desafortunadamente, no coincide bien con nuestra representación habitual del número base-10, por lo que resulta que a menudo le estamos dando una entrada que está ligeramente alejada de lo que escribimos.
Incluso los números simples como 0.01, 0.02, 0.03, 0.04 ... 0.24 no se pueden representar exactamente como fracciones binarias. Si cuentas hasta 0.01, .02, .03 ..., no hasta que llegues a 0.25 obtendrás la primera fracción representable en la base 2 . Si intentara usar FP, su 0.01 habría estado ligeramente apagado, por lo que la única forma de sumar 25 de ellos hasta un buen valor exacto de 0.25 habría requerido una larga cadena de causalidad que involucra brocas de protección y redondeo. Es difícil de predecir, así que levantamos nuestras manos y decimos que "FP es inexacto", pero eso no es realmente cierto.
Constantemente le damos al hardware de FP algo que parece simple en la base 10 pero es una fracción que se repite en la base 2.
¿Cómo pasó esto?
Cuando escribimos en decimal, cada fracción (específicamente, cada decimal de terminación) 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 . Debido a que la base 10 incluye 2 como factor primordial, cada número que podamos escribir como una fracción binaria también se puede escribir como una fracción base 10. Sin embargo, casi nada de lo que escribimos como fracción base 10 es representable en binario. En el rango de 0.01, 0.02, 0.03 ... 0.99, solo tres números se pueden representar en nuestro formato de 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, mientras que 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 para hacer comparaciones de <epsilon , un mejor consejo podría ser redondear a valores integrales (en la biblioteca C: round () y roundf (), es decir, permanecer en el formato de PF) 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 reales de procesamiento de números (los problemas para los que se inventó la FP en computadoras tempranas y terriblemente caras) las constantes físicas del universo y todas las demás mediciones solo se conocen en un número relativamente pequeño de cifras significativas, por lo que todo el espacio del problema era "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 se adhiere a los valores integrales, qué tipo de derrotas el punto de usarlo. Es por eso que tenemos todas esas bibliotecas de software de fracción decimal.
Me encanta la respuesta de Pizza de , porque describe el problema real, no solo el habitual gesto de "inexactitud". Si la PF fuera simplemente "inexacta", podríamos solucionarlo y lo habríamos hecho décadas atrás. 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 procesar muchos números. Además, es un legado de la era espacial y la carrera de armamentos y los primeros intentos de resolver grandes problemas con computadoras muy lentas que usan sistemas de memoria pequeña. (A veces, núcleos magnéticos individuales para almacenamiento de 1 bit, pero esa es otra historia ) .
Conclusión
Si solo está contando beans en un banco, las soluciones de software que utilizan representaciones de cadenas decimales en primer lugar funcionan perfectamente bien. Pero no puedes hacer cromodinámica cuántica o aerodinámica de esa manera.
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
devuelve false
en JavaScript, pero afortunadamente la aritmética de enteros en coma flotante es exacta, por lo que se pueden evitar los errores de representación decimal mediante la escala.
Como ejemplo práctico, para evitar problemas de punto flotante donde la precisión es primordial, se recomienda 1 para manejar el dinero como un número entero que representa la cantidad de centavos: 2550
centavos en lugar de 25.50
dólares.
1 Douglas Crockford: JavaScript: Las partes buenas : Apéndice A - Partes horribles (página 105) .
Cuando convierte .1 o 1/10 a base 2 (binario), obtiene un patrón de repetición después del punto decimal, al igual que tratar de representar 1/3 en la base 10. El valor no es exacto y, por lo tanto, no puede hacerlo Matemáticas exactas con él usando métodos normales de punto flotante
Errores de redondeo de punto flotante. 0.1 no se puede representar con la misma 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.
Los números de punto flotante almacenados en la computadora constan de dos partes, un entero y un exponente al que la base se lleva y multiplica por la parte entera.
Si la computadora estuviera trabajando 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⁻¹
. La matemática de enteros es fácil y exacta, por lo que agregar 0.1 + 0.2
obviamente resultará en 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
es 1 x 2⁻¹
y 0.25
es 1 x 2⁻²
, y 1 x 2⁻²
resulta 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 que el formato de punto flotante IEEE de 64 bits es muy común, 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⁻⁵⁵
; 10808639105689191 x 2⁻⁵⁵
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 mostrar.
Matemáticas de punto flotante binario es así. En la mayoría de los lenguajes de programación, se basa en el estándar IEEE 754 . JavaScript utiliza una representación de punto flotante de 64 bits, que es lo mismo que el double
de Java. El quid del problema es que los números se representan en este formato como un número entero por una potencia de dos; los números racionales (como 0.1
, que es 1/10
) cuyo denominador no es una potencia de dos no se pueden representar exactamente.
Para 0.1
en el formato binary64
estándar, la representación se puede escribir exactamente como
-
0.1000000000000000055511151231257827021181583404541015625
en decimal, o -
0x1.999999999999ap-4
en la notación hexfloat C99 .
En contraste, el número racional 0.1
, que es 1/10
, puede escribirse exactamente como
-
0.1
en decimal, o -
0x1.99999999999999...p-4
en un análogo de la notación hexfloat 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 reales. Sucede que el double
más cercano a 0.2
es más grande que el número racional 0.2
pero que el double
más cercano a 0.3
es más pequeño que el número racional 0.3
. La suma de 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 científico 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 .
¿Probaste la solución de cinta adhesiva?
Trate de determinar cuándo ocurren los errores y corríjalos con frases 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 **
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.
Imagina que estás tratando de cortar pizzas. Usted tiene un cortador de pizza robótico que puede cortar porciones de pizza exactamente a la mitad. Puede reducir a la mitad toda una pizza, o puede reducir a la mitad una rebanada existente, pero en cualquier caso, la reducción a la mitad siempre es exacta.
El cortador de pizza tiene movimientos muy finos, y si empiezas con una pizza entera, luego divide a la mitad, y continúa dividiendo la porción más pequeña cada vez, puedes hacer la reducción de la mitad 53 veces antes de que la porción sea demasiado pequeña incluso para sus habilidades de alta precisión. . En ese momento, ya no puede reducir a la mitad esa rebanada muy delgada, sino que debe incluirla o excluirla tal como está.
Ahora, ¿cómo dividirías todas las rebanadas de tal manera que sumarían una décima parte (0.1) o una quinta parte (0.2) de una pizza? Realmente piénsalo y trata de resolverlo. Incluso puedes intentar usar una pizza real, si tienes a mano un cortador de pizza de precisión mítica. :-)
Por supuesto, la mayoría de los programadores experimentados conocen la respuesta real, y es que no hay forma de juntar una décima o quinta parte exacta de la pizza con esas rebanadas, sin importar cuán finamente las rebane. Puedes hacer una aproximación bastante buena, y si sumas la aproximación de 0.1 con la aproximación de 0.2, obtendrás una aproximación bastante buena de 0.3, pero aún así es solo 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), el número inmediatamente inferior y superior a 0,1 son 0,09999999999999999167332731531132594682276248931884765625 y 0,1000000000000000055511151231257827021181583404541015625. Este último es bastante más cercano a 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 la "porción más pequeña" que debemos decidir incluir, que introduce un sesgo hacia arriba, o excluye, que introduce un sesgo hacia abajo. El término técnico para esa porción más pequeña es una ulp .)
En el caso de 0.2, los números son todos iguales, simplemente aumentados por un factor de 2. Una vez más, favorecemos el valor que es ligeramente superior a 0.2.
Tenga en cuenta que en ambos casos, las aproximaciones de 0.1 y 0.2 tienen un ligero sesgo hacia arriba. Si agregamos suficiente cantidad de estos sesgos, empujarán el número más y 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 particular, 0.100.000 es el número de usuario de la licencia de la licencia.
PD: Algunos lenguajes de programación también proporcionan cortadores de pizza que pueden dividir las porciones en décimas exactas . Si bien estos cortadores de pizza no son comunes, si tiene acceso a uno, debe usarlo cuando sea importante poder obtener exactamente una décima parte de una rebanada.
Mi respuesta es bastante larga, así que la dividí en tres secciones. Dado que la pregunta es sobre las matemáticas de punto flotante, he puesto énfasis en lo que realmente hace la máquina. También lo hice específico para precisión doble (64 bits), pero el argumento se aplica igualmente a cualquier aritmética de punto flotante.
Preámbulo
Un número de formato de punto flotante binario de doble precisión IEEE 754 (binary64) representa un número del formulario
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 :
1
si el número es negativo,0
caso contrario 1 . - Los siguientes 11 bits son el exponent , que está offset por 1023. En otras palabras, después de leer los bits del exponente de un número de precisión doble, se debe restar 1023 para obtener la potencia de dos.
- Los 52 bits restantes son el significand (o mantisa). En la mantisa, un 1 ''implícito'' siempre se omite 2 ya que el bit más significativo de cualquier valor binario es
1
.
1 - IEEE 754 permite el concepto de 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 de los números denormales , que tienen un exponente de desplazamiento de cero (y un 0.
implícito). El rango de los números de doble precisión 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 consta de 1
s) es 2 - 1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 ).
Convertir un número de doble precisión a binario.
Existen muchos convertidores en línea para convertir un número de punto flotante de doble precisión a binario (por ejemplo, en binaryconvert.com ), pero aquí hay un código de 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.
(Ir a la parte inferior de la versión TL; DR)
(el que pregunta) preguntó por qué 0.1 + 0.2! = 0.3.
Escritas en binario (con dos puntos separando 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
. Esto es clave para explicar 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, como máximo 1/9, 1/3 o 1/7 se pueden representar precisamente en dígitos decimales
Al convertir los exponentes a decimales, eliminar el desplazamiento y volver a agregar el 1
implícito (entre corchetes), 0.1 y 0.2 son:
0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010
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
Dado que la suma no es de la forma 2 n * 1. {bbb} aumentamos el exponente en uno y cambiamos el punto decimal ( binario ) para obtener:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
Ahora hay 53 bits en la mantisa (la 53 se encuentra 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 se encuentra entre dos valores a y b , se elige el valor donde el bit menos significativo es cero.
a = 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
Tenga en cuenta que a y b difieren solo en el último bit; ...0011
+ 1
= ...0100
. En este caso, el valor con el bit menos significativo de cero es b , por lo que la suma es:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
TL; DR
Escribiendo 0.1 + 0.2
en una representación binaria IEEE 754 (con dos puntos separando las tres partes) y comparándolo con 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]
Convertido 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) en comparación con los valores originales.
La comparación de los últimos bits de un número de punto flotante es intrínsecamente peligrosa, como lo sabrá cualquiera que lea " Lo que todo científico informático debe saber sobre la aritmética de punto flotante " (que cubre todas las partes principales de esta respuesta).
La mayoría de las calculadoras usan dígitos de guarda adicionales para solucionar este problema, que es cómo 0.1 + 0.2
darían 0.3
: los pocos bits finales se redondean.
Math.sum (javascript) .... tipo de reemplazo de 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;
}
},
diff:{
value: function(A,B){
var prec = this.max(this.get_precision(A),this.get_precision(B));
return +this.precision(A-B,prec);
}
},
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 utilizar operadores matemáticos en lugar de evitar errores de flotación
Math.diff(0.2, 0.11) == 0.09 // true
0.2 - 0.11 == 0.09 // false
También tenga en cuenta que Math.diff y Math.sum detectan automáticamente la precisión a utilizar
Math.sum acepta cualquier número de argumentos
En realidad, esto fue pensado como una respuesta para esta pregunta , que se cerró como un duplicado de esta pregunta, mientras estaba armando esta respuesta, por lo que ahora no puedo publicarla allí ... ¡así que publicaré aquí!
Resumen de la pregunta:
En la hoja de trabajo
10^-8/1000
y10^-11
evalúe como Igual mientras que en VBA no lo hacen.
En la hoja de trabajo, los números están predeterminados a Notación científica.
Si cambia las células a un formato de número ( Ctrl+ 1) de Number
la 15
decimales, se obtiene:
=10^-11 returns 0.000000000010000
=10^(-8/1000) returns 0.981747943019984
Por lo tanto, definitivamente no son lo mismo ... uno es casi cero y el otro casi 1.
Excel no fue diseñado para tratar con números extremadamente pequeños, al menos no con la instalación de stock. Hay complementos para ayudar a mejorar la precisión de los números.
Excel fue diseñado de acuerdo con el estándar IEEE para aritmética de punto flotante binario ( IEEE 754 ). El estándar define cómo se almacenan y calculan los números de punto flotante . El estándar IEEE 754 se usa ampliamente porque permite que los números de punto flotante se almacenen en una cantidad razonable de espacio y los cálculos pueden ocurrir con relativa rapidez.
La ventaja de flotar sobre la representación de punto fijo es que puede admitir un rango más amplio de valores. Por ejemplo, una representación de punto fijo que tiene 5 dígitos decimales con punto decimal posicionado después de la tercera dígitos puede representar los números
123.34
,12.23
,2.45
, etc. mientras coma flotante representación con 5 precisión dígitos puede representar 1,2345, 12.345, 0,00012345, etc. De manera similar, la representación de punto flotante también permite cálculos en un amplio rango de magnitudes al mismo tiempo que mantiene la precisión. Por ejemplo,
Otras referencias:
- Soporte de oficina: Mostrar números en notación científica (exponencial)
- Blog de Microsoft 365: Entender la precisión de punto flotante , también conocido como "¿Por qué Excel me da respuestas aparentemente incorrectas?"
- Soporte de Office: Ajuste de precisión de redondeo en Excel
- Soporte de oficina:
POWER
Función - Superusuario: ¿Cuál es el mayor valor (número) que puedo almacenar en una variable de Excel VBA?
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 un 15% de probabilidad de error de precisión . Tenga en cuenta que el error podría resultar en valores ligeramente más grandes o más pequeños. 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 un 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)
* El 15% y el 34% son realmente enormes, por lo que siempre use BigDecimal cuando la precisión sea de gran importancia. Con 2 dígitos decimales (paso 0.01) la situación empeora un poco más (18% y 36%).
Dado que este hilo se ramificó un poco en una discusión general sobre las implementaciones actuales de punto 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 corrige el tipo de problemas en la pregunta. Proyecto bastante interesante, la persona detrás de él es un matemático, 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 ).
Dado que nadie ha mencionado esto ...
Algunos lenguajes de alto nivel, como Python y Java, vienen con herramientas para superar las limitaciones de los puntos flotantes binarios. Por ejemplo:
decimal
Módulo de Python yBigDecimal
clase de Java , que representan números internamente con notación decimal (en oposición a notación binaria). Ambos tienen una precisión limitada, por lo que aún son propensos a errores, sin embargo, resuelven los problemas más comunes con la aritmética de punto flotante binario.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 yBigFraction
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 los rendimientos, o si requerimos una precisión muy alta), pero aún así solucionan una gran cantidad de problemas con la aritmética de punto flotante binario.
Esos números extraños aparecen porque las computadoras usan un sistema numérico binario (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 precisamente en binario o en decimal o en ambos. Resultado: un número redondeado (pero preciso) de resultados.
Mi solución:
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
precisión se refiere al número de dígitos que desea conservar después del punto decimal durante la suma.
Se han publicado muchas respuestas buenas, pero me gustaría añadir una más.
No todos los números se pueden representar a través de flotantes / dobles Por ejemplo, el número "0.2" se representará como "0.200000003" en precisión simple en el estándar de punto flotante IEEE754.
El modelo para almacenar números reales debajo del capó representa números flotantes como
Aunque puedes 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)".
Así que es un poco difícil representar exactamente esos números. Incluso si especifica esta variable explícitamente sin ningún cálculo intermedio.
Solo por diversión, jugué con la representación de flotadores, siguiendo las definiciones del Estándar C99 y escribí el siguiente código.
El código imprime la representación binaria de flotadores 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.
Por lo tanto, cuando escriba float x = 999...
, el compilador transformará ese número en una representación de bits impresa por la función de xx
tal manera que la suma impresa por la función yy
sea igual al número dado.
En realidad, esta suma es sólo una aproximación. Para el número 999,999,999 el compilador insertará en la 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 computo la suma de los términos para ambas constantes (menos PI y 999999999) que realmente existen en el hardware, insertadas 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 flotador que existe en el hardware. Solía bc
imprimir la suma de los términos emitidos 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 puedes verificar bc
que -3.14 también está perturbado. No te olvides de establecer un scale
factor en bc
.
La suma mostrada es lo que está dentro del hardware. El valor que obtenga al calcularlo depende de la escala que establezca. Establecí el scale
factor a 15. Matemáticamente, con una precisión infinita, parece que es 1,000,000,000.
Una pregunta diferente ha sido nombrada como un duplicado a este:
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 por defecto float
es 6, por lo que se redondea a 6 dígitos decimales.
Vea here para referencia
¿Puedo simplemente añadir; la gente siempre asume que esto es un problema de computadora, pero si cuenta con sus 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 base 2, se trunca a 0.333 + 0.333 = 0.666 y probablemente se redondea a 0.667, lo que también sería técnicamente inexacto.
Cuente en ternario, y las terceras no son un problema, sin embargo, tal vez una carrera con 15 dedos en cada mano preguntaría por qué se rompió su matemática decimal ...
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 tratar sus erratas y su refinamiento adicional).
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 involucra una cantidad de casos excepcionales explícitos tanto en el 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 (por ejemplo, utilizando el número π, en lugar de uno de sus muchos más cortos suplentes), 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 son confusas en valor y la lógica y los errores se pueden acumular rápidamente, y puede escribir sus requisitos y pruebas para permitirlo, entonces su código con frecuencia puede subsistir con el contenido su FPU.
Error de redondeo de punto flotante. De lo que todo científico informático debe saber sobre la aritmética de punto flotante :
Exprimir infinitos números reales en un número finito de bits requiere una representación aproximada. Aunque hay infinitos enteros, en la mayoría de los programas el resultado de los cálculos de enteros se puede almacenar en 32 bits. En contraste, dado un número fijo de bits, la mayoría de los cálculos con números reales producirán cantidades que no se pueden representar exactamente usando tantos bits. Por lo tanto, el resultado de un cálculo de punto flotante a menudo se debe redondear para encajar en su representación finita. Este error de redondeo es el rasgo característico del cálculo de punto flotante.
Muchos de los numerosos duplicados de esta pregunta preguntan acerca de los efectos del redondeo de punto flotante en números específicos. En la práctica, es más fácil tener una idea de cómo funciona al observar los resultados exactos de los cálculos de interés en lugar de solo leerlo. Algunos lenguajes proporcionan maneras de hacerlo - como la conversión de una float
o double
de BigDecimal
Java.
Dado que se trata de una pregunta independiente del lenguaje, necesita herramientas independientes del lenguaje, como un convertidor de decimal a punto flotante .
Aplicándolo a los números en la pregunta, tratados como dobles:
0.1 se convierte en 0.1000000000000000055511151231257827021181583404541015625,
0.2 se convierte a 0.200000000000000011102230246251565404236316680908203125,
0.3 se convierte a 0.299999999999999988897769753748434595763683319091796875, y
0.30000000000000004 se convierte a 0.3000000000000000444089209850062616169452667236328125.
Agregar los dos primeros 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.30000000000000166533453693773481063544750213623046875.
Si se redondeara al equivalente de 0.3, el error de redondeo sería 0.00000000000000277555756156289135105907917022705078125. Redondear hasta el equivalente a 0.30000000000000004 también da un error de redondeo 0.0000000000000000757575756156289135105907917022705078125. Se aplica el desempate de ronda a par.
Volviendo al convertidor de punto flotante, el hexadecimal sin formato para 0.30000000000000004 es 3fd3333333333334, que termina en un dígito par y, por lo tanto, es el resultado correcto.
Otra forma de ver esto: se usan 64 bits para representar números. Como consecuencia, no hay forma de que se representen con precisión más de 2 ** 64 = 18,446,744,073,709,551,616 números diferentes.
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 más grande más NaN e +/- Infinito, por lo que hay espacios entre los números representados con precisión y llenos solo números aproximados.
Lamentablemente 0,3 se encuentra en un hueco.
Para ofrecer la mejor solución , puedo decir que 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 mencionados en las respuestas anteriores, es una buena idea usar la función toFixed () de listo 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 una precisión de 1 dígito en este caso. Así que deberías haber usado, (0.2 + 0.7).tofixed(1)
pero no puedes simplemente darle un determinado 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)
, entonces, ¿cuál debería ser el parámetro para ajustarse a cada número flotante dado?
Se podría decir que sea 10 en cada situación, entonces:
(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"
¡Maldita sea! ¿Qué vas a hacer con esos ceros no deseados después de 9? Es el momento de convertirlo en flotador para hacerlo como desees:
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));
}
Intentémoslo usted 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, puedes 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 el flotador de entrada en un flotador de salida preciso.
Sine Python 3.5 puedes usar la math.isclose()
función si las condiciones
import math
if math.isclose(0.1 + 0.2, 0.3, abs_tol=0.01):
pass