Pourquoi les nombres à virgule flottante sont-ils inexacts?

Pourquoi certains nombres perdent-ils en précision lorsqu’ils sont stockés sous forme de nombres à virgule flottante?

Par exemple, le nombre décimal 9.2 peut être exprimé exactement comme un rapport de deux nombres entiers décimaux ( 92/10 ), les deux pouvant être exprimés exactement en binary ( 0b1011100/0b1010 ). Cependant, le même ratio stocké en nombre à virgule flottante n’est jamais exactement égal à 9.2 :

 32-bit "single precision" float: 9.19999980926513671875 64-bit "double precision" float: 9.199999999999999289457264239899814128875732421875 

Comment un nombre apparemment simple peut-il être “trop ​​grand” pour s’exprimer dans 64 bits de mémoire?

Dans la plupart des langages de programmation, les nombres à virgule flottante sont représentés comme la notation scientifique : avec un exposant et une mantisse (également appelée significande). Un nombre très simple, disons 9.2 , est en réalité cette fraction:

5179139571476070 * 2 -49

Où l’exposant est -49 et la mantisse est 5179139571476070 . La raison pour laquelle il est impossible de représenter des nombres décimaux de cette façon est que l’exposant et la mantisse doivent être des entiers. En d’autres termes, tous les flottants doivent être un entier multiplié par une puissance entière de 2 .

9.2 peut être simplement 92/10 , mais 10 ne peut pas être exprimé comme 2 n si n est limité à des valeurs entières.


Voir les données

Premièrement, quelques fonctions pour voir les composants qui créent un float 32 et 64 bits. Si vous ne vous souciez que de la sortie (exemple 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)] 

Il y a beaucoup de complexité derrière cette fonction, et ce serait assez compliqué à expliquer, mais si vous êtes intéressé, la ressource importante pour nos besoins est le module struct .

Le float de Python est un nombre double précision de 64 bits. Dans d’autres langages tels que C, C ++, Java et C #, la double précision a un type double distinct, souvent implémenté en 64 bits.

Lorsque nous appelons cette fonction avec notre exemple, 9.2 , voici ce que nous obtenons:

 >>> float_to_bin_parts(9.2) ['0', '10000000010', '0010011001100110011001100110011001100110011001100110'] 

Interpréter les données

Vous verrez que j’ai divisé la valeur de retour en trois composants. Ces composants sont:

  • Signe
  • Exposant
  • Mantissa (aussi appelée Significand ou Fraction)

Signe

Le signe est stocké dans le premier composant en un seul bit. C’est facile à expliquer: 0 signifie que le flottant est un nombre positif; 1 signifie que c’est négatif. Puisque 9.2 est positif, notre valeur de signe est 0 .

Exposant

L’exposant est stocké dans le composant du milieu sous la forme de 11 bits. Dans notre cas, 0b10000000010 . En décimal, cela représente la valeur 1026 . Une bizarrerie de ce composant est que vous devez soustraire un nombre égal à 2 (# de bits) – 1 – 1 pour obtenir le véritable exposant; dans notre cas, cela signifie soustraire 0b1111111111 (nombre décimal 1023 ) pour obtenir le véritable exposant, 0b00000000011 (nombre décimal 3).

Mantissa

La mantisse est stockée dans le troisième composant sous la forme de 52 bits. Cependant, il y a une bizarrerie à cette composante aussi. Pour comprendre cette bizarrerie, considérez un nombre en notation scientifique, comme ceci:

6.0221413×10 23

La mantisse serait le 6.0221413 . Rappelons que la mantisse en notation scientifique commence toujours par un seul chiffre non nul. La même chose est vraie pour le binary, sauf que le binary ne comporte que deux chiffres: 0 et 1 . Donc, la mantisse binary commence toujours par 1 ! Quand un flottant est stocké, le 1 à l’avant de la mantisse binary est omis pour économiser de l’espace; nous devons le placer à l’avant de notre troisième élément pour obtenir la vraie mantisse:

1.0010011001100110011001100110011001100110011001100110

Cela implique plus qu’une simple addition, car les bits stockés dans notre troisième composant représentent en réalité la partie fractionnaire de la mantisse, à droite du sharepoint la base .

Quand on traite des nombres décimaux, on “déplace le point décimal” en multipliant ou en divisant par des puissances de 10. En binary, on peut faire la même chose en multipliant ou en divisant par des puissances de 2. Comme notre troisième élément a 52 bits, on divise par 2 52 pour le déplacer 52 places à droite:

0.0010011001100110011001100110011001100110011001100110

En notation décimale, cela 675539944105574 diviser 675539944105574 par 4503599627370496 pour obtenir 0.1499999999999999 . (Ceci est un exemple d’un ratio qui peut être exprimé exactement en binary, mais seulement approximativement en décimal; pour plus de détails, voir: 675539944105574/4503599627370496 .)

Maintenant que nous avons transformé le troisième composant en un nombre fractionnaire, l’ajout de 1 donne la vraie mantisse.

Recapage des composants

  • Signe (premier composant): 0 pour positif, 1 pour négatif
  • Exposant (composant du milieu): Soustrayez 2 (# de bits) – 1 – 1 pour obtenir le véritable exposant
  • Mantisse (dernier composant): divise par 2 (# de bits) et ajoute 1 pour obtenir la vraie mantisse

Calcul du nombre

En rassemblant les trois parties, on nous donne ce nombre binary:

1.0010011001100110011001100110011001100110011001100110 x 10 11

Que l’on peut alors convertir de binary en décimal:

1.1499999999999999 x 2 3 (inexact!)

Et multiplier pour révéler la représentation finale du nombre que nous avons commencé avec ( 9.2 ) après avoir été stocké en tant que valeur à virgule flottante:

9.1999999999999993


Représenter comme une fraction

9.2

Maintenant que nous avons construit le nombre, il est possible de le reconstruire en une fraction simple:

1.0010011001100110011001100110011001100110011001100110 x 10 11

Décaler la mantisse sur un nombre entier:

10010011001100110011001100110011001100110011001100110 x 10 11-110100

Convertir en décimal:

5179139571476070 x 2 3-52

Soustraire l’exposant:

5179139571476070 x 2 -49

Transformez l’exposant négatif en division:

5179139571476070/2 49

Multiplier l’exposant:

5179139571476070/562949953421312

Qui est égal à:

9.1999999999999993

9,5

 >>> float_to_bin_parts(9.5) ['0', '10000000010', '0011000000000000000000000000000000000000000000000000'] 

Vous pouvez déjà voir que la mantisse ne comprend que 4 chiffres suivis de beaucoup de zéros. Mais passons à l’étape.

Assemblez la notation scientifique binary:

1.0011 x 10 11

Décale le point décimal:

10011 x 10 11-100

Soustraire l’exposant:

10011 x 10 -1

Binaire à décimal:

19 x 2 -1

Exposant négatif à la division:

19/2 1

Multiplier l’exposant:

19/2

Équivaut à:

9,5



Lectures complémentaires

  • Le Guide en virgule flottante: ce que tout programmeur devrait savoir sur l’arithmétique en virgule flottante, ou pourquoi mes chiffres ne concordent-ils pas? (floating-point-gui.de)
  • Ce que chaque informaticien devrait savoir sur l’arithmétique en virgule flottante (Goldberg 1991)
  • IEEE Format à virgule flottante double précision (Wikipedia)
  • Arithmétique en virgule flottante: problèmes et limites (docs.python.org)
  • Point binary flottant

Ce n’est pas une réponse complète ( mhlester a déjà couvert beaucoup de bonnes choses que je ne vais pas reproduire), mais je voudrais souligner combien la représentation d’un nombre dépend de la base dans laquelle vous travaillez.

Considérons la fraction 2/3

Dans le bon 10 base, nous écrivons généralement comme quelque chose comme

  • 0.666 …
  • 0,666
  • 0.667

Lorsque nous regardons ces représentations, nous avons tendance à les associer à la fraction 2/3, même si seule la première représentation est mathématiquement égale à la fraction. Les deuxième et troisième représentations / approximations ont une erreur de l’ordre de 0,001, ce qui est en réalité bien pire que l’erreur entre 9,2 et 9,1999999999999993. En fait, la deuxième représentation n’est même pas arrondie correctement! Néanmoins, nous n’avons pas de problème avec 0.666 comme approximation du nombre 2/3, donc nous ne devrions pas vraiment avoir de problème avec la façon dont 9.2 est approximée dans la plupart des programmes . (Oui, dans certains programmes, c’est important.)

Bases de nombres

Alors, voici où les bases numériques sont essentielles. Si nous essayions de représenter les 2/3 de la base 3, alors

(2/3) 10 = 0,2 3

En d’autres termes, nous avons une représentation exacte et finie pour le même nombre en changeant de base! Le problème est que, même si vous pouvez convertir n’importe quel nombre en n’importe quelle base, tous les nombres rationnels ont des représentations finies exactes dans certaines bases, mais pas dans d’autres .

Pour ramener ce point à la maison, regardons 1/2. Cela peut vous surprendre que même si ce nombre parfaitement simple a une représentation exacte en base 10 et 2, il nécessite une représentation répétée en base 3.

(1/2) 10 = 0,5 10 = 0.1 2 = 0.1111 … 3

Pourquoi les nombres à virgule flottante sont-ils inexacts?

Parce que souvent, ils approchent des rationnels qui ne peuvent pas être représentés de manière définitive dans la base 2 (les chiffres se répètent) et en général ils approchent des nombres réels (éventuellement irrationnels) qui peuvent ne pas être représentables dans un nombre fini de bases.

Bien que toutes les autres réponses soient bonnes, il manque encore une chose:

Il est impossible de représenter des nombres irrationnels (par exemple, π, sqrt(2) , log(3) , etc.) avec précision!

Et c’est pourquoi ils sont appelés irrationnels. Aucune quantité de stockage de bits dans le monde ne suffirait à contenir un seul d’entre eux. Seule l’ arithmétique symbolique est capable de préserver leur précision.

Bien que si vous voulez limiter vos besoins en mathématiques à des nombres rationnels, seul le problème de la précision devient gérable. Vous devez stocker une paire d’entiers (éventuellement très grands) a et b pour contenir le nombre représenté par la fraction a/b . Toute votre arithmétique devrait être faite sur des fractions comme dans les mathématiques au secondaire (par exemple, a/b * c/d = ac/bd ).

Mais bien sûr, vous rencontrerez toujours le même genre de problème lorsque pi , sqrt , log , sin , etc.

TL; DR

Pour l’arithmétique à accélération matérielle, seule une quantité limitée de nombres rationnels peut être représentée. Chaque nombre non représentable est approximatif. Certains nombres (irrationnels) ne peuvent jamais être représentés quel que soit le système.

Pourquoi ne pouvons-nous pas représenter 9.2 en virgule flottante binary?

Les nombres à virgule flottante sont (simplifiant légèrement) un système de numérotation de position avec un nombre restreint de chiffres et un sharepoint base mobile.

Une fraction ne peut être exprimée exactement en utilisant un nombre fini de chiffres dans un système de numérotation positionnelle que si les facteurs premiers du dénominateur (lorsque la fraction est exprimée en termes les plus faibles) sont des facteurs de la base.

Les facteurs premiers de 10 sont 5 et 2, donc en base 10, nous pouvons représenter une fraction quelconque de la forme a / (2 b 5 c ).

Par contre, le seul facteur premier de 2 est 2, donc en base 2, nous ne pouvons représenter que des fractions de la forme a / (2 b )

Pourquoi les ordinateurs utilisent cette représentation?

Parce que c’est un format simple à utiliser et qu’il est suffisamment précis dans la plupart des cas. Fondamentalement, les scientifiques utilisent la “notation scientifique” pour arrondir leurs résultats à un nombre raisonnable de chiffres à chaque étape.

Il serait certainement possible de définir un format de fraction, avec (par exemple) un numérateur de 32 bits et un dénominateur de 32 bits. Il serait capable de représenter des nombres que la virgule flottante à double précision IEEE ne pourrait pas, mais il y aurait aussi beaucoup de nombres qui pourraient être représentés en virgule flottante à double précision qui ne pourraient pas être représentés dans un format de fraction de taille fixe.

Cependant, le gros problème est qu’un tel format est difficile à faire. Pour deux raisons.

  1. Si vous voulez avoir exactement une représentation de chaque nombre, après chaque calcul, vous devez réduire la fraction à ses termes les plus bas. Cela signifie que pour chaque opération, vous devez essentiellement effectuer un calcul de diviseur commun.
  2. Si, après votre calcul, vous vous retrouvez avec un résultat non représentable, car le numérateur ou le dénominateur dont vous avez besoin pour trouver le résultat représentable le plus proche. Ceci est non sortingvile.

Certains langages offrent des types de fractions, mais en général ils le font en combinaison avec une précision arbitraire, ce qui évite de se soucier de l’approximation des fractions mais crée un problème quand un nombre passe par un grand nombre d’étapes de calcul par conséquent, le stockage nécessaire à la fraction peut exploser.

Certains langages offrent également des types décimaux à virgule flottante, principalement utilisés dans les scénarios où il est important que les résultats obtenus par l’ordinateur correspondent à des règles d’arrondi pré-existantes écrites pour les humains (principalement les calculs financiers). Celles-ci sont légèrement plus difficiles à utiliser que les virgules flottantes, mais le plus gros problème est que la plupart des ordinateurs ne les prennent pas en charge.