web-dev-qa-db-fra.com

Qu'est-ce qu'un nombre à virgule flottante sous-normal?

page de référence isnormal () indique:

Détermine si le nombre à virgule flottante donné arg est normal, c'est-à-dire ni nul, ni sous-normal, ni infini, ni NaN.

Un nombre étant nul, infini ou NaN est clair ce que cela signifie. Mais cela dit aussi sous-normal. Quand un nombre est-il sous-normal?

61
BЈовић

Dans la norme IEEE754, les nombres à virgule flottante sont représentés sous forme de notation scientifique binaire, x = M × 2e. Ici [[# #]] m [~ # ~] est le mantisse et e est le exposant. Mathématiquement, vous pouvez toujours choisir l'exposant de telle sorte que 1 ≤ M <2. * Cependant, étant donné que dans la représentation informatique, l'exposant ne peut avoir qu'une plage finie, il y a des nombres supérieurs à zéro mais inférieurs à 1,0 × 2emin. Ces nombres sont les subnormals ou denormals.

Pratiquement, la mantisse est stockée sans le 1 de tête, car il y a toujours un 1 de tête, sauf pour les nombres subnormaux (et zéro). Ainsi, l'interprétation est que si l'exposant n'est pas minimal, il y a un premier implicite, et si l'exposant est minimal, il n'y en a pas et le nombre est sous-normal.

*) Plus généralement, 1 ≤ M < B pour toute base --- B notation scientifique.

61
Kerrek SB

Notions de base IEEE 754

Examinons d'abord les bases des numéros IEEE 754.

Nous nous concentrerons sur la précision simple (32 bits), mais tout peut être immédiatement généralisé à d'autres précisions.

Le format est:

  • 1 bit: signe
  • 8 bits: exposant
  • 23 bits: fraction

Ou si vous aimez les photos:

enter image description here

Source .

Le signe est simple: 0 est positif, et 1 est négatif, fin de l'histoire.

L'exposant a une longueur de 8 bits et varie donc de 0 à 255.

L'exposant est appelé biaisé car il a un décalage de -127, par exemple.:

  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 convention de bits de tête

Lors de la conception de l'IEEE 754, les ingénieurs ont remarqué que tous les nombres, sauf 0.0, en avoir un 1 en binaire comme premier chiffre

Par exemple.:

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

les deux commencent avec ce _ 1. partie.

Par conséquent, il serait inutile de laisser ce chiffre prendre un bit de précision presque tous les nombres.

Pour cette raison, ils ont créé la "convention de bits de tête":

supposez toujours que le nombre commence par un

Mais alors comment gérer 0.0? Eh bien, ils ont décidé de créer une exception:

  • si l'exposant est 0
  • et la fraction est 0
  • alors le nombre représente plus ou moins 0.0

de sorte que les octets 00 00 00 00 représente également 0.0, qui a l'air bien.

Si nous ne considérions que ces règles, le plus petit nombre non nul pouvant être représenté serait:

  • exposant: 0
  • fraction: 1

qui ressemble à ceci dans une fraction hexadécimale en raison de la convention de bits de tête:

1.000002 * 2 ^ (-127)

.000002 est 22 zéros avec un 1 à la fin.

Nous ne pouvons pas prendre fraction = 0, sinon ce nombre serait 0.0.

Mais alors les ingénieurs, qui avaient aussi un sens artistique aigu, se sont dit: n'est-ce pas moche? Que nous sautons de droite 0.0 à quelque chose qui n'est même pas une puissance propre de 2? Ne pourrions-nous pas représenter des nombres encore plus petits d'une manière ou d'une autre?

Nombres anormaux

Les ingénieurs se grattèrent la tête pendant un moment et revinrent, comme d'habitude, avec une autre bonne idée. Et si nous créons une nouvelle règle:

Si l'exposant est 0, alors:

  • le bit de tête devient 0
  • l'exposant est fixé à -126 (pas -127 comme si nous n'avions pas cette exception)

De tels nombres sont appelés nombres subnormaux (ou nombres dénormaux qui sont synonymes).

Cette règle implique immédiatement que le nombre tel que:

  • exposant: 0
  • fraction: 0

est 0.0, ce qui est plutôt élégant car cela signifie une règle de moins à suivre.

Donc 0.0 est en fait un nombre sous-normal selon notre définition!

Avec cette nouvelle règle, le plus petit nombre non-normal est:

  • exposant: 1 (0 serait inférieur à la normale)
  • fraction: 0

qui représente:

1.0 * 2 ^ (-126)

Ensuite, le plus grand nombre subnormal est:

  • exposant: 0
  • fraction: 0x7FFFFF (23 bits 1)

ce qui équivaut à:

0.FFFFFE * 2 ^ (-126)

.FFFFFE est de nouveau 23 bits un à droite du point.

C'est assez proche du plus petit nombre non-normal, ce qui semble sain.

Et le plus petit nombre sous-normal non nul est:

  • exposant: 0
  • fraction: 1

ce qui équivaut à:

0.000002 * 2 ^ (-126)

qui semble également assez proche de 0.0!

Incapables de trouver un moyen sensé de représenter des nombres inférieurs à cela, les ingénieurs étaient heureux et sont retournés à la visualisation de photos de chats en ligne, ou quoi que ce soit qu'ils aient fait dans les années 70 à la place.

Comme vous pouvez le voir, les nombres sous-normaux font un compromis entre précision et longueur de représentation.

Comme exemple le plus extrême, la plus petite sous-normale non nulle:

0.000002 * 2 ^ (-126)

a essentiellement une précision d'un seul bit au lieu de 32 bits. Par exemple, si nous le divisons par deux:

0.000002 * 2 ^ (-126) / 2

nous atteignons effectivement 0.0 exactement!

Visualisation

C'est toujours une bonne idée d'avoir une intuition géométrique sur ce que nous apprenons, alors voilà.

Si nous traçons des nombres à virgule flottante IEEE 754 sur une ligne pour chaque exposant donné, cela ressemble à ceci:

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

De cela, nous pouvons voir que pour chaque exposant:

  • pour chaque exposant, il n'y a pas de chevauchement entre les nombres représentés
  • pour chaque exposant, nous avons le même nombre 2 ^ 32 de nombres (ici représentés par 4 *)
  • les points sont également espacés pour un exposant donné
  • de plus grands exposants couvrent de plus grandes gammes, mais avec des points plus étalés

Maintenant, réduisons cela jusqu'à l'exposant 0.

Sans sous-normales, cela ressemblerait hypothétiquement à:

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

Avec des sous-normales, cela ressemble à ceci:

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

En comparant les deux graphiques, nous voyons que:

  • les sous-normales doublent la longueur de la plage d'exposant 0, de [2^-127, 2^-126) à [0, 2^-126)

    L'espace entre les flotteurs dans la plage subnormale est le même que pour [0, 2^-126).

  • la gamme [2^-127, 2^-126) a la moitié du nombre de points qu'il aurait sans sous-normales.

    La moitié de ces points va remplir l'autre moitié de la fourchette.

  • la gamme [0, 2^-127) a quelques points avec des sous-normales, mais aucun sans.

    Ce manque de points dans [0, 2^-127) n'est pas très élégant et est la principale raison de l'existence de sous-normales!

  • puisque les points sont également espacés:

    • la gamme [2^-128, 2^-127) a la moitié des points que [2^-127, 2^-126) -[2^-129, 2^-128) a la moitié des points que [2^-128, 2^-127)
    • etc

    C'est ce que nous voulons dire lorsque nous disons que les sous-normales sont un compromis entre taille et précision.

Exemple C exécutable

Jouons maintenant avec du code réel pour vérifier notre théorie.

Dans presque toutes les machines actuelles et de bureau, C float représente les nombres à virgule flottante IEEE 754 simple précision.

C'est notamment le cas de mon portable Ubuntu 18.04 AMD64 Lenovo P51.

Avec cette hypothèse, toutes les assertions transmettent le programme suivant:

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 en amont .

Compilez et exécutez avec:

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

C++

En plus d'exposer toutes les API de C, C++ expose également certaines fonctionnalités liées sous-normales supplémentaires qui ne sont pas aussi facilement disponibles dans C dans <limits> , par exemple:

  • denorm_min : renvoie la valeur sous-normale positive minimale du type T

En C++, l'API de trou est modélisée pour chaque type à virgule flottante et est beaucoup plus agréable.

Implémentations

x86_64 et ARMv8 implémentent IEEE 754 directement sur le matériel, ce que le code C traduit.

Les subnormales semblent être moins rapides que les normales dans certaines implémentations: Pourquoi le changement de 0.1f en 0 ralentit-il les performances de 10x? Ceci est mentionné dans le manuel ARM, voir la section "Détails ARMv8" de cette réponse.

Détails ARMv8

ARM Architecture Reference Manual ARMv8 DDI 0487C.a manual A1.5.4 "Flush-to-zero" décrit un mode configurable où les sous-normales sont arrondies à zéro pour améliorer les performances:

Les performances du traitement en virgule flottante peuvent être réduites lors des calculs impliquant des nombres dénormalisés et des exceptions de sous-dépassement. Dans de nombreux algorithmes, ces performances peuvent être récupérées, sans affecter de manière significative la précision du résultat final, en remplaçant les opérandes dénormalisés et les résultats intermédiaires par des zéros. Pour permettre cette optimisation, ARM les implémentations à virgule flottante permettent d'utiliser un mode Flush-to-zero pour différents formats à virgule flottante comme suit:

  • Pour AArch64:

    • Si FPCR.FZ==1, le mode Flush-to-Zero est utilisé pour toutes les entrées et sorties simple précision et double précision de toutes les instructions.

    • Si FPCR.FZ16==1, puis le mode Flush-to-Zero est utilisé pour toutes les entrées et sorties demi-précision des instructions à virgule flottante, autres que: —Conversions entre les nombres demi-précision et simple précision. — Conversions entre demi-précision et double- Numéros de précision.

A1.5.2 "Normes à virgule flottante et terminologie" Le tableau A1-3 "Terminologie à virgule flottante" confirme que les sous-normales et les dénormales sont synonymes:

This manual                 IEEE 754-2008
-------------------------   -------------
[...]
Denormal, or denormalized   Subnormal

C5.2.7 "FPCR, registre de contrôle à virgule flottante" décrit comment ARMv8 peut éventuellement lever des exceptions ou définir un bit de drapeau chaque fois que l'entrée d'une opération à virgule flottante est sous-normale:

FPCR.IDE, bit [15] Validation d'interruption d'exception à virgule flottante en entrée. Les valeurs possibles sont:

  • 0b0 Gestion des exceptions non capturées sélectionnée. Si l'exception à virgule flottante se produit, le bit FPSR.IDC est défini sur 1.

  • 0b1 Gestion des exceptions piégées sélectionnée. Si l'exception à virgule flottante se produit, le PE ne met pas à jour le bit FPSR.IDC. Le logiciel de gestion des interruptions peut décider de définir le bit FPSR.IDC sur 1.

D12.2.88 "MVFR1_EL1, AArch32 Media and VFP Feature Register 1" montre que la prise en charge dénormale est en fait complètement facultative, et offre un peu pour détecter s'il y a prise en charge:

FPFtZ, bits [3: 0]

Rincer au mode zéro. Indique si l'implémentation en virgule flottante prend uniquement en charge le mode de fonctionnement Flush-to-Zero. Les valeurs définies sont:

  • 0b0000 Non implémenté ou le matériel prend uniquement en charge le mode de fonctionnement Flush-to-Zero.

  • 0b0001 Le matériel prend en charge l'arithmétique complète des nombres dénormalisés.

Toutes les autres valeurs sont réservées.

Dans ARMv8-A, les valeurs autorisées sont 0b0000 et 0b0001.

Cela suggère que lorsque les sous-normales ne sont pas implémentées, les implémentations reviennent à zéro.

Infinity et NaN

Curieuse? J'ai écrit des choses à:

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

Il existe potentiellement plusieurs façons de représenter le même nombre, en utilisant la décimale comme exemple, le nombre 0,1 pourrait être représenté par 1 * 10-1 ou 0,1 * 10 ou même 0,01 * 10. La norme veut que les nombres soient toujours stockés avec le premier bit comme un. En décimal, cela correspond à l'exemple 1 * 10-1.

Supposons maintenant que l'exposant le plus bas qui puisse être représenté est -100. Donc, le plus petit nombre qui peut être représenté sous forme normale est 1 * 10-100. Cependant, si nous relâchons la contrainte que le bit de tête soit un, alors nous pouvons réellement représenter des nombres plus petits dans le même espace. En prenant un exemple décimal, nous pourrions représenter 0,1 * 10-100. C'est ce qu'on appelle un nombre sous-normal. Le but d'avoir des nombres sous-normaux est de lisser l'écart entre le plus petit nombre normal et zéro.

Il est très important de réaliser que les nombres sous-normaux sont représentés avec moins de précision que les nombres normaux. En fait, ils échangent une précision réduite pour leur plus petite taille. Par conséquent, les calculs utilisant des nombres sous-normaux n'auront pas la même précision que les calculs sur des nombres normaux. Ainsi, une application qui effectue un calcul significatif sur des nombres sous-normaux mérite probablement d'être étudiée pour voir si le redimensionnement (c'est-à-dire la multiplication des nombres par un facteur d'échelle) produirait moins de sous-normales et des résultats plus précis.

25
allwyn.menezes