Pregunta ¿Las matemáticas de punto flotante están rotas?


Considera el siguiente código:

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

¿Por qué ocurren estas imprecisiones?


2259
2018-02-25 21:39


origen


Respuestas:


Binario punto flotante las matemáticas son así En la mayoría de los lenguajes de programación, se basa en Estándar IEEE 754. JavaScript usa una representación de punto flotante de 64 bits, que es lo mismo que Java double. 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; números racionales (como 0.1, cual es 1/10) cuyo denominador no es un poder de dos no puede representarse exactamente.

por 0.1 en el estándar binary64 formato, la representación se puede escribir exactamente como

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

Por el contrario, el número racional 0.1, cual es 1/10, se puede escribir exactamente como

  • 0.1 en decimal, o
  • 0x1.99999999999999...p-4 en un análogo de notación CW hexfloat, donde el ... representa una secuencia interminable de 9's.

Las constantes 0.2 y 0.3 en tu programa también serán aproximaciones a sus verdaderos valores. 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 de 0.1 y 0.2 termina siendo más grande que el número racional 0.3 y por lo tanto, está en desacuerdo con la constante en su código.

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


1718
2018-04-18 11:52



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 coma 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 los errores de punto flotante ocurren y parecen acumularse con el tiempo.

1. Información general

Desde una perspectiva de ingeniería, la mayoría de las operaciones de punto flotante tendrán algún elemento de error, ya que el hardware que hace los cálculos de coma flotante solo debe tener un error de menos de la mitad de una unidad en el último lugar. Por lo tanto, mucho 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 un operación única que es especialmente problemático en la división de coma flotante. Lo que constituye una sola operación depende de cuántos operandos toma 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 lugar a un error deseable ya que los errores se acumulan con el tiempo.

2. Estándares

La mayoría de los procesadores siguen el IEEE-754 estándar, 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. Lo siguiente, sin embargo, cubrirá el modo normalizado de IEEE-754 que es el modo de operación típico.

En el estándar IEEE-754, los diseñadores de hardware tienen cualquier valor de error / épsilon, siempre y cuando sea menos de la mitad de una unidad en el último lugar, y el resultado solo debe ser menor que 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 número 54, ya que se utilizan 53 bits para representar la parte numérica (normalizada), también llamada mantisa, del número de punto flotante (por ejemplo, el 5.3 en 5.3e5). Las siguientes secciones entran en más detalles sobre las causas del error de hardware en varias operaciones de coma flotante.

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

La 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 computacionales 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, 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 del cociente suele ser el ancho del radix, o un número de bits de el cociente calculado en cada iteración, más algunos bits de guardia. 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 protección k, donde k>=2. Entonces, por ejemplo, una Tabla de Selección de Cocientes típica para un divisor que compute 2 bits del cociente a la vez (raíz 4) sería 2+2= 4 bits (más algunos bits opcionales).

3.1 Error de redondeo de división: Aproximación de reciprocidad

Qué reciprocals están en la tabla de selección del cociente dependen de 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 según 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. Tanto la división lenta como los métodos rápidos de división calculan el cociente de forma iterativa, es decir, se calcula un 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 es menos de 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 son generalmente menos costosos de construir, y los métodos de división rápida calculan un número variable de dígitos por paso y suelen ser 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 un aproximación de un recíproco, por lo que son propensos al error.

4. Errores de redondeo en otras operaciones: truncamiento

Otra causa de los errores de redondeo en todas las operaciones son los diferentes modos de truncamiento de la respuesta final que permite IEEE-754. Hay truncado, redondo hacia cero, redondo a más cercano (predeterminado), redondear hacia abajo y redondear. 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 agrega acumulativamente al error resultante. Este error de truncamiento es especialmente problemático en la exponenciación, que implica alguna forma de multiplicación repetida.

5. Operaciones repetidas

Dado que el hardware que realiza los cálculos de punto flotante solo tiene que arrojar 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 limitado, los matemáticos utilizan métodos como el uso de la ronda a la más cercana incluso dígito en el último lugar de IEEE-754, porque, con el tiempo, es más probable que los errores se cancelen entre sí, y Aritmética de intervalo combinado con variaciones de 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, redondee 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, redondeado a más cercano incluso dígito 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, el rodeo y el redondeo solos puede dar como resultado un error que es mayor que la mitad de una unidad en el último lugar, pero menos de una unidad en el último lugar, por lo que estos modos no se recomiendan a menos que sean utilizado en Interval Arithmetic.

6. Resumen

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


490
2018-02-25 21:43



Cuando conviertes .1 o 1/10 en base 2 (binario) obtienes un patrón de repetición después del punto decimal, igual que tratar de representar 1/3 en la base 10. El valor no es exacto, y por lo tanto no puedes hacer matemática exacta usando métodos de coma flotante normales.


356
2017-11-20 02:39



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

Imagina que estás tratando de cortar las pizzas. Usted tiene un cortador de pizza robótico que puede cortar rebanadas de pizza exactamente a la mitad. Puede reducir a la mitad una pizza entera, o puede reducir a la mitad una porción existente, pero en cualquier caso, la mitad es siempre exacta.

Ese cortador de pizza tiene movimientos muy finos, y si comienzas con una pizza entera, luego la reduces a la mitad y sigues dividiendo a la mitad la rebanada más pequeña cada vez, puedes hacer la mitad 53 veces antes de que la rebanada sea demasiado pequeña, incluso para sus habilidades de alta precisión. En ese momento, ya no puede dividir a la mitad ese segmento muy fino, pero debe incluirlo o excluirlo tal como está.

Ahora, ¿cómo unirías todas las rebanadas de una manera que sumaría una décima parte (0.1) o una quinta parte (0.2) de una pizza? Piensa en ello y trata de resolverlo. Incluso puede intentar usar una pizza de verdad, si tiene a mano un cortador de pizza de precisión mítica. :-)


La mayoría de los programadores experimentados, por supuesto, conocen la respuesta real, que es que no hay forma de reconstruir una exacto décimo o quinto de la pizza usando esas rebanadas, no importa cuán finamente las cortes. Puede hacer una buena aproximación, y si suma la aproximación de 0.1 con la aproximación de 0.2, obtiene una aproximación bastante buena de 0.3, pero sigue siendo eso, una aproximación.

Para números de precisión doble (que es la precisión que le permite dividir la pizza a la mitad 53 veces), los números inmediatamente menores y mayores 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 excluir, lo que introduce un sesgo a la baja. El término técnico para el 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. De nuevo, preferimos el valor que es ligeramente mayor que 0.2.

Observe que en ambos casos, las aproximaciones para 0.1 y 0.2 tienen un ligero sesgo ascendente. Si agregamos suficientes 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 particular, 0.1 + 0.2 es realmente 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, mientras que el número más cercano a 0.3 es en realidad 0.299999999999999988897769753748434595763683319091796875.


PD Algunos lenguajes de programación también proporcionan cortadores de pizza que pueden dividir rebanadas en décimas exactas. Aunque estos cortadores de pizza son poco comunes, si tiene acceso a uno, debe usarlo cuando es importante poder obtener exactamente una décima o una quinta parte de un corte.

(Publicado originalmente en Quora).


225
2018-02-25 21:41



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


199
2018-04-09 12:25



Además de las otras respuestas correctas, es posible que desee considerar ampliar 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 expresion 0.1 + 0.2 === 0.3 devoluciones 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 escalando.

Como ejemplo práctico, para evitar problemas de coma flotante donde la precisión es primordial, se recomienda1 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 buenas partes: Apéndice A - Piezas espantosas (página 105).


98
2018-02-23 17:15



Mi respuesta es bastante larga, así que la he dividido en tres secciones. Dado que la pregunta es acerca de las matemáticas de punto 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 punto flotante.

Preámbulo

Un Formato de coma flotante binaria de doble precisión IEEE 754 (binary64) número representa un número de la forma

valor = (-1) ^ s * (1.m51metro50...metro2metro1metro0)2 * 2e-1023

en 64 bits:

  • El primer bit es el signo de bit: 1 si el número es negativo, 0 de otra manera1.
  • Los siguientes 11 bits son exponente, cual es compensar por 1023. En otras palabras, después de leer los bits de exponente de un número de precisión doble, se debe restar 1023 para obtener la potencia de dos.
  • Los 52 bits restantes son significand (o mantisa). En la mantisa, un "implícito" 1. es siempre2 omitido ya que el bit más significativo de cualquier valor binario es 1.

1 - IEEE 754 permite el concepto de firmado cero - +0 y -0 son tratados 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 denormal2.

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


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

Existen muchos convertidores en línea para convertir un número de coma flotante de doble precisión en binario (por ejemplo, en binaryconvert.com), pero aquí hay un ejemplo de código C # para obtener la representación IEEE 754 para un número de precisión doble (separé 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 (la pregunta asker) preguntó por qué 0.1 + 0.2! = 0.3.

Escrito en binario (con dos puntos que separan las tres partes), las representaciones de los valores IEEE 754 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 llave a 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 finito número de bits binarios más de 1/9, 1/3 o 1/7 se puede representar con precisión en dígitos decimales.

Convertir los exponentes en decimal, eliminar el desplazamiento y volver 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

Para agregar 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 2norte * 1. {bbb} aumentamos el exponente en uno y desplazamos el decimal (binario) punto para obtener:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

Ahora hay 53 bits en la mantisa (el 53 está entre corchetes en la línea superior). El valor por defecto modo de redondeo para IEEE 754 es 'Redondear a la más cercana'- es decir, si un número X cae entre dos valores un y segundo, 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 un y segundo diferir solo en el último bit; ...0011 + 1 = ...0100. En este caso, el valor con el bit menos significativo de cero es segundo, entonces la suma es:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

TL; DR

Escritura 0.1 + 0.2 en una representación binaria IEEE 754 (con dos puntos que separan las tres partes) y compararla con 0.3, esto es (he puesto los bits distintos 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.

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

La mayoría de las calculadoras usan dígitos de guardia para evitar este problema, que es cómo 0.1 + 0.2 daría 0.3: los últimos bits están redondeados.


80
2018-03-16 05:27



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

Si la computadora estaba 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 entera es fácil y exacta, por lo que 0.1 + 0.2 obviamente resultará en 0.3.

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

El problema viene con números que pueden representarse exactamente en la base 10, pero no en la base 2. Esos números deben redondearse a su equivalente más cercano. Asumiendo el muy común formato IEEE de coma flotante 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 resultados en 10808639105689191 x 2⁻⁵⁵, o un valor decimal exacto de 0.3000000000000000444089209850062616169452667236328125. Los números de coma flotante generalmente se redondean para mostrarlos.


48
2018-02-25 21:42



Error de redondeo de punto flotante. De Lo que todo informático debería saber sobre la aritmética de coma flotante:

Exprimir infinitamente muchos 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 enteros se puede almacenar en 32 bits. Por el contrario, 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 utilizando tantos bits. Por lo tanto, el resultado de un cálculo de punto flotante a menudo debe redondearse para volver a su representación finita. Este error de redondeo es la característica del cálculo en coma flotante.


40
2017-12-26 06:51



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 a la cantidad de dígitos que desea conservar después del punto decimal durante la suma.


29
2017-10-05 18:39



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

No todos los números se pueden representar a través de carrozas/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 los números reales de la tienda bajo el capó representa los números del flotador como

enter image description here

Aunque puedes escribir 0.2 fácilmente, FLT_RADIX y DBL_RADIX es 2; no 10 para una computadora con FPU que usa "Estándar IEEE para aritmética de punto flotante binario (ISO / IEEE Std 754-1985)".

Entonces es un poco difícil representar esos números exactamente. Incluso si especifica esta variable explícitamente sin ningún cálculo intermedio.


26
2018-02-02 23:49