¿Por qué los números de coma flotante son inexactos?

198

¿Por qué algunos números pierden precisión cuando se almacenan como números de coma flotante?

Por ejemplo, el número decimal 9.2se puede expresar exactamente como una relación de dos enteros decimales ( 92/10), los cuales se pueden expresar exactamente en binario ( 0b1011100/0b1010). Sin embargo, la misma proporción almacenada como un número de coma flotante nunca es exactamente igual a 9.2:

32-bit "single precision" float: 9.19999980926513671875
64-bit "double precision" float: 9.199999999999999289457264239899814128875732421875

¿Cómo puede un número aparentemente simple ser "demasiado grande" para expresarlo en 64 bits de memoria?

mhlester
fuente

Respuestas:

241

En la mayoría de los lenguajes de programación, los números de coma flotante se representan de manera muy similar a la notación científica : con un exponente y una mantisa (también llamada significado). Un número muy simple, por ejemplo 9.2, es en realidad esta fracción:

5179139571476070 * 2 -49

Donde está el exponente -49y está la mantisa 5179139571476070. La razón por la que es imposible representar algunos números decimales de esta manera es que tanto el exponente como la mantisa deben ser enteros. En otras palabras, todos los flotadores deben ser un número entero multiplicado por una potencia entera de 2 .

9.2puede ser simple 92/10, pero 10 no puede expresarse como 2 n si n está limitado a valores enteros.


Ver los datos

Primero, algunas funciones para ver los componentes que hacen un 32 y 64 bits float. Pase por alto estos si solo le importa la salida (ejemplo en Python):

def float_to_bin_parts(number, bits=64):
    if bits == 32:          # single precision
        int_pack      = 'I'
        float_pack    = 'f'
        exponent_bits = 8
        mantissa_bits = 23
        exponent_bias = 127
    elif bits == 64:        # double precision. all python floats are this
        int_pack      = 'Q'
        float_pack    = 'd'
        exponent_bits = 11
        mantissa_bits = 52
        exponent_bias = 1023
    else:
        raise ValueError, 'bits argument must be 32 or 64'
    bin_iter = iter(bin(struct.unpack(int_pack, struct.pack(float_pack, number))[0])[2:].rjust(bits, '0'))
    return [''.join(islice(bin_iter, x)) for x in (1, exponent_bits, mantissa_bits)]

Hay una gran cantidad de complejidad detrás de esa función, y sería bastante tangente explicarlo, pero si le interesa, el recurso importante para nuestros propósitos es el módulo de estructura .

Python floates un número de doble precisión de 64 bits. En otros lenguajes como C, C ++, Java y C #, la precisión doble tiene un tipo separado double, que a menudo se implementa como 64 bits.

Cuando llamamos a esa función con nuestro ejemplo 9.2, esto es lo que obtenemos:

>>> float_to_bin_parts(9.2)
['0', '10000000010', '0010011001100110011001100110011001100110011001100110']

Interpretando los datos

Verá que he dividido el valor de retorno en tres componentes. Estos componentes son:

  • Firmar
  • Exponente
  • Mantissa (también llamada Significand, o Fraction)

Firmar

El signo se almacena en el primer componente como un solo bit. Es fácil de explicar: 0significa que el flotador es un número positivo; 1significa que es negativo Porque 9.2es positivo, nuestro valor de signo es 0.

Exponente

El exponente se almacena en el componente medio como 11 bits. En nuestro caso 0b10000000010,. En decimal, eso representa el valor 1026. Una peculiaridad de este componente es que debe restar un número igual a 2 (# de bits) - 1 - 1 para obtener el verdadero exponente; en nuestro caso, eso significa restar 0b1111111111(número decimal 1023) para obtener el verdadero exponente 0b00000000011(número decimal 3).

Mantissa

La mantisa se almacena en el tercer componente como 52 bits. Sin embargo, también hay una peculiaridad en este componente. Para comprender esta peculiaridad, considere un número en notación científica, como este:

6.0221413x10 23

La mantisa sería la 6.0221413. Recuerde que la mantisa en notación científica siempre comienza con un solo dígito distinto de cero. Lo mismo es cierto para el binario, excepto que el binario solo tiene dos dígitos: 0y 1. ¡Entonces la mantisa binaria siempre comienza con 1! Cuando se almacena un flotador, 1se omite el frente de la mantisa binaria para ahorrar espacio; tenemos que volver a colocarlo al frente de nuestro tercer elemento para obtener la verdadera mantisa:

1.0010011001100110011001100110011001100110011001100110

Esto implica algo más que una simple adición, porque los bits almacenados en nuestro tercer componente en realidad representan la parte fraccional de la mantisa, a la derecha del punto de raíz .

Cuando tratamos con números decimales, "movemos el punto decimal" multiplicando o dividiendo por potencias de 10. En binario, podemos hacer lo mismo multiplicando o dividiendo por potencias de 2. Dado que nuestro tercer elemento tiene 52 bits, dividimos por 2 52 para moverlo 52 lugares a la derecha:

0.0010011001100110011001100110011001100110011001100110

En notación decimal, eso es lo mismo que dividir 675539944105574entre 4503599627370496para obtener 0.1499999999999999. (Este es un ejemplo de una relación que se puede expresar exactamente en binario, pero solo aproximadamente en decimal; para más detalles, consulte: 675539944105574/4503599627370496 ).

Ahora que hemos transformado el tercer componente en un número fraccionario, la suma 1da la verdadera mantisa.

Recapitulando los componentes

  • Signo (primer componente): 0para positivo, 1para negativo
  • Exponente (componente medio): resta 2 (# de bits) - 1 - 1 para obtener el verdadero exponente
  • Mantissa (último componente): divide entre 2 (# de bits) y suma 1para obtener la verdadera mantisa

Calcular el número

Al unir las tres partes, se nos da este número binario:

1.0010011001100110011001100110011001100110011001100110 x 10 11

Que luego podemos convertir de binario a decimal:

1.1499999999999999 x 2 3 (¡inexacto!)

Y multiplique para revelar la representación final del número con el que comenzamos ( 9.2) después de ser almacenado como un valor de coma flotante:

9.1999999999999993


Representando como una fracción

9.2

Ahora que hemos construido el número, es posible reconstruirlo en una fracción simple:

1.0010011001100110011001100110011001100110011001100110 x 10 11

Cambia la mantisa a un número entero:

10010011001100110011001100110011001100110011001100110 x 10 11-110100

Convierte a decimal:

5179139571476070 x 2 3-52

Resta el exponente:

5179139571476070 x 2 -49

Convierta el exponente negativo en división:

5179139571476070/2 49

Multiplicar exponente:

5179139571476070/562949953421312

Que es igual a:

9.1999999999999993

9.5

>>> float_to_bin_parts(9.5)
['0', '10000000010', '0011000000000000000000000000000000000000000000000000']

Ya puedes ver que la mantisa tiene solo 4 dígitos seguidos de muchos ceros. Pero vamos a través de los pasos.

Montar la notación científica binaria:

1.0011 x 10 11

Desplaza el punto decimal:

10011 x 10 11-100

Resta el exponente:

10011 x 10 -1

Binario a decimal:

19 x 2 -1

Exponente negativo a la división:

19/2 1

Multiplicar exponente:

19/2

Igual a:

9.5



Otras lecturas

mhlester
fuente
1
También hay un buen tutorial que muestra cómo ir para otro lado: dada una representación decimal de un número, ¿cómo se construye el equivalente de coma flotante? El enfoque de "división larga" muestra muy claramente cómo terminas con un "resto" después de intentar representar el número. Debe agregarse si quiere ser verdaderamente "canónico" con su respuesta.
Floris
1
Si está hablando de Python y coma flotante, le sugiero que incluya al menos el tutorial de Python en sus enlaces: docs.python.org/3.4/tutorial/floatingpoint.html Se supone que es la única opción. recurso para problemas de punto flotante para programadores de Python. Si falta de alguna manera (y casi seguro que sí), abra un problema en el rastreador de errores de Python para actualizaciones o cambios.
Mark Dickinson
@mhlester Si esto se convierte en wiki comunitario, siéntase libre de incorporar mi respuesta a la suya.
Nicu Stiurca
55
Esta respuesta definitivamente también debe estar vinculada a floating-point-gui.de , ya que es probablemente la mejor introducción para principiantes. OMI, incluso debería ir por encima de "Lo que todo científico de la computación debería saber ...": en estos días, las personas que pueden comprender razonablemente el artículo de Goldberg generalmente ya lo saben.
Daniel Pryden
1
"Este es un ejemplo de una relación que puede expresarse exactamente en binario, pero solo aproximadamente en decimal". Esto no es verdad. Todas estas proporciones de 'número sobre una potencia de dos' son exactas en decimal. Cualquier aproximación es solo para acortar el número decimal, por conveniencia.
Rick Regan
29

Esta no es una respuesta completa ( mhlester ya cubrió mucho terreno bueno que no duplicaré), pero me gustaría enfatizar cuánto depende la representación de un número de la base en la que está trabajando.

Considere la fracción 2/3

En la buena base 10, generalmente la escribimos como algo así

  • 0,666 ...
  • 0,666
  • 0,667

Cuando miramos esas representaciones, tendemos a asociar cada una de ellas con la fracción 2/3, aunque solo la primera representación es matemáticamente igual a la fracción. Las representaciones / aproximaciones segunda y tercera tienen un error del orden de 0.001, que en realidad es mucho peor que el error entre 9.2 y 9.1999999999999993. De hecho, ¡la segunda representación ni siquiera se redondea correctamente! Sin embargo, no tenemos un problema con 0.666 como una aproximación del número 2/3, por lo que realmente no deberíamos tener un problema con la aproximación de 9.2 en la mayoría de los programas . (Sí, en algunos programas es importante).

Bases de números

Así que aquí es donde las bases numéricas son cruciales. Si intentamos representar 2/3 en la base 3, entonces

(2/3) 10 = 0.2 3

En otras palabras, tenemos una representación exacta y finita para el mismo número cambiando de base. La conclusión es que, aunque puede convertir cualquier número a cualquier base, todos los números racionales tienen representaciones finitas exactas en algunas bases pero no en otras .

Para conducir este punto a casa, veamos 1/2. Puede sorprenderle que a pesar de que este número perfectamente simple tiene una representación exacta en la base 10 y 2, requiere una representación repetitiva en la base 3.

(1/2) 10 = 0.5 10 = 0.1 2 = 0.1111 ... 3

¿Por qué los números de coma flotante son inexactos?

Debido a que a menudo son aproximaciones racionales que no se pueden representar de manera finita en la base 2 (se repiten los dígitos), y en general se aproximan a números reales (posiblemente irracionales) que pueden no ser representables en muchos dígitos en una base.

Nicu Stiurca
fuente
3
En otras palabras, base-3 sería perfecto para 1/3igual que base-10 es perfecto para 1/10. Ninguna fracción funciona en base-2
mhlester
2
@mhlester Sí. Y en general, la base-N es perfecta para cualquier fracción cuyo denominador sea No un múltiplo de la misma.
Nicu Stiurca
2
Y esta es una razón por la cual algunas cajas de herramientas numéricas hacen un seguimiento de "lo que se dividió por qué", y en el proceso pueden mantener una "precisión infinita" para todos los números racionales. Al igual que a los físicos les gusta mantener sus ecuaciones simbólicas hasta el último momento posible, en caso de que se πcancelen factores de etc.
Floris
3
@Floris También he visto casos en los que un algoritmo que solo realiza aritmética básica (es decir, conserva la racionalidad de la entrada), determina si la entrada fue (probable) racional, realiza las matemáticas usando aritmética de coma flotante normal y luego vuelve a estimar una racional aproximación al final para corregir cualquier error de redondeo. En particular, el algoritmo de forma escalonada reducida de Matlab hace esto, y ayuda enormemente a la estabilidad numérica.
Nicu Stiurca
@SchighSchagh: interesante, no lo sabía. Sé que la estabilidad numérica es algo que no se enseña lo suficiente en estos días de doble doble precisión. Lo que significa que muchos extrañan aprender sobre la elegancia de muchos algoritmos hermosos. Realmente me gustan los algoritmos que calculan y corrigen sus propios errores.
Floris
13

Si bien todas las otras respuestas son buenas, todavía falta una cosa:

Es imposible representar números irracionales (por ejemplo π, sqrt(2), log(3), etc.), precisamente!

Y esa es la razón por la que se les llama irracionales. Ninguna cantidad de almacenamiento de bits en el mundo sería suficiente para contener incluso uno de ellos. Solo la aritmética simbólica puede preservar su precisión.

Aunque si limitara sus necesidades matemáticas a números racionales, solo el problema de la precisión se vuelve manejable. Debería almacenar un par de enteros (posiblemente muy grandes) ay bmantener el número representado por la fracción a/b. Toda su aritmética tendría que hacerse en fracciones al igual que en matemáticas de secundaria (por ejemplo a/b * c/d = ac/bd).

Pero, por supuesto, todavía se encontraría con el mismo tipo de problemas cuando pi, sqrt, log, sin, etc., están involucrados.

TL; DR

Para la aritmética acelerada por hardware, solo se puede representar una cantidad limitada de números racionales. Cada número no representable es aproximado. Algunos números (es decir, irracionales) nunca pueden representarse sin importar el sistema.

Bulto
fuente
44
Curiosamente, existen bases irracionales. Phinary , por ejemplo.
Veedrac
55
los números irracionales pueden ser (solo) representados en su base. Por ejemplo, pi es 10 en base pi
phuclv
44
El punto sigue siendo válido: algunos números nunca pueden representarse sin importar el sistema. No gana nada cambiando su base porque entonces algunos otros números ya no se pueden representar.
LumpN
4

Hay infinitos números reales (tantos que no puedes enumerarlos), y hay infinitos números racionales (es posible enumerarlos).

La representación de punto flotante es finita (como cualquier cosa en una computadora), por lo que inevitablemente muchos, muchos, muchos números son imposibles de representar. En particular, 64 bits solo le permiten distinguir entre solo 18,446,744,073,709,551,616 valores diferentes (que no es nada en comparación con el infinito). Con la convención estándar, 9.2 no es uno de ellos. Los que pueden son de la forma m.2 ^ e para algunos enteros mye.


Podría encontrar un sistema de numeración diferente, 10 basado, por ejemplo, donde 9.2 tendría una representación exacta. Pero otros números, digamos 1/3, aún serían imposibles de representar.


También tenga en cuenta que los números de coma flotante de doble precisión son extremadamente precisos. Pueden representar cualquier número en un rango muy amplio con hasta 15 dígitos exactos. Para los cálculos de la vida diaria, 4 o 5 dígitos son más que suficientes. Nunca necesitará esos 15, a menos que quiera contar cada milisegundo de su vida.

Yves Daoust
fuente
1

¿Por qué no podemos representar 9.2 en coma flotante binaria?

Los números de punto flotante son (simplificando ligeramente) un sistema de numeración posicional con un número restringido de dígitos y un punto de raíz móvil.

Una fracción solo se puede expresar exactamente usando un número finito de dígitos en un sistema de numeración posicional si los factores primos del denominador (cuando la fracción se expresa en sus términos más bajos) son factores de la base.

Los factores primos de 10 son 5 y 2, por lo que en la base 10 podemos representar cualquier fracción de la forma a / (2 b 5 c ).

Por otro lado, el único factor primo de 2 es 2, por lo que en la base 2 solo podemos representar fracciones de la forma a / (2 b )

¿Por qué las computadoras usan esta representación?

Porque es un formato simple para trabajar y es lo suficientemente preciso para la mayoría de los propósitos. Básicamente, la misma razón por la que los científicos usan la "notación científica" y redondean sus resultados a un número razonable de dígitos en cada paso.

Sin duda sería posible definir un formato de fracción, con (por ejemplo) un numerador de 32 bits y un denominador de 32 bits. Sería capaz de representar números que el punto flotante de precisión doble IEEE no podría, pero igualmente habría muchos números que pueden representarse en punto flotante de precisión doble que no podrían representarse en un formato de fracción de tamaño fijo.

Sin embargo, el gran problema es que un formato de este tipo es difícil de hacer. Por dos razones.

  1. Si desea tener exactamente una representación de cada número, luego de cada cálculo debe reducir la fracción a sus términos más bajos. Eso significa que para cada operación básicamente necesita hacer un cálculo máximo de divisor común.
  2. Si después de su cálculo termina con un resultado no representable debido al numerador o denominador que necesita para encontrar el resultado representable más cercano. Esto no es civil.

Algunos idiomas ofrecen tipos de fracciones, pero generalmente lo hacen en combinación con precisión arbitraria, esto evita tener que preocuparse por aproximar fracciones pero crea su propio problema, cuando un número pasa a través de una gran cantidad de pasos de cálculo del tamaño del denominador y por lo tanto, el almacenamiento necesario para la fracción puede explotar.

Algunos idiomas también ofrecen tipos de coma flotante decimal, estos se utilizan principalmente en escenarios en los que es importante que los resultados que obtiene la computadora coincidan con las reglas de redondeo preexistentes que se escribieron teniendo en cuenta a los humanos (principalmente cálculos financieros). Estos son un poco más difíciles de trabajar que el punto flotante binario, pero el mayor problema es que la mayoría de las computadoras no ofrecen soporte de hardware para ellos.

lavado
fuente
-4

Prueba esto

DecimalFormat decimalFormat = new DecimalFormat("#.##");
String.valueOf(decimalFormat.format(decimalValue))));

' decimalValue' es su valor para convertir.

Popal
fuente