single simple punto partes numeros numero hexadecimal flotantes flotante float estandar ejemplos coma binario language-agnostic floating-point floating-point-precision

language-agnostic - simple - single precision floating point



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

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

Por ejemplo, el número decimal 9.2 se puede expresar exactamente como una razó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 expresar en 64 bits de memoria?


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

5179139571476070 * 2 -49

Donde el exponente es -49 y la mantisa es 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 flotantes deben ser un número entero multiplicado por una potencia entera de 2 .

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

Ver los datos

Primero, algunas funciones para ver los componentes que forman un float 32 y 64 bits. Brilla sobre estos si solo te 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 complejidad detrás de esa función, y sería bastante la tangente para explicar, pero si estás interesado, el recurso importante para nuestros propósitos es el módulo struct .

El float de Python es un número de 64 bits de precisión doble. En otros lenguajes como C, C ++, Java y C #, la precisión doble tiene un double tipo separado, 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ás que dividí el valor devuelto en tres componentes. Estos componentes son:

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

Firmar

El signo se almacena en el primer componente como un solo bit. Es fácil de explicar: 0 significa que el flotador es un número positivo; 1 significa que es negativo. Como 9.2 es 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 entender 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 vale para binario, excepto que el binario solo tiene dos dígitos: 0 y 1 . ¡Entonces la mantisa binaria siempre comienza con 1 ! Cuando se almacena un flotador, el 1 en el frente de la mantisa binaria se omite para ahorrar espacio; tenemos que colocarlo nuevamente al frente de nuestro tercer elemento para obtener la mantisa verdadera :

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 la base .

Al tratar 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, es lo mismo que dividir 675539944105574 por 4503599627370496 para obtener 0.1499999999999999 . (Este es un ejemplo de una proporción que se puede expresar exactamente en binario, pero solo aproximadamente en decimal; para obtener más detalles, consulte: 675539944105574/4503599627370496 ).

Ahora que hemos transformado el tercer componente en un número fraccionario, al agregar 1 obtiene la mantisa verdadera.

Reaplicando los componentes

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

Cálculo del número

Juntando las tres partes, nos da este número binario:

1.0010011001100110011001100110011001100110011001100110 x 10 11

Que 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 haber sido 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

Convertir a decimal:

5179139571476070 x 2 3-52

Reste 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 por un montón de ceros. Pero sigamos los pasos.

Montar la notación científica binaria:

1.0011 x 10 11

Cambia el punto decimal:

10011 x 10 11-100

Reste 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:

9.5

Otras lecturas


Esta no es una respuesta completa ( mhlester ya cubrió una buena cantidad de terreno que no voy a duplicar), pero me gustaría enfatizar en qué medida la representación de un número depende de la base en la que esté trabajando.

Considere la fracción 2/3

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

  • 0.666 ...
  • 0.666
  • 0.667

Cuando observamos esas representaciones, tendemos a asociar cada una de ellas con la fracción 2/3, aunque solo la primera representación sea matemáticamente igual a la fracción. La segunda y tercera representaciones / aproximaciones 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 está redondeada 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ía haber un problema con la aproximación de 9.2 en la mayoría de los programas . (Sí, en algunos programas importa)

Bases numéricas

Así que aquí es donde las bases numéricas son crutiales. Si intentáramos representar 2/3 en la base 3, entonces

(2/3) 10 = 0.2 3

En otras palabras, ¡tenemos una representación finita y exacta para el mismo número cambiando las bases! Lo que se lleva es que, aunque puedes convertir cualquier número en cualquier base, todos los números racionales tienen representaciones finitas exactas en algunas bases pero no en otras .

Para llevar este punto a casa, veamos 1/2. Puede sorprenderle que a pesar de que este número perfectamente simple tenga una representación exacta en la base 10 y 2, requiera 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 muchas veces, se aproximan a racionales que no se pueden representar finitamente en la base 2 (los dígitos se repiten), y en general se aproximan a números reales (posiblemente irracionales) que pueden no ser representables en finitos dígitos en cualquier base.


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 llaman irracionales. Ninguna cantidad de almacenamiento de bits en el mundo sería suficiente para contener ni siquiera uno de ellos. Solo la aritmética simbólica puede preservar su precisión.

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

Pero, por supuesto, aún te encontrarás 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 se pueden representar sin importar el sistema.