binary - error - floating point representation
¿La precisión del punto flotante es mutable o invariable? (10)
¿Es la precisión del punto flotante mutable o invariante, y por qué?
Normalmente, dado cualquier número en el mismo rango de potencia de 2, la precisión del punto flotante es invariable, un valor fijo. La precisión absoluta cambia con cada paso de potencia de 2. En todo el rango FP, la precisión es aproximadamente relativa a la magnitud. Al relacionar esta precisión binaria relativa en términos de precisión decimal, se DBL_DIG
un oscilación que varía entre los dígitos decimales DBL_DIG
y DBL_DECIMAL_DIG
. Típicamente de 15 a 17.
¿Qué es la precisión? Con FP, tiene más sentido discutir la precisión relativa .
Los números de coma flotante tienen la forma de:
Signo * Significand * pow (base, exponente)
Ellos tienen una distribución logarítmica. Hay aproximadamente tantos números diferentes de punto flotante entre 100.0 y 3000.0 (un rango de 30x) como entre 2.0 y 60.0. Esto es cierto independientemente de la representación de almacenamiento subyacente.
1.23456789e100
tiene aproximadamente la misma precisión relativa que 1.23456789e-100
.
La mayoría de las computadoras implementan el double
como binary64 . Este formato tiene 53 bits de precisión binaria .
Los n
números entre 1.0 y 2.0 tienen la misma precisión absoluta de 1 parte en ((2.0-1.0) / pow (2,52).
Los números entre 64.0 y 128.0, también n
, tienen la misma precisión absoluta de 1 parte en ((128.0-64.0) / pow (2,52).
Incluso un grupo de números entre potencias de 2 tiene la misma precisión absoluta.
En todo el rango normal de números FP, esto se aproxima a una precisión relativa uniforme.
Cuando estos números se representan como decimales, la precisión se tambalea : los números 1.0 a 2.0 tienen 1 bit más de precisión absoluta que los números 2.0 a 4.0. 2 bits más que 4.0 a 8.0, etc.
C proporciona DBL_DIG
, DBL_DECIMAL_DIG
, y su float
y sus contrapartes long double
. DBL_DIG
indica la precisión decimal relativa mínima. DBL_DECIMAL_DIG
se puede considerar como la máxima precisión decimal relativa .
Normalmente, esto significa que el double
dado tendrá de 15 a 17 dígitos decimales de precisión.
Considere 1.0
y su siguiente double
representable, los dígitos no cambian hasta el decimoséptimo dígito decimal significativo. Cada próximo double
es pow(2,-52)
o aproximadamente 2.2204e-16
aparte.
/*
1 234567890123456789 */
1.000000000000000000...
1.000000000000000222...
Ahora considere "8.521812787393891"
y su próximo número representable como una cadena decimal usando 16 dígitos decimales significativos. Ambas cadenas, convertidas al double
son las mismas 8.521812787393891142073699...
aunque difieren en el decimosexto dígito. Decir que este double
tenía 16 dígitos de precisión fue exagerado.
/*
1 234567890123456789 */
8.521812787393891
8.521812787393891142073699...
8.521812787393892
Sigo obteniendo respuestas mixtas sobre si los números de coma flotante (es decir, float
, double
o long double
) tienen un solo valor de precisión o tienen un valor de precisión que puede variar.
Un tema llamado flotación vs. doble precisión parece implicar que la precisión del punto flotante es absoluta.
Sin embargo, otro tema llamado Diferencia entre float y double dice:
En general, un doble tiene de 15 a 16 dígitos decimales de precisión
Otra source dice:
Las variables de tipo flotante suelen tener una precisión de aproximadamente 7 dígitos significativos
Las variables de tipo doble suelen tener una precisión de alrededor de 16 dígitos significativos
No me gusta referirme a aproximaciones como las anteriores si estoy trabajando con código sensible que se puede romper fácilmente cuando mis valores no son exactos. Así que vamos a dejar las cosas claras. ¿Es la precisión del punto flotante mutable o invariante, y por qué?
Agregaré aquí la respuesta poco convencional y diré que, dado que ha etiquetado esta pregunta como C ++, no hay garantía alguna sobre la precisión de los datos de coma flotante. La gran mayoría de las implementaciones usan IEEE-754 cuando implementan sus tipos de punto flotante, pero eso no es necesario. Lo único requerido por el lenguaje C ++ es que (especificación C ++ §3.9.1.8):
Hay tres tipos de puntos de flotación: flotante, doble y larga doble. El tipo double proporciona al menos tanta precisión como float, y el tipo double double proporciona al menos tanta precisión como el doble. El conjunto de valores del tipo float es un subconjunto del conjunto de valores del tipo double; el conjunto de valores del tipo double es un subconjunto del conjunto de valores del tipo double double. La representación del valor de los tipos de puntos flotantes está definida por la implementación . Los tipos integrales y flotantes se denominan colectivamente tipos aritméticos. Las especializaciones de la plantilla estándar std :: numeric_limits (18.3) especificarán los valores máximos y mínimos de cada tipo aritmético para una implementación.
Bueno, la respuesta a esto es simple pero complicado. Estos números se almacenan en binario. Dependiendo de si es un flotador o un doble, la computadora usa diferentes cantidades de binarios para almacenar el número. La precisión que obtienes depende de tu binario. Si no sabe cómo funcionan los números binarios, sería una buena idea buscarlos. Pero en pocas palabras, algunos números necesitan más unos y ceros que otros números.
Entonces la precisión es fija (el mismo número de dígitos binarios), pero la precisión real que obtienes depende de los números que estás usando.
El almacenamiento tiene un conteo de dígitos preciso en binario, como explican otras respuestas.
Una cosa a saber, la CPU puede ejecutar operaciones con una precisión diferente internamente, como 80 bits. Significa que un código como ese puede desencadenar:
void Kaboom( float a, float b, float c ) // same is true for other floating point types.
{
float sum1 = a+b+c;
float sum2 = a+b;
sum2 += c; // let''s assume that the compiler did not keep sum2 in a register and the value was write to memory then load again.
if (sum1 !=sum2)
throw "kaboom"; // this can happen.
}
Es más probable con cálculos más complejos.
El código 80x86 que utiliza su coprocesador de hardware (originalmente el 8087) proporciona tres niveles de precisión: 32 bits, 64 bits y 80 bits. Esos siguen muy de cerca el estándar IEEE-754 de 1985. El estándar reciente especifica un formato de 128 bits . Los formatos de coma flotante tienen 24, 53, 65 y 113 bits de mantisa que corresponden a 7.22, 15.95, 19.57 y 34.02 dígitos decimales de precisión.
La fórmula es mantissa_bits / log_2 10 donde la base de registro dos de diez es 3.321928095.
Si bien la precisión de una implementación en particular no varía, puede aparecer cuando un valor de coma flotante se convierte a decimal. Tenga en cuenta que el valor 0.1
no tiene una representación binaria exacta. Es un patrón de bits repetitivo (0.0001100110011001100110011001100 ...) como estamos acostumbrados en decimal para 0.3333333333333 para aproximar 1/3.
Muchos idiomas a menudo no son compatibles con el formato de 80 bits. Algunos compiladores de C pueden ofrecer long double
que usa flotadores de 80 bits o flotadores de 128 bits. Por desgracia, también podría usar un flotador de 64 bits, dependiendo de la implementación.
La NPU tiene registros de 80 bits y realiza todas las operaciones utilizando el resultado completo de 80 bits. El código que calcula dentro de la pila NPU se beneficia con esta precisión adicional. Desafortunadamente, la generación de código deficiente, o código mal escrito, puede truncar o redondear cálculos intermedios almacenándolos en una variable de 32 o 64 bits.
El tipo de variable de coma flotante define qué rango de valores y cuántos bits fraccionarios (!) Se pueden representar. Como no existe una relación entera entre fracción decimal y binaria, la fracción decimal es en realidad una aproximación.
Segundo: Otro problema es que se realizan operaciones aritméticas de precisión. Solo piense en 1.0/3.0
o PI. Dichos valores no se pueden representar con un número limitado de dígitos, ni decimal, ni binario. Entonces los valores tienen que ser redondeados para encajar en el espacio dado. Cuantos más dígitos fraccionarios estén disponibles, mayor será la precisión.
Ahora piense en la aplicación de múltiples operaciones de este tipo, por ejemplo, PI / 3.0. Esto requeriría redondear dos veces: PI como tal no es exacto y el resultado tampoco. Esto perderá precisión dos veces, si se repite, empeorará.
Por lo tanto, volver a float
y double
: el float
tiene de acuerdo con el estándar (C11, Anexo F, también para el resto) menos bits disponibles, por lo que roundig será menos preciso que para el double
. Solo piense en tener un decimal con 2 dígitos fraccionarios (m.ff, llámelo flotación) y uno con cuatro (m.ffff, llámelo doble). Si se usa el doble para todos los cálculos, puede tener más operaciones hasta que su resultado tenga solo 2 dígitos fraccionarios correctos, que si ya comienza con flotación, incluso si un resultado de flotación sería suficiente.
Tenga en cuenta que en algunas CPU (integradas) como ARM Cortex-M4F, el hardware FPU solo admite folat (precisión simple), por lo que la aritmética doble será mucho más costosa. Otros MCU no tienen ninguna calculadora de punto flotante de hardware, por lo que tienen que simular mi software (muy costoso). En la mayoría de las GPU, la flotación también es mucho más barata de realizar que el doble, a veces más de un factor de 10.
La cantidad de espacio requerido para almacenar un float
será constante, y también un double
; sin embargo, la cantidad de precisión útil en términos relativos variará entre una parte en 2 23 y una parte en 2 24 para float
, o una parte en 2 52 y 2 53 para double
. La precisión muy cercana a cero no es tan buena, con el segundo valor positivo más pequeño que es dos veces más grande que el más pequeño, que a su vez será infinitamente mayor que cero. Sin embargo, en la mayor parte del rango, la precisión variará según lo descrito anteriormente.
Tenga en cuenta que aunque a menudo no es práctico tener tipos cuya precisión relativa varíe en menos de un factor de dos a lo largo de su rango, la variación en la precisión a veces puede hacer que los cálculos arrojen cálculos mucho menos precisos de lo que parece. Considere, por ejemplo, 16777215.0f + 4.0f - 4.0f
. Todos los valores se pueden representar con precisión como float
usando la misma escala, y los valores más cercanos a los más grandes son +/- una parte en 16,777,215, pero la primera suma arroja un resultado en parte del rango float
donde los valores están separados por una parte en solo 8,388,610, lo que hace que el resultado se redondee a 16.777.220. En consecuencia, restando 4 rendimientos 16.777.216 en lugar de 16.777.215. Para la mayoría de los valores de float
cerca de 16777216
, al sumar 4.0f
y restar 4.0f
obtendría el valor original sin cambios, pero la precisión cambiante en el punto de ruptura ocasiona que el resultado se desactive en un bit adicional en el lugar más bajo.
La precisión es fija, que es exactamente 53 dígitos binarios para la precisión doble (o 52 si excluimos la ventaja implícita 1). Esto sale a aproximadamente 15 dígitos decimales .
El OP me pidió que explicara por qué tener exactamente 53 dígitos binarios significa "aproximadamente" 15 dígitos decimales.
Para entender esto de manera intuitiva, consideremos un formato de coma flotante menos preciso: en lugar de una mantisa de 52 bits como la que tienen los números de doble precisión, solo vamos a usar una mantisa de 4 bits.
Entonces, cada número se verá como: (-1) s × 2 yyy × 1.xxxx (donde s
es el bit de signo, yyy
es el exponente, y 1.xxxx
es la mantisa normalizada). Para la discusión inmediata, nos enfocaremos solo en la mantisa y no en el signo o exponente.
Aquí hay una tabla de cómo se ve 1.xxxx
para todos los valores xxxx
(todo el redondeo es igual a la mitad, al igual que el modo de redondeo de coma flotante predeterminado):
xxxx | 1.xxxx | value | 2dd | 3dd
--------+----------+----------+-------+--------
0000 | 1.0000 | 1.0 | 1.0 | 1.00
0001 | 1.0001 | 1.0625 | 1.1 | 1.06
0010 | 1.0010 | 1.125 | 1.1 | 1.12
0011 | 1.0011 | 1.1875 | 1.2 | 1.19
0100 | 1.0100 | 1.25 | 1.2 | 1.25
0101 | 1.0101 | 1.3125 | 1.3 | 1.31
0110 | 1.0110 | 1.375 | 1.4 | 1.38
0111 | 1.0111 | 1.4375 | 1.4 | 1.44
1000 | 1.1000 | 1.5 | 1.5 | 1.50
1001 | 1.1001 | 1.5625 | 1.6 | 1.56
1010 | 1.1010 | 1.625 | 1.6 | 1.62
1011 | 1.1011 | 1.6875 | 1.7 | 1.69
1100 | 1.1100 | 1.75 | 1.8 | 1.75
1101 | 1.1101 | 1.8125 | 1.8 | 1.81
1110 | 1.1110 | 1.875 | 1.9 | 1.88
1111 | 1.1111 | 1.9375 | 1.9 | 1.94
¿Cuántos dígitos decimos que proporciona? Podría decir 2, ya que cada valor en el rango de dos dígitos decimales está cubierto, aunque no de forma exclusiva; o podría decir 3, que cubre todos los valores únicos, pero no proporciona cobertura para todos los valores en el rango de tres dígitos decimales.
En aras de la argumentación, diremos que tiene 2 dígitos decimales: la precisión decimal será el número de dígitos donde se pueden representar todos los valores de esos dígitos decimales.
De acuerdo, entonces, ¿qué sucede si dividimos a la mitad todos los números (entonces estamos usando yyy
= -1)?
xxxx | 1.xxxx | value | 1dd | 2dd
--------+----------+-----------+-------+--------
0000 | 1.0000 | 0.5 | 0.5 | 0.50
0001 | 1.0001 | 0.53125 | 0.5 | 0.53
0010 | 1.0010 | 0.5625 | 0.6 | 0.56
0011 | 1.0011 | 0.59375 | 0.6 | 0.59
0100 | 1.0100 | 0.625 | 0.6 | 0.62
0101 | 1.0101 | 0.65625 | 0.7 | 0.66
0110 | 1.0110 | 0.6875 | 0.7 | 0.69
0111 | 1.0111 | 0.71875 | 0.7 | 0.72
1000 | 1.1000 | 0.75 | 0.8 | 0.75
1001 | 1.1001 | 0.78125 | 0.8 | 0.78
1010 | 1.1010 | 0.8125 | 0.8 | 0.81
1011 | 1.1011 | 0.84375 | 0.8 | 0.84
1100 | 1.1100 | 0.875 | 0.9 | 0.88
1101 | 1.1101 | 0.90625 | 0.9 | 0.91
1110 | 1.1110 | 0.9375 | 0.9 | 0.94
1111 | 1.1111 | 0.96875 | 1. | 0.97
Con los mismos criterios que antes, ahora nos ocupamos de 1 dígito decimal. Entonces puede ver cómo, dependiendo del exponente, puede tener más o menos dígitos decimales, porque los números de coma flotante binarios y decimales no se mapean limpiamente entre sí .
El mismo argumento se aplica a los números de coma flotante de precisión doble (con la mantisa de 52 bits), solo en ese caso obtendrá 15 o 16 dígitos decimales según el exponente.
No, es variable. El punto de partida es el muy débil estándar IEEE-754, que solo identifica el formato de los números del puntero flotante a medida que se almacenan en la memoria. Puede contar con 7 dígitos de precisión para precisión simple, 15 dígitos para precisión doble.
Pero una falla importante en ese estándar es que no especifica cómo se realizarán los cálculos. Y hay problemas, el procesador de punto flotante Intel 8087 en particular ha causado muchas noches de insomnio a los programadores. Una falla de diseño significativa en ese chip es que almacena valores de coma flotante con más bits que el formato de memoria. 80 bits en lugar de 32 o 64. La teoría detrás de esa elección de diseño es que esto permite que los cálculos intermedios sean más precisos y causen menos errores de redondeo.
Suena como una buena idea, que sin embargo no resultó bien en la práctica. Un escritor de compiladores intentará generar código que deje los valores intermedios almacenados en la FPU el mayor tiempo posible. Importante para codificar la velocidad, almacenar el valor en la memoria es costoso. El problema es que a menudo debe almacenar los valores, el número de registros en la FPU es limitado y el código puede cruzar un límite de función. En ese punto, el valor se trunca y pierde mucha precisión. Pequeños cambios en el código fuente ahora pueden producir valores drásticamente diferentes. Además, la compilación no optimizada de un programa produce resultados diferentes a los optimizados. De una forma completamente no diagnosticable, tendría que mirar el código de la máquina para saber por qué el resultado es diferente.
Intel rediseñó su procesador para resolver este problema, el conjunto de instrucciones SSE se calcula con el mismo número de bits que el formato de memoria. Sin embargo, lento para ponerse al día, el rediseño del generador de código y el optimizador de un compilador es una inversión importante. Los tres grandes compiladores de C ++ han cambiado todos. Pero, por ejemplo, la inestabilidad x86 en .NET Framework aún genera código FPU, siempre lo hará.
Luego está el error sistémico, que pierde precisión como efecto secundario inevitable de la conversión y el cálculo. Conversión primero, los humanos trabajamos en números en la base 10, pero el procesador usa la base 2. Los números redondos que usamos, como 0.1, no se pueden convertir en buenos números redondos en el procesador. 0.1 es perfecto como una suma de potencias de 10, pero no hay una suma finita de potencias de 2 que produzcan el mismo valor. Convirtiéndolo produce un número infinito de 1s y 0s de la misma manera que no puede escribir perfectamente 10 / 3. Por lo tanto, necesita ser truncado para adaptarse al procesador y que produce un valor que está desactivado en +/- 0.5 bit desde el valor decimal
Y el cálculo produce error. Una multiplicación o división duplica la cantidad de bits en el resultado, redondeándola para volver a colocarla en el valor almacenado produce un error de +/- 0.5 bit. La resta es la operación más peligrosa y puede causar la pérdida de muchos dígitos significativos. Si, por ejemplo, calcula 1.234567f - 1.234566f, el resultado solo tiene 1 dígito significativo. Eso es un resultado basura. Sumar la diferencia entre los números que tienen casi el mismo valor es muy común en los algoritmos numéricos.
Obtener errores sistémicos excesivos es, en última instancia, un defecto en el modelo matemático. Solo como ejemplo, nunca se quiere usar la eliminación gaussiana, es muy antipático con la precisión. Y siempre considere un enfoque alternativo, LU Descomposición es un excelente enfoque. Sin embargo, no es tan común que un matemático participe en la construcción del modelo y tenga en cuenta la precisión esperada del resultado. Un libro común como Recetas numéricas tampoco presta suficiente atención a la precisión, aunque indirectamente te aleja de los malos modelos al proponer el mejor. Al final, un programador a menudo se queda con el problema. Bueno, era fácil que alguien pudiera hacerlo y estaría fuera de un buen trabajo de pago :)
Todas las computadoras modernas usan aritmética binaria en coma flotante. Eso significa que tenemos una mantisa binaria, que tiene típicamente 24 bits para precisión simple, 53 bits para precisión doble y 64 bits para precisión extendida. (La precisión extendida está disponible en procesadores x86, pero no en ARM o posiblemente en otros tipos de procesadores).
Las mantisas de 24, 53 y 64 bits significan que para un número de punto flotante entre 2 ky 2 k + 1, el siguiente número más grande es 2 k-23 , 2 k-52 y 2 k-63, respectivamente. Esa es la resolución. El error de redondeo de cada operación de coma flotante es como máximo la mitad de eso.
Entonces, ¿cómo se traduce eso en números decimales? Depende
Tome k = 0 y 1 ≤ x <2. La resolución es 2 -23 , 2 -52 y 2 -63, que es aproximadamente 1.19 × 10 -7 , 2.2 × 10 -16 y 1.08 × 10 -19 respectivamente. Eso es un poco menos de 7, 16 y 19 decimales. Luego toma k = 3 y
8 ≤ x <16. La diferencia entre dos números de punto flotante es ahora 8 veces mayor. Para 8 ≤ x <10 obtienes un poco más de 6, menos de 15 y algo más de 18 decimales, respectivamente. ¡Pero para 10 ≤ x <16 obtienes un decimal más!
Obtiene el mayor número de dígitos decimales si x es solo un poco menor que 2 k + 1 y solo un poco más de 10 n , por ejemplo 1000 ≤ x <1024. Obtiene el menor número de dígitos decimales si x es solo un poco más de 2 ky un poco menos de 10 n , por ejemplo 1/1024 ≤ x < 1/1000 . La misma precisión binaria puede producir precisión decimal que varía hasta 1.3 dígitos o log 10 (2 × 10).
Por supuesto, podría leer el artículo " Lo que todo científico de la computación debería saber sobre la aritmética de coma flotante ".