Nombre à virgule flottantes #
pdf : pour impression
Comment représenter un nombre à virgule en machine ?
Souvenons nous des contraines de base : chaque nombre doit occuper un nombre fixe de bits.
Approche simpliste : la virgule fixe. #
On écrit chaque chiffre dans une case, la position indique la valeur !
Le séparateur décimal est toujours au même endroit.
Voici ce que cela donnerait pour 8 chiffres décimaux avec un séparateur décimal au centre :
1000 | 100 | 10 | 1 | 0.1 | 0.01 | 0.001 | 0.0001 | |
---|---|---|---|---|---|---|---|---|
0 | 2 | 3 | 4 | . | 4 | 6 | 2 | 1 |
Pas très efficace :
Les nombres sont limités : ici on ne peut dépasser 9999.9999
et le plus petit est 0.0001
.
Avec 8 chiffres on devrait pouvoir décrire 9999 9999
et 0.000 0001
La virgule flottante #
Comment le faire efficacement pour des nombres
-
extrèmement proches de 0
0,0000000000000000000000000000000123
-
ou extrèmement grands
876867867868721637163871687168,78613
?
La réponse à ce premier problème est la notation scientifique : $2.3772 \times 10^{32}$
Bien sûr en machine, on utilise la base 2 tandis que nous utilisons quotidiennement la base 10.
Cela soulève immédiatement une autre difficulté :
Les décimaux sont exprimés en base 10 ($2 \times 5$).
ils n’ont généralement pas de représentation exacte en machine.
0.30000000000000004 #
Les ordinateurs savent manipuler les “nombres à virgules”
>>> 1.255465 * 753156.45
945561.5624992499
mais les résultats sont parfois surprenants :
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
Nombres à virgule flottante #
Dans les machines, on utilise les nombres à virgule flottante
Les nombres sont alors appelés des flottants (floats en anglais)
L’égalité de deux flottants n’a aucun sens
>>> 0.1 + 0.2 == 0.3 False
Notation positionnelle des décimaux #
Dans le système décimal on utilise les puissances de 10 et la position des
chiffres par rapport à la virgule indique la puissance correspondante :
Par exemple le nombre décimal 325,47 s’écrit
Position 100 10 1 virgule 1/10 1/100...
chiffres 3 2 5 . 4 7
Nombres dyadiques #
Dans la machine on utilise le même principe mais avec des puissances de 2.
On parle de nombres dyadiques
Par exemple : $4 + 2 + 1 + 1/2 + 1/8$ et s’écrit en dyadique :
Position 4 2 1 virgule 1/2 1/4 1/8
chiffres 1 1 1 . 1 0 1
4 + 2 + 1 + 1/2 + 1/8 = 7.625
Exactement comme pour les décimaux, n’importe quel nombre réel peut être approché aussi précisément que l’on veut par des dyadiques.
$3.14 < \pi < 3.15$
Décimal vers binaire pour les nombres à virgule #
On cherche à convertir 2.3 en dyadique (on dira souvent “en binaire”).
-
On commence par la partie entière :
2 = 0b10
-
On multiplie le nombre précédent, sans sa partie entière, par 2. Le premier chiffre sera 0 ou 1 et c’est le bit correspondant à cette position :
On répète jusqu’à atteindre un entier ou jusqu’à atteindre la précision souhaitée.
Opération | Résultat | bit | position |
---|---|---|---|
$0.3$ | $0.3$ | 0 |
0 |
$0.3 \times 2$ | $0.6$ | 0 |
1 |
$0.6 \times 2$ | $1.2$ | 1 |
2 |
$0.2 \times 2$ | $0.4$ | 0 |
3 |
$0.4 \times 2$ | $0.8$ | 0 |
4 |
$0.8 \times 2$ | $1.6$ | 1 |
5 |
$0.6 \times 2$ | $1.2$ | 1 |
6 |
$0.2 \times 2$ | $0.4$ | 0 |
7 |
$0.4 \times 2$ | $0.8$ | 0 |
8 |
$0.8 \times 2$ | $1.6$ | 1 |
9 |
$0.6 \times 2$ | $1.2$ | 1 |
10 |
$0.2 \times 2$ | $0.4$ | 0 |
11 |
On peut s’arrêter ici, étant donné que la suite des bits va se répéter.
$0.3 = 0.010011001100_2$ et $2.3 = 10.01001100110_2$
Vérifions :
>>> 2 + 1/4 + 1/32 + 1/64 + 1/512 + 1/1024
2.2998046875
Binaire vers décimal #
Dans l’autre sens c’est beaucoup plus facile, on compte les positions des bits et on ajoute, lorsque le bit est 1, la puissance de $\frac{1}{2}$ correspondante :
$$x = 0,001001001_2 = \frac{1}{2^3} + \frac{1}{2^6} + \frac{1}{2^9} = 0.142578125$$
Revenons sur 0,1 + 0,2 #
0,1 et 0,2 ont des notations décimales finies (ce sont des décimaux)
Leur notation dyadique n’est pas finie !
$$0,1 = (0,00011001100110011001100110011001100110011\cdots)_2$$
En machine elle est tronquée (mais sera très proche de 0,1)
Ce n’est généralement pas gênant : on n’a généralement pas besoin d’une telle précision.
Cette approche est intéressante et naïvement, on pourrait penser que la machine stocke ainsi ses nombres.
Problème :
comment manipuler des nombres très grands et des nombres très petits en même temps ?
La taille de l’univers d’un côté, la masse d’un atome de l’autre : il faudrait des milliers de chiffres.
Rappel : calculer avec la notation scientifique #
$A = 300 000 000 \times 0.000 000 15$
La notation décimale n’est pas adaptée.
On préfère la notation scientifique :
$A = (3 \times 10^8) \times (1.5 \times 10^{-7}$)
Souvenons nous
- on multiplie 3 et 1,5
- on ajoute les exposants 8 et -7
$A = (3 \times 1.5) \times 10^{8 - 7}$
$A = 4.5 \times 10 ^ 1$
$A = 45$
La machine procède de la même manière en base 2.
Nombre dyadique #
Un nombre dyadique est s’écrit : $$\pm( 1,b1 \cdots bk)_2 \times 2^e$$
où $b1,\ldots,bk$ sont des bits et $e$ est un entier relatif.
La suite de bits $b1\ldots bk$ est la mantisse du nombre,
La puissance de 2 est l’exposant du nombre.
Exemple #
$6,25 = (110,01)_2 = (1,1001)_2 \times 2^2$
- La mantisse est la suite
1 0 0 1
- L’exposant est
2
Nombres à virgule flottante en détail #
On rencontre deux représentations courantes des flottants simple précision : 32 bits et double précision : 64 bits.
La norme IEE 754 de 1985 est adoptée par la majorité des langages informatiques modernes.
Dans cette norme (IEE 754, double précision), les nombres dyadiques sont codés sur 64 bits en réservant :
- $1$ bit pour le signe $S$,
- $11$ bits pour l’exposant $E$,
- $52$ bits pour la mantisse $M$.
La valeur du nombre est alors :
$$(-1)^S \times M \times 2^{E - 1033}$$
Ce qu’on peut résumer ainsi :
Norme | Encodage | Signe | Exposant | Mantisse | Valeur | Précision |
---|---|---|---|---|---|---|
Double précision | 64 bits | 1 bit | 11 bits | 52 bits | $$(-1)^S \times M \times 2^{E - 1033}$$ | 53 bits |
Pour des questions techniques il est nécessaire d’y inclure d’autres objets comme NaN
(not a number)
et des infinis positifs et négatifs.
Amplitude #
Sans entrer dans les détails, en codant sur 64 bits on peut représenter des nombres entre :
-
$2^{-1022} \approx 2,23 \times 10^{-308}$ pour le plus petit et
-
$2^{1024} - 2^{971} \approx 1,80 \times 10^{308}$ pour le plus grand
Des améliorations sont faites pour les nombres très proches de 0.
Quand un flottant dépasse le plus grand nombre possible il est considéré comme infini
>>> 2.0 * 10**308 # dépasse le plus grand
inf
Quelques surprises avec inf
et nan
#
inf
se comporte “grosso modo” comme l’infini des mathématiques…
mais l’implémentation révèle quelques surprises :
>>> a = float('inf') # pour définir inf
>>> a
inf
>>> -a
-inf # - inifini
>>> a + a
inf
>>> a - a # opération interdite
nan # not a number
>>> a + a == a
True
>>> b = 2.0 * 10 ** 309 # b = inf
>>> c = 2 * 10 ** 1000 # un integer
>>> c > b # inf est plus grand que tous les nombres
False
Attention donc, les comparaisons entre grands entiers et grands flottants ne sont pas correctes mathématiquement parlant. Il faut absolument les éviter.
Bon okay… c’est bizarre mais on retrouve quelques-unes de ces règles en maths, lorsqu’on calcule des limites.
Et pour nan
? Et bien c’est mieux encore :
>>> a = float('inf') - float('inf')
>>> a
nan
>>> a == a
False
En gros, nan
est le résultat d’une opération qui ne peut être comparé…
Lorsqu’on a deux nombres immenses comme
$a = \text{nombre de grains de sable sur terre}^\text{nombre d’étoiles de la voie lactée}$
$b = \text{nombre d’étoiles de la voie lactée}^\text{nombre de grains de sable sur terre}$
qu’on soustraie, on ne peut deviner l’ordre de grandeur.
Ainsi ce résultat est évalué à nan
Deux nan
ne sont pas forcément égaux… D’où nan != nan
Deux problèmes dans les calculs avec les flottants #
Absorption #
>>> (1.0 + 2.0**53) - 2.0**53 # = 1
0.0 # 1 a été absorbé par l'enorme nombre 2**53
>>> 2.0**53 - 2.0**53 + 1 # on change l'ordre...
1 # et ça fonctionne
Annulation #
Soustraire deux nombres proches fait perdre de la précision
>>> a = 2.0 ** 53 + 1
>>> b = 2.0 ** 53
>>> a - b
0.0
Il peut y avoir des conséquences #
Les calculs avec des flottants engendrent toujours des erreurs qu’il est possible d’éviter en limitant leur quantité et les répétitions.
-
Le 25 février 1991, à Dharan en Arabie Saoudite, un missile Patriot américain a raté l’interception d’un missile Scud irakien, ce dernier provoquant la mort de 28 personnes. L’enquête a mis en évidence le défaut suivant :
-
L’horloge interne du missile mesure le temps en 1/10s. Ce nombre n’est pas dyadique et est converti avec une erreur d’environ 0,000000095s
-
Le missile a été mis en route 100h avant son lancement, ce qui entraine un décalage de $$0,000000095 \times 100 \times 3600 \times 10 \approx 0,34s.$$
-
C’est assez pour qu’il rate sa cible.
Outils #
Obtenir la représentation interne d’un nombre à virgule flottante en Python :
import struct
def float_rep(num: float) -> str:
"""
Renvoie les 64 bits de la représentation interne en précision double
s : signe : 1 bit
e : exposant : 11 bits
m : mantisse : 52 bits
num = (-1)**s * 1,m * 2 ** (e - 1023)
"""
return ''.join("{:08b}".format(elem) for elem in struct.pack('!d', num))
print(float_rep(8 + 4 + 2 + 1/4))
Qui affiche :
0100000000101100100000000000000000000000000000000000000000000000
Représentation des flottants #
Faisons le calcul :
Commençons par découper :
- signe : 1 bit,
- exposant : 11 bits,
- mantisse : 52 bits
0100000000101100100000000000000000000000000000000000000000000000
Soit
0 10000000010 1100100000000000000000000000000000000000000000000000
^ \_________/ \__________________________________________________/
| exposant e mantisse m
|
signe s
s: 0
le nombre est positif
C’était facile.
Pour l’exposant, il faut lui soustraire 1023 :
exposant:
10000000010
soit (0b10000000010 - 1023) = (1026 - 1023) = 3
Le vrai exposant est donc 3
, le nombre est multiplé par 2 ** 3
.
Cette soustraction permet de représenter les exposants négatifs (proches de 0) par des nombres positifs !
Pour la mantisse, on doit se souvenir que le premier bit est toujours égal à 1 et est effacé :
mantisse
1100100000000000000000000000000000000000000000000000
Ces bits étant après la virugle, on peut enlever les 0 finaux :
11001
On ajoute 1 avant la virgule :
1,11001 = 1 + 1/2 + 1/4 + 0 + 0 + 1/32 = 57 / 32
On peut maintenant appliquer la formule vue plus haut :
n = (-1)**0 * 57/32 * 2**3 = 14.25
Vérifions : 8 + 4 + 2 + 1/4 = 14.25
Un outil en ligne : floating point converter,