Saltar al contenido

¿Qué es un número de coma flotante subnormal?

Solución:

Conceptos básicos de IEEE 754

Primero, repasemos los conceptos básicos de la organización de los números IEEE 754.

Nos centraremos en la precisión simple (32 bits), pero todo se puede generalizar inmediatamente a otras precisiones.

El formato es:

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

O si te gustan las fotos:

ingrese la descripción de la imagen aquí

Fuente.

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 varía de 0 a 255.

El exponente se llama sesgado porque tiene un desplazamiento de -127, p.ej:

  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 bits líder

(Lo que sigue es una narrativa hipotética ficticia, no basada en ninguna investigación histórica real).

Al diseñar IEEE 754, los ingenieros notaron que todos los números, excepto 0.0, tener uno 1 en binario como 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 ese molesto 1. parte.

Por lo tanto, sería un desperdicio dejar que ese dígito ocupe un bit de precisión en casi todos los números.

Por esta razón, crearon la “convención de bits inicial”:

siempre asume 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

para que los bytes 00 00 00 00 también representan 0.0, que se ve bien.

Si solo consideráramos estas reglas, entonces el número más pequeño distinto de cero que se puede representar sería:

  • exponente: 0
  • fracción: 1

que se parece a esto en una fracción hexadecimal debido a la convención de bits inicial:

1.000002 * 2 ^ (-127)

dónde .000002 es 22 ceros con a 1 al final.

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

Pero entonces los ingenieros, que también tenían un agudo sentido estético, pensaron: ¿no es feo eso? Que saltamos de derecho 0.0 a algo que ni siquiera es una potencia propia de 2? ¿No podríamos representar números aún más pequeños de alguna manera? (De acuerdo, fue un poco más preocupante que “feo”: en realidad, la gente estaba obteniendo malos resultados en sus cálculos, consulte “Cómo los subnormales mejoran los cálculos” a continuación).

Números subnormales

Los ingenieros se rascaron la cabeza durante un rato y volvieron, 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 -127 como si no tuviéramos esta excepción)

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

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

  • exponente: 0
  • fracción: 0

es todavía 0.0, que es un poco elegante, ya que significa una regla menos de la que realizar un seguimiento.

Entonces 0.0 es en realidad un número subnormal según nuestra definición.

Entonces, con esta nueva regla, el número no anormal más pequeño es:

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

que representa:

1.0 * 2 ^ (-126)

Entonces, el número subnormal más grande es:

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

que es igual a:

0.FFFFFE * 2 ^ (-126)

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

Esto está bastante cerca del número no anormal más pequeño, lo que suena cuerdo.

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

  • exponente: 0
  • fracción: 1

que es igual a:

0.000002 * 2 ^ (-126)

que también se parece bastante a 0.0!

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

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

Como ejemplo más extremo, el subnormal distinto de cero más pequeño:

0.000002 * 2 ^ (-126)

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

0.000002 * 2 ^ (-126) / 2

realmente alcanzamos 0.0 ¡exactamente!

Visualización

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

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

          +---+-------+---------------+-------------------------------+
exponent  |126|  127  |      128      |              129              |
          +---+-------+---------------+-------------------------------+
          |   |       |               |                               |
          v   v       v               v                               v
          -------------------------------------------------------------
floats    ***** * * * *   *   *   *   *       *       *       *       *
          -------------------------------------------------------------
          ^   ^       ^               ^                               ^
          |   |       |               |                               |
          0.5 1.0     2.0             4.0                             8.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 ^ 23 de números de punto flotante (aquí representado por 4 *)
  • dentro de cada exponente, los puntos están igualmente espaciados
  • los exponentes más grandes cubren rangos más grandes, pero con puntos más dispersos

Ahora, bajemos eso hasta el exponente 0.

Sin subnormales, hipotéticamente se vería así:

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

Con subnormales, se ve así:

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

Al comparar los dos gráficos, vemos que:

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

    The space between floats in subnormal range is the same as for [0, 2^-126).

  • the range [2^-127, 2^-126) has half the number of points that it would have without subnormals.

    Half of those points go to fill the other half of the range.

  • the range [0, 2^-127) has some points with subnormals, but none without.

    This lack of points in [0, 2^-127) is not very elegant, and is the main reason for subnormals to exist!

  • since the points are equally spaced:

    • the range [2^-128, 2^-127) has half the points than [2^-127, 2^-126)
      [2^-129, 2^-128) has half the points than [2^-128, 2^-127)
    • and so on

    This is what we mean when saying that subnormals are a tradeoff between size and precision.

Runnable C example

Now let’s play with some actual code to verify our theory.

In almost all current and desktop machines, C float represents single precision IEEE 754 floating point numbers.

This is in particular the case for my Ubuntu 18.04 amd64 Lenovo P51 laptop.

With that assumption, all assertions pass on the following program:

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 upstream.

Compile and run with:

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

C++

In addition to exposing all of C’s APIs, C++ also exposes some extra subnormal related functionality that is not as readily available in C in <limits>, e.g.:

  • denorm_min: Returns the minimum positive subnormal value of the type T

In C++ the whole API is templated for each floating point type, and is much nicer.

Implementations

x86_64 and ARMv8 implemens IEEE 754 directly on hardware, which the C code translates to.

Subnormals seem to be less fast than normals in certain implementations: Why does changing 0.1f to 0 slow down performance by 10x? This is mentioned in the ARM manual, see the “ARMv8 details” section of this answer.

ARMv8 details

ARM Architecture Reference Manual ARMv8 DDI 0487C.a manual A1.5.4 “Flush-to-zero” describes a configurable mode where subnormals are rounded to zero to improve performance:

The performance of floating-point processing can be reduced when doing calculations involving denormalized numbers and Underflow exceptions. In many algorithms, this performance can be recovered, without significantly affecting the accuracy of the final result, by replacing the denormalized operands and intermediate results with zeros. To permit this optimization, ARM floating-point implementations allow a Flush-to-zero mode to be used for different floating-point formats as follows:

  • For AArch64:

    • If FPCR.FZ==1, then Flush-to-Zero mode is used for all Single-Precision and Double-Precision inputs and outputs of all instructions.

    • If FPCR.FZ16==1, then Flush-to-Zero mode is used for all Half-Precision inputs and outputs of floating-point instructions, other than:—Conversions between Half-Precision and Single-Precision numbers.—Conversions between Half-Precision and Double-Precision numbers.

A1.5.2 “Floating-point standards, and terminology” Table A1-3 “Floating-point terminology” confirms that subnormals and denormals are synonyms:

This manual                 IEEE 754-2008
-------------------------   -------------
[...]

Subnormal desnormalizado o desnormalizado

C5.2.7 “FPCR, registro de control de punto flotante” describe cómo ARMv8 puede generar excepciones o establecer bits de bandera cuando la entrada de una operación de punto flotante es subnormal:

FPCR.IDE, poco [15] Entrada Habilitación de captura de excepción de punto flotante denormal. Los valores posibles son:

  • 0b0 Manejo de excepciones no envuelto seleccionado. Si se produce la excepción de punto flotante, el bit FPSR.IDC se establece en 1.

  • 0b1 Manejo de excepciones atrapadas seleccionado. Si ocurre la excepción de punto flotante, el PE no actualiza el bit FPSR.IDC. El software de manejo de capturas puede decidir si establecer el bit FPSR.IDC en 1.

D12.2.88 “MVFR1_EL1, AArch32 Media y VFP Feature Register 1” muestra que el soporte desnormal es completamente opcional de hecho, y ofrece un poco para detectar si hay soporte:

FPFtZ, bits [3:0]

Enjuague a modo cero. Indica si la implementación de punto flotante proporciona soporte solo para el modo de operación Flush-to-Zero. Los valores definidos son:

  • 0b0000 No implementado o el hardware solo admite el modo de operación Flush-to-Zero.

  • 0b0001 El hardware admite aritmética numérica completamente desnormalizada.

Los demás valores están reservados.

En ARMv8-A, los valores permitidos son 0b0000 y 0b0001.

Esto sugiere que cuando no se implementan los subnormales, las implementaciones simplemente vuelven a cero.

Infinito y NaN

¿Curioso? He escrito algunas cosas en:

  • infinito: rangos de tipo de datos de punto flotante en C?
  • NaN: ¿Cuál es la diferencia entre NaN silencioso y NaN de señalización?

Cómo los subnormales mejoran los cálculos

TODO: comprender mejor con mayor precisión cómo ese salto empeora los resultados del cálculo / cómo los subnormales mejoran los resultados del cálculo.

Historia real

Una entrevista con el anciano de Floating-Point por Charles Severance. (1998) es una breve descripción histórica del mundo real en forma de una entrevista con William Kahan sugerida por John Coleman en los comentarios.

En el estándar IEEE754, los números de coma flotante se representan como notación científica binaria, X = METRO × 2mi. Aquí METRO es el mantisa y mi es el exponente. Matemáticamente, siempre puede elegir el exponente para que 1 ≤ METRO <2. * Sin embargo, dado que en la representación por computadora el exponente solo puede tener un rango finito, hay algunos números que son mayores que cero, pero menores que 1.0 × 2mimin. Esos números son los subnormales o desnormales.

Prácticamente, la mantisa se almacena sin el 1 inicial, ya que siempre hay un 1 inicial, excepto para números subnormales (y cero). Por lo tanto, la interpretación es que si el exponente no es mínimo, hay un 1 a la izquierda implícito, y si el exponente es mínimo, no lo hay y el número es subnormal.

*) Más generalmente, 1 ≤ METRO < B para cualquier baseB notación cientifica.

De http://blogs.oracle.com/d/entry/subnormal_numbers:

Hay potencialmente varias formas de representar el mismo número, usando el decimal como ejemplo, el número 0.1 podría representarse como 1 * 10-1 o 0,1 * 100 o incluso 0.01 * 10. El estándar dicta que los números siempre se almacenan con el primer bit como uno. En decimal que corresponde al ejemplo 1 * 10-1.

Ahora suponga que el exponente más bajo que se puede representar es -100. Entonces, el número más pequeño que se puede representar en forma normal es 1 * 10-100. Sin embargo, si relajamos la restricción de que el bit inicial sea uno, entonces podemos representar números más pequeños en el mismo espacio. Tomando un ejemplo decimal podríamos representar 0.1 * 10-100. A esto se le llama un número subnormal. El propósito de tener números subnormales es suavizar la brecha entre el número normal más pequeño y cero.

Es muy importante darse cuenta de que los números subnormales se representan con menos precisión que los números normales. De hecho, están intercambiando precisión reducida por su tamaño más pequeño. Por lo tanto, los cálculos que utilizan números subnormales no tendrán la misma precisión que los cálculos con números normales. Por lo tanto, probablemente valga la pena investigar una aplicación que realiza cálculos significativos sobre números subnormales para ver si el cambio de escala (es decir, multiplicar los números por algún factor de escala) produciría menos subnormales y resultados más precisos.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *