valor science flotante errors error computer c++ floating-point hardware cpu

c++ - science - floating point relative error



¿De qué se trata esta "información denormal"?-C++ (4)

Me gustaría tener una visión amplia acerca de los "datos denormales" y de qué se trata porque lo único que creo que entendí es el hecho de que es algo especialmente relacionado con los valores de punto flotante desde un punto de vista del programador y está relacionado con un Enfoque informático desde el punto de vista de la CPU.

Alguien puede descifrar estas 2 palabras para mí?

EDITAR

recuerde que estoy orientado a las aplicaciones de C ++ y solo al lenguaje C ++.


De la Documentación IEEE

Si el exponente es todo 0s, pero la fracción es distinta de cero (de lo contrario, se interpretaría como cero), entonces el valor es un número desnormalizado, que no tiene un 1 inicial supuesto antes del punto binario. Por lo tanto, esto representa un número (-1) s × 0.f × 2-126, donde s es el bit de signo yf es la fracción. Para precisión doble, los números desnormalizados son de la forma (-1) s × 0.f × 2-1022. A partir de esto, puede interpretar cero como un tipo especial de número desnormalizado.


Para comprender los valores de punto flotante anormales, primero debe comprender los valores normales. Un valor de punto flotante tiene una mantisa y un exponente. En un valor decimal, como 1.2345E6, 1.2345 es la mantisa, 6 es el exponente. Una cosa buena acerca de la notación de punto flotante es que siempre se puede escribir normalizado. Como 0.012345E8 y 0.12345E7 es el mismo valor que 1.2345E6. O, en otras palabras, siempre puede hacer que el primer dígito de la mantisa sea un número distinto de cero, siempre que el valor no sea cero.

Las computadoras almacenan valores de punto flotante en binario, los dígitos son 0 o 1. Por lo tanto, una propiedad de un valor de punto flotante binario que no es cero es que siempre se puede escribir comenzando con un 1.

Este es un objetivo de optimización muy atractivo. Como el valor siempre comienza con 1, no tiene sentido almacenar ese 1 . Lo bueno de esto es que, en efecto, obtiene un poco más de precisión de forma gratuita. En un doble de 64 bits, la mantisa tiene 52 bits de almacenamiento. La precisión real es de 53 bits gracias al 1 implícito.

Tenemos que hablar sobre el valor de punto flotante más pequeño posible que pueda almacenar de esta manera. Hágalo primero en decimal, si tenía un procesador decimal con 5 dígitos de almacenamiento en la mantisa y 2 en el exponente, entonces el valor más pequeño que podría almacenar que no sea cero es 1.00000E-99. Con 1 es el dígito implícito que no se almacena (no funciona en decimal sino que soporta conmigo). Así que la mantisa almacena 00000 y el exponente almacena -99. No puede almacenar un número más pequeño, el exponente está en el máximo en -99.

Bien tu puedes. Podría renunciar a la representación normalizada y olvidarse de la optimización de dígitos implícita. Puede almacenarlo des-normalizado . Ahora puedes almacenar 0.1000E-99, o 1.000E-100. Hasta el final de 0.0001E-99 o 1E-103, el número más pequeño absoluto que ahora puede almacenar.

Esto es en general deseable, amplía el rango de valores que puede almacenar. Lo que tiende a importar en los cálculos prácticos, los números muy pequeños son muy comunes en los problemas del mundo real, como el análisis diferencial.

Sin embargo, también hay un gran problema, pierdes precisión con los números des-normalizados. La precisión de los cálculos de punto flotante está limitada por el número de dígitos que puede almacenar. Es intuitivo con el falso procesador decimal que usé como ejemplo, solo puede calcular con 5 dígitos significativos. Mientras el valor se normalice, siempre obtendrá 5 dígitos significativos.

Pero perderás dígitos cuando des-normalices. Cualquier valor entre 0.1000E-99 y 0.9999E-99 tiene solo 4 dígitos significativos. Cualquier valor entre 0.0100E-99 y 0.0999E-99 tiene solo 3 dígitos significativos. Todo el camino hasta 0.0001E-99 y 0.0009E-99, solo queda un dígito significativo.

Esto puede reducir en gran medida la precisión del resultado del cálculo final. Lo que es peor, lo hace de una manera altamente impredecible ya que estos valores des-normalizados muy pequeños tienden a aparecer en un cálculo más complejo. Eso es ciertamente algo de qué preocuparse, ya no puede confiar en el resultado final cuando solo le queda un dígito significativo.

Los procesadores de punto flotante tienen formas de informarle acerca de esto o de lo contrario resolver el problema. Por ejemplo, pueden generar una interrupción o señal cuando un valor se desactualiza, lo que le permite interrumpir el cálculo. Y tienen una opción de "descarga a cero", un bit en la palabra de estado que le dice al procesador que convierta automáticamente todos los valores fuera de lo normal a cero. Lo que tiende a generar infinitos, un resultado que le dice que el resultado es basura y que debe descartarse.


Usted pregunta acerca de C ++, pero las especificaciones de los valores y codificaciones de punto flotante están determinadas por una especificación de punto flotante, en particular IEEE 754, y no por C ++. IEEE 754 es, con mucho, la especificación de punto flotante más utilizada, y responderé usándola.

En IEEE 754, los valores binarios de punto flotante se codifican con tres partes: un bit de signo s (0 para positivo, 1 para negativo), un exponente sesgado e (el exponente representado más un desplazamiento fijo) y un campo significativo f (el porción de la fracción). Para los números normales, estos representan exactamente el número (-1) s • 2 e - bias • 1. f , donde 1. f es el número binario formado al escribir los bits significativos después de "1". (Por ejemplo, si el campo significativo tiene los diez bits 0010111011, representa el significado 1.0010111011 2 , que es 1.182617175 o 1211/1024).

El sesgo depende del formato de punto flotante. Para el binario IEEE 754 de 64 bits, el campo del exponente tiene 11 bits y el sesgo es 1023. Cuando el exponente real es 0, el campo del exponente codificado es 1023. Exponentes reales de -2, -1, 0, 1 y 2 tienen exponentes codificados de 1021, 1022, 1023, 1024 y 1025. Cuando alguien habla de que el exponente de un número subnormal es cero, significa que el exponente codificado es cero. El exponente real sería menor que -1022. Para 64 bits, el intervalo de exponente normal es de -1022 a 1023 (valores codificados de 1 a 2046). Cuando el exponente se mueve fuera de este intervalo, suceden cosas especiales.

Por encima de este intervalo, las paradas de punto flotante representan números finitos. Un exponente codificado de 2047 (todos los 1 bits) representa el infinito (con el campo significativo establecido en cero). Por debajo de este rango, el punto flotante cambia a números subnormales. Cuando el exponente codificado es cero, el campo de significand representa 0. f en lugar de 1. f .

Hay una razón importante para esto. Si el valor de exponente más bajo fuera solo otra codificación normal, entonces los bits más bajos de su significado serían demasiado pequeños para representarlos como valores de punto flotante por sí mismos. Sin ese primer "1", no habría manera de decir dónde estaba el primer bit 1. Por ejemplo, supongamos que tiene dos números, ambos con el exponente más bajo, y con significands 1.0010111011 2 y 1.0000000000 2 . Cuando restas los significandos, el resultado es .0010111011 2 . Desafortunadamente, no hay manera de representar esto como un número normal. Debido a que ya estaba en el exponente más bajo, no puede representar el exponente más bajo que se necesita para decir dónde está el primer 1 en este resultado. Dado que el resultado matemático es demasiado pequeño para ser representado, una computadora se vería obligada a devolver el número representable más cercano, que sería cero.

Esto crea la propiedad indeseable en el sistema de punto flotante que puede tener a != b pero ab == 0 . Para evitar eso, se utilizan números subnormales. Al usar números subnormales, tenemos un intervalo especial en el que el exponente real no disminuye y podemos realizar operaciones aritméticas sin crear números demasiado pequeños para representar. Cuando el exponente codificado es cero, el exponente real es el mismo que cuando el exponente codificado es uno, pero el valor del significando cambia a 0. f en lugar de 1. f . Cuando hacemos esto, a != b garantiza que el valor calculado de ab no es cero.

Aquí están las combinaciones de valores en las codificaciones de punto flotante binario IEEE 754 de 64 bits:

Sign Exponent (e) Significand Bits (f) Meaning 0 0 0 +zero 0 0 Non-zero +2-1022•0.f (subnormal) 0 1 to 2046 Anything +2e-1023•1.f (normal) 0 2047 0 +infinity 0 2047 Non-zero but high bit off +, signaling NaN 0 2047 High bit on +, quiet NaN 1 0 0 -zero 1 0 Non-zero -2-1022•0.f (subnormal) 1 1 to 2046 Anything -2e-1023•1.f (normal) 1 2047 0 -infinity 1 2047 Non-zero but high bit off -, signaling NaN 1 2047 High bit on -, quiet NaN

Algunas notas:

+0 y -0 son matemáticamente iguales, pero el signo se conserva. Las aplicaciones escritas con cuidado pueden usarlo en ciertas situaciones especiales.

NaN significa "No es un número". Comúnmente, significa que ha ocurrido algún resultado no matemático u otro error, y un cálculo debe descartarse o rehacerse de otra manera. Generalmente, una operación con un NaN produce otro NaN, preservando así la información de que algo salió mal. Por ejemplo, 3 + NaN produce un NaN. Un NaN de señalización está destinado a causar una excepción, ya sea para indicar que un programa ha fallado o para permitir que otro software (por ejemplo, un depurador) realice alguna acción especial. Se pretende que un NaN silencioso se propague a otros resultados, lo que permite completar el resto de un cómputo grande, en los casos en que un NaN es solo una parte de un gran conjunto de datos y se manejará por separado más adelante o se descartará.

Los signos, + y -, se retienen con NaN pero no tienen valor matemático.

En la programación normal, no debe preocuparse por la codificación de punto flotante, excepto en la medida en que le informa sobre los límites y el comportamiento de los cálculos de punto flotante. No debería tener que hacer nada especial con respecto a los números subnormales.

Desafortunadamente, algunos procesadores no funcionan porque violan el estándar IEEE 754 al cambiar los números subnormales a cero o funcionan muy lentamente cuando se usan números subnormales. Al programar para tales procesadores, puede intentar evitar el uso de números subnormales.


Fundamentos de IEEE 754

Primero repasemos los fundamentos de IEEE 754, los números están organizados.

Centrémonos en la precisión simple (32 bits) primero.

El formato es:

  • 1 bit: signo
  • 8 bits: exponente
  • 23 bits: fracción

O si te gustan las fotos:

Source

El signo es simple: 0 es positivo y 1 es negativo, final de la historia.

El exponente tiene una longitud de 8 bits, por lo que oscila entre 0 y 255.

El exponente se llama polarizado porque tiene un desplazamiento de -127 , por ejemplo:

0 == special case: zero or subnormal, explained below 1 == 2 ^ -126 ... 125 == 2 ^ -2 126 == 2 ^ -1 127 == 2 ^ 0 128 == 2 ^ 1 129 == 2 ^ 2 ... 254 == 2 ^ 127 255 == special case: infinity and NaN

La convención de bit líder.

Al diseñar IEEE 754, los ingenieros notaron que todos los números, excepto 0.0 , tienen un 1 en binario como el primer dígito

P.ej:

25.0 == (binary) 11001 == 1.1001 * 2^4 0.625 == (binary) 0.101 == 1.01 * 2^-1

ambos comienzan con esa molesta 1. parte.

Por lo tanto, sería inútil dejar que ese dígito tome el bit de precisión en casi todos los números.

Por esta razón, crearon la "convención de bit líder":

Siempre asuma que el número comienza con uno

Pero entonces, ¿cómo lidiar con 0.0 ? Bueno, decidieron crear una excepción:

  • si el exponente es 0
  • y la fracción es 0
  • entonces el número representa más o menos 0.0

de modo que los bytes 00 00 00 00 también representan 0.0 , lo que se ve bien.

Si solo consideramos estas reglas, entonces el número más pequeño que no sea cero que se pueda representar sería:

  • exponente: 0
  • fracción: 1

que se ve algo así en una fracción hexadecimal debido a la convención de bits líder:

1.000002 * 2 ^ (-127)

donde .000002 es 22 ceros con un 1 al final.

No podemos tomar la fraction = 0 , de lo contrario ese número sería 0.0 .

Pero entonces los ingenieros, que también tenían un agudo sentido artístico, pensaron: ¿no es tan feo? ¿Que saltamos de la recta 0.0 a algo que ni siquiera es una potencia adecuada de 2? ¿No podríamos representar números aún más pequeños de alguna manera?

Numeros denormales

Los ingenieros se rascaron la cabeza por un rato y regresaron, como de costumbre, con otra buena idea. ¿Qué pasa si creamos una nueva regla:

Si el exponente es 0, entonces:

  • el bit inicial se convierte en 0
  • el exponente se fija en -126 (no en -127 como si no tuviéramos esta excepción)

Dichos números se denominan números subnormales (o números denormales, que es sinónimo).

Esta regla inmediatamente implica que el número tal que:

  • exponente: 0
  • fracción: 0

es 0.0 , lo que es bastante elegante, ya que significa una regla menos de la cual realizar un seguimiento.

¡Entonces 0.0 es un número subnormal según nuestra definición!

Con esta nueva regla, el número no subnormal más pequeño es:

  • exponente: 1 (0 sería subnormal)
  • fracción: 0

que representa:

1.0 * 2 ^ (-126)

Entonces, el mayor número subnormal es:

  • exponente: 0
  • fracción: 0x7FFFFF (23 bits 1)

que es igual a

0.FFFFFE * 2 ^ (-126)

donde .FFFFFE es una vez más 23 bits uno a la derecha del punto.

Esto está bastante cerca del número no subnormal más pequeño, que suena sano.

Y el número subnormal no cero más pequeño es:

  • exponente: 0
  • fracción: 1

que es igual a

0.000002 * 2 ^ (-126)

que también se ve bastante cerca de 0.0 !

Incapaces de encontrar una forma sensata de representar números más pequeños que eso, los ingenieros estaban contentos y volvieron a ver las fotos de gatos en línea, o lo que sea que hicieron en los años 70.

Como puede ver, los números subnormales hacen un equilibrio entre la precisión y la longitud de representación.

Como el ejemplo más extremo, el subnormal más pequeño que no es cero:

0.000002 * 2 ^ (-126)

Tiene esencialmente una precisión de un solo bit en lugar de 32 bits. Por ejemplo, si lo dividimos por dos:

0.000002 * 2 ^ (-126) / 2

¡Llegamos a 0.0 exactamente!

Ejemplo de Runnable C

Ahora vamos a jugar con un código real para verificar nuestra teoría.

En casi todas las máquinas actuales y de escritorio, C float representa números de punto flotante IEEE 754 de precisión simple.

Este es en particular el caso de mi computadora portátil Ubuntu 18.04 amd64.

Con esa suposición, todas las aserciones pasan sobre el siguiente programa:

subnormal.c

#if __STDC_VERSION__ < 201112L #error C11 required #endif #ifndef __STDC_IEC_559__ #error IEEE 754 not implemented #endif #include <assert.h> #include <float.h> /* FLT_HAS_SUBNORM */ #include <inttypes.h> #include <math.h> /* isnormal */ #include <stdlib.h> #include <stdio.h> #if FLT_HAS_SUBNORM != 1 #error float does not have subnormal numbers #endif typedef struct { uint32_t sign, exponent, fraction; } Float32; Float32 float32_from_float(float f) { uint32_t bytes; Float32 float32; bytes = *(uint32_t*)&f; float32.fraction = bytes & 0x007FFFFF; bytes >>= 23; float32.exponent = bytes & 0x000000FF; bytes >>= 8; float32.sign = bytes & 0x000000001; bytes >>= 1; return float32; } float float_from_bytes( uint32_t sign, uint32_t exponent, uint32_t fraction ) { uint32_t bytes; bytes = 0; bytes |= sign; bytes <<= 8; bytes |= exponent; bytes <<= 23; bytes |= fraction; return *(float*)&bytes; } int float32_equal( float f, uint32_t sign, uint32_t exponent, uint32_t fraction ) { Float32 float32; float32 = float32_from_float(f); return (float32.sign == sign) && (float32.exponent == exponent) && (float32.fraction == fraction) ; } void float32_print(float f) { Float32 float32 = float32_from_float(f); printf( "%" PRIu32 " %" PRIu32 " %" PRIu32 "/n", float32.sign, float32.exponent, float32.fraction ); } int main(void) { /* Basic examples. */ assert(float32_equal(0.5f, 0, 126, 0)); assert(float32_equal(1.0f, 0, 127, 0)); assert(float32_equal(2.0f, 0, 128, 0)); assert(isnormal(0.5f)); assert(isnormal(1.0f)); assert(isnormal(2.0f)); /* Quick review of C hex floating point literals. */ assert(0.5f == 0x1.0p-1f); assert(1.0f == 0x1.0p0f); assert(2.0f == 0x1.0p1f); /* Sign bit. */ assert(float32_equal(-0.5f, 1, 126, 0)); assert(float32_equal(-1.0f, 1, 127, 0)); assert(float32_equal(-2.0f, 1, 128, 0)); assert(isnormal(-0.5f)); assert(isnormal(-1.0f)); assert(isnormal(-2.0f)); /* The special case of 0.0 and -0.0. */ assert(float32_equal( 0.0f, 0, 0, 0)); assert(float32_equal(-0.0f, 1, 0, 0)); assert(!isnormal( 0.0f)); assert(!isnormal(-0.0f)); assert(0.0f == -0.0f); /* ANSI C defines FLT_MIN as the smallest non-subnormal number. */ assert(FLT_MIN == 0x1.0p-126f); assert(float32_equal(FLT_MIN, 0, 1, 0)); assert(isnormal(FLT_MIN)); /* The largest subnormal number. */ float largest_subnormal = float_from_bytes(0, 0, 0x7FFFFF); assert(largest_subnormal == 0x0.FFFFFEp-126f); assert(largest_subnormal < FLT_MIN); assert(!isnormal(largest_subnormal)); /* The smallest non-zero subnormal number. */ float smallest_subnormal = float_from_bytes(0, 0, 1); assert(smallest_subnormal == 0x0.000002p-126f); assert(0.0f < smallest_subnormal); assert(!isnormal(smallest_subnormal)); return EXIT_SUCCESS; }

GitHub aguas arriba .

Compila y ejecuta con:

gcc -ggdb3 -O0 -std=c11 -Wall -Wextra -Wpedantic -Werror -o subnormal.out subnormal.c ./subnormal.out

Visualización

Siempre es una buena idea tener una intuición geométrica sobre lo que aprendemos, así que aquí va.

Si trazamos los números de punto flotante IEEE 754 en una línea para cada exponente dado, se verá algo así:

+---+-------+---------------+ exponent |126| 127 | 128 | +---+-------+---------------+ | | | | v v v v ----------------------------- floats ***** * * * * * * * * ----------------------------- ^ ^ ^ ^ | | | | 0.5 1.0 2.0 4.0

De eso podemos ver que para cada exponente:

  • No hay superposición entre los números representados.
  • para cada exponente, tenemos el mismo número 2 ^ 32 números (aquí representados por 4 * )
  • Los puntos están igualmente espaciados para un exponente dado
  • Los exponentes más grandes cubren rangos más grandes, pero con puntos más dispersos

Ahora, bajemos todo eso hasta el exponente 0.

Sin subnormales (hipotéticos):

+---+---+-------+---------------+ exponent | ? | 0 | 1 | 2 | +---+---+-------+---------------+ | | | | | v v v v v --------------------------------- floats * ***** * * * * * * * * --------------------------------- ^ ^ ^ ^ ^ | | | | | 0 | 2^-126 2^-125 2^-124 | 2^-127

Con subnormales:

+-------+-------+---------------+ exponent | 0 | 1 | 2 | +-------+-------+---------------+ | | | | v v v v --------------------------------- floats * * * * * * * * * * * * * --------------------------------- ^ ^ ^ ^ ^ | | | | | 0 | 2^-126 2^-125 2^-124 | 2^-127

Al comparar los dos gráficos, vemos que:

  • los subnormales duplican la longitud del rango del exponente 0 , de [2^-127, 2^-126) a [0, 2^-126)

    El espacio entre flotadores en el rango subnormal es el mismo que para [0, 2^-126) .

  • el rango [2^-127, 2^-126) tiene la mitad del número de puntos que tendría sin subnormales.

    La mitad de esos puntos van a llenar la otra mitad del rango.

  • el rango [0, 2^-127) tiene algunos puntos con subnormales, pero ninguno sin.

  • el rango [2^-128, 2^-127) tiene la mitad de los puntos que [2^-127, 2^-126) .

    Esto es lo que queremos decir cuando decimos que los subnormales son una compensación entre tamaño y precisión.

En esta configuración, tendríamos un espacio vacío entre 0 y 2^-127 , lo cual no es muy elegante.

Sin embargo, el intervalo está bien poblado y contiene 2^23 flotadores como cualquier otro.

Implementaciones

x86_64 implementa IEEE 754 directamente en el hardware, al cual se traduce el código C.

TODO: ¿Algún ejemplo notable de hardware moderno que no tenga subnormales?

TODO: ¿alguna implementación permite controlarla en tiempo de ejecución?

Los subnormales parecen ser menos rápidos que los normales en ciertas implementaciones: ¿por qué cambiar 0.1f a 0 ralentiza el rendimiento en 10x?

Infinito y NaN

Aquí hay un breve ejemplo ejecutable: ¿ Rangos de tipo de datos de punto flotante en C?