programmez-en-d/virgule_flottante.whata

320 lines
20 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = "Types à virgule flottante"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Goujet"
]
Dans le chapitre précédent, nous avons vu que malgré leur facilité d'utilisation, les opérations arithmétiques sur les entiers sont sujettes à des erreurs de programmations à cause des débordements, des soupassements et des troncages. Nous avons aussi vu que les entiers ne peuvent pas représenter des valeurs avec des parties fractionnaires comme 1,25.
Les types flottants sont spécialement conçus pour prendre en charge les parties fractionnaires. La "virgule" dans leur nom vient de la virgule qui sépare partie entière et partie fractionnaire et "flottant" fait référence à la manière avec laquelle ces types sont implémentés~ : la virgule **flotte** à droite ou à gauche de façon appropriée. (Ce détail n'est pas important pour l'utilisation de ces types).
De même que pour les entiers, nous devons voir des détails important dans ce chapitre. Avant tout, voici une liste de quelques aspects intéressants des types flottants~ :
- Ajouter 0,001 un millier de fois n'est pas pareil qu'ajouter 1.
- Utiliser les opérateur [c > ==] et [c !=] avec des types flottants est une erreur dans la plupart des cas.
- La valeur initiale d'un type flottant est [c .nan], et non 0. [c .nan] ne peut pas être utilisé dans des expressions. Quand [c .nan] est utilisé dans des opérations de comparaison, il n'est ni plus petit, ni plus grand que n'importe quelle valeur.
- La valeur de débordement est [c .infinity] et la valeur de soupassement est [c .infinity] négatif.
Même si les types flottants sont plus utiles dans certains cas, ils ont des singularités que tous les programmeurs doivent connaître. Par rapport aux entiers, ils sont très bons pour éviter les troncages parce que leur but principal est de prendre en charge les valeurs fractionnaires. Comme n'importe quel autre type, basés sur un certain nombre de bits, ils sont également sujet aux dépassements et soupassements, mais comparés aux entiers, l'ensemble des valeurs qu'ils prennent en charge est vaste.
De plus, au lieu d'être silencieux dans le cas de dépassements et de soupassements, ils prennent les valeurs spéciales d'infini positif ou négatif.
Pour rappel, voici les types flottants~ :
|= Type |= Nombre de bits |= Valeur initiale
| [c float] | 32 | [c float.nan]
| [c double] | 64 | [c double.nan]
| [c real] | Au moins 64, peut-être plus (par exemple 80, selon ce que le matériel prend en charge) | [c real.nan]
[ = Attributs des types flottants
Les types flottants ont plus d'attributs que les autres types~ :
- [c .stringof] est le nom du type.
- [c .sizeof] est la longueur du type en octets (pour avoir le nombre de bits, il faut multiplier cette valeur par 8).
- [c .max] est la valeur maximale qu'un type peut stocker ; l'opposé de cette valeur est la valeur minimale que le type peut avoir.
- [c .min_normal] est la plus petite valeur normalisée que ce type peut avoir (le type peut stocker des valeurs plus petites que [c .min_normal] mais la précision de ces valeurs est moins grande que la précision normale du type).
- [c .dig] (pour ''digits'' (chiffres)) indique le nombre de chiffres décimaux significatifs pour la précision du type.
- [c .infinity] est la valeur spéciale utilisée pour indiquer un dépassement ou un soupassement.
Notez que la valeur minimale d'un type flottant n'est pas [c .min] mais l'opposé de [c .max]. Par exemple, la valeur minimale de [c double] est [c -double.max].
D'autres attributs des types flottants sont utilisés moins souvent. Vous pouvez tous les voir sur cette page (en anglais)~ : [[http://digitalmars.com/d/2.0/property.html | Properties for Floating Point Types.]]
Les propriétés des types flottants et leurs relations peuvent être vues sur un axe comme celui-ci~ :
[pre <<<
+ +-----------+------------+ .... + .... +----------+----------+ +
| -max -1 | 0 | 1 max |
-infinity -min_normal min_normal infinity
>>>]
Les parties avec des tirets sont à l'échelle~ : le nombre de valeurs qui peuvent être représentées entre [c min_normal] et 1 est égal au nombre de valeurs qui peuvent être représentées entre 1 et max. Cela veut dire que la précision de la partie fractionnaire des valeurs qui sont entre [c min_normal] et 1 est très grande (c'est également vrai pour le côté négatif).
]
[ = [c .nan]
Nous avons déà vu que [c .nan] est la valeur par défaut des variables flottantes. [c .nan] peut apparaître comme résultat d'une expression flottant n'ayant pas de sens. Par exemple, les expressions flottantes du programme suivant produisent toutes [c double.nan] :
[code=d <<<
import std.stdio;
void main()
{
double zero = 0;
double infini = double.infinity;
writeln("n'importe quelle expression avec nan : ", double.nan + 1);
writeln("zero / zero : ", zero / zero);
writeln("zero * infini : ", zero * infini);
writeln("infini / infini : ", infini / infini);
writeln("infini - infini : ", infini - infini);
}
>>>]
]
[ = Écrire des valeurs flottantes
Les valeurs flottantes peuvent simplement être écrites sans point décimal comme 123, ou avec un point décimal comme 12.3 (NdT~ : pour écrire un nombre à virgule, on n'utilise pas la virgule mais le point).
Les valeurs flottantes peuvent aussi être écrites avec la syntaxe flottante~: 1.23e+4. La partie ''e+'' dans cette syntaxe peut être lue comme «~ fois 10 puissance~ ». On lit 1.23e+4 comme ceci «~ 1.23 fois 10 puissance 4~ », qui est pareil que «~ 1.23 fois 104~ », qui est en fait pareil que [m 1.23 \times 10000], ce qui vaut 12 300.
Si la valeur après e est négative, comme pour 5.67e-3, alors on lit «~ divisé par 10 puissance~ ». Ainsi, pour cet exemple, on lit «~ 5.67 divisé par 10[^ 3]~ », ce qui est pareil que [m \frac{5.67}{1000}], ce qui vaut 0.00567.
Les valeurs qui sont affichées par le programme suivant sont toutes dans le format flottant. Il affiche les propriétés des trois types~ :
[code=d <<<
import std.stdio;
void main()
{
writeln("Type : ", float.stringof);
writeln("Précision : ", float.dig);
writeln("Valeur minimale normalisée : ", float.min_normal);
writeln("Valeur maximale : ", float.max);
writeln("Valeur minimale : ", -float.max);
writeln();
writeln("Type : ", double.stringof);
writeln("Précision : ", double.dig);
writeln("Valeur minimale normalisée : ", double.min_normal);
writeln("Valeur maximale : ", double.max);
writeln("Valeur minimale : ", -double.max);
writeln();
writeln("Type : ", real.stringof);
writeln("Précision : ", real.dig);
writeln("Valeur minimale normalisée : ", real.min_normal);
writeln("Valeur maximale : ", real.max);
writeln("Valeur minimale : ", -real.max);
}
>>>]
La sortie du programme est celle-ci dans mon environnement. Comme [c real] dépend [comment <<<erreur de traduction>>>] du matériel (NdT~ : et du système qui tourne dessus), vous pouvez obtenir autre chose~ :
[output <<<
Type : float
Précision : 6
Valeur minimale normalisée : 1.17549e-38
Valeur maximale : 3.40282e+38
Valeur minimale : -3.40282e+38
Type : double
Précision : 15
Valeur minimale normalisée : 2.22507e-308
Valeur maximale : 1.79769e+308
Valeur minimale : -1.79769e+308
Type : real
Précision : 18
Valeur minimale normalisée : 3.3621e-4932
Valeur maximale : 1.18973e+4932
Valeur minimale : -1.18973e+4932
>>>]
]
[ = Observations
Comme on l'a vu dans le chapitre précédent~ ; la valeur maximum de [c ulong] a 20 chiffres décimaux~ : 18 446 744 073 709 551 616. Cette valeur semble petite même comparée au plus petit des types flottants~ : [c float] peut stocker des valeurs de l'ordre de 10[^ 38], par exemple 340 282 000 000 000 000 000 000 000 000 000 000 000.
La valeur maximale de [c real] est de l'ordre de 10[^ 4932], une valeur de plus de 4 900 chiffres décimaux~ !
Regardons la valeur minimale que [c double] peut représenter avec une précision de 15 chiffres décimaux~ : 0.000...''(il y a 300 zéros additionnels ici)''...0000222507385850720.
]
[ = Les débordements et les soupassements
Malgré leur capacité de représenter des valeurs très grandes, les types flottants peuvent aussi déborder ou soupasser. Les types flottants sont plus sûrs que les types entiers dans ce domaine parce que les dépassements et les soupassements ne sont pas ignorés. Les valeurs qui débordent deviennent [c .infinity] et les valeurs qui soupassent deviennent [c -.infinity]. Pour l'observer, augmentons la valeur de [c .max] de 10%. Comme la valeur est déjà le maximum, ça débordera~ :
[code=d <<<
import std.stdio;
void main()
{
real valeur = real.max;
writeln("Avant : ", valeur);
// Ajouter 10% est équivalent à multiplier par 1.1
valeur *= 1.1;
writeln("10% ajoutés : ", valeur);
// Essayons de la diminuer en divisant par 2 :
valeur /= 2;
writeln("Divisé par 2 : ", valeur);
}
>>>]
Une fois que la valeur déborde et devient [c real.infinity], elle y reste même après avoir été divisée par 2~ :
[output <<<
Avant : 1.18973e+4932
10% ajoutés : inf
Divisé par 2 : inf
>>>]
]
[ = Précision
La précision est une idée omniprésente dans la vie de tous les jours, sans qu'on y prête toujours attention.. C'est le nombre de chiffres qui sont utilisés pour écrire une valeur. Par exemple, quand on dit que le tiers de 100 est 33, la précision est de 2 parce que 33 a deux chiffres. Quand la valeur est notée plus précisément comme 33.33, la précision est alors de 4 chiffres.
Le nombre de bits qu'a chaque type flottant n'affecte pas seulement sa valeur maximale, mais aussi sa précision. Plus il y a de bits, plus la précision est grande.
]
[ = Il n'y a pas de troncage lors d'une division
Comme nous l'avons vu dans le chapitre précédent, les divisions entières ne conservent pas la partie fractionnaire du résultat~ :
[code=d <<<
int premier = 3;
int second = 2;
writeln(premier / second);
>>>]
Sortie : [output 1]
Les types flottants n'ont pas ce problème de troncage ; ils sont spécialement conçu pour préserver les parties fractionnaires :
[code=d <<<
double premier = 3;
double second = 2;
writeln(premier / second);
>>>]
Sortie : [output 1.5]
La précision de la partie fractionnaire dépend de la précision du type~ : [c real] a la plus grande précision et [c float] la plus petite.
]
[ = Quel type utiliser
Sauf s'il y a une raison spécifique de ne pas le faire, vous pouvez utiliser [c double] pour les valeurs flottantes. [c float] a une précision faible mais parce qu'il est plus petit que les autres types, il peut être utile quand l'espace de stockage est limité.
D'un autre côté, comme la précision de [c real] est plus grande que [c double] sur le même matériel, il sera préférable pour des calculs de haute précision.
]
[ = On ne peut pas représenter toutes les valeurs
On ne peut pas représenter certaines valeurs de la vie de tous les jours. Dans le système décimal que nous utilisons quotidiennement, les chiffres avant la virgule représentent les unités, les dizaines, les centaines, etc. et les chiffres après la virgule représente les dixièmes, les centièmes, les millièmes, etc.
Si une valeur est une combinaisons exacte de ces valeurs, elle peut être représentée précisément. Par exemple, du fait que 0.23 consiste en 2 dixièmes et 3 centièmes, cette valeur est représentée précisément. D'un autre côté, la valeur de [m \frac 1 3] ne peut pas être représenté précisément dans le système décimal, parce quelque soit le nombre de chiffres qui sont écrits, ce n'est jamais suffisant~ : 0,33333...
C'est très similaire pour les types flottants. Parce que ces types sont basés sur un certain nombre de bits, ils ne peuvent pas représenter toutes les valeurs.
La différence avec le système binaire que l'ordinateur utilise est que les chiffres avant la virgule sont les unités, les deuzaines, les quatraines, etc. et les chiffres après la virgules sont les moitiés, les quarts, les huitièmes, etc. Seules les valeurs qui sont des combinaisons exactes de ces chiffres peuvent être représentées précisément.
Par exemple, on ne peut représenter directement dans le système binaire utilisé par les ordinateurs une valeur telle que 0,1 (comme dans 10 centimes). Alors que cette valeur peut être représentée précisément dans le système décimal, sa représentation binaire ne se finit jamais et répète continuellement 4 chiffres~ : 0.0001100110011... (en utilisant l'écriture binaire, pas décimale). C'est toujours imprécis à un certain niveau, selon la précision du type flottant utilisé.
Le programme suivant montre ce problème. La valeur d'une variable est incrémentée de 0.001 un millier de fois dans une boucle. De façon étonnante, le résultat n'est pas 1~ :
[code=d <<<
import std.stdio;
void main()
{
float résultat = 0;
// On s'attendrait à ce que résultat vaille 1 après avoir bouclé 1000 fois :
while (résultat < 1) {
résultat += 0.001;
}
// Vérifions
if (résultat == 1) {
writeln("Comme attendu: 1");
} else {
writeln("DIFFERENT: ", résultat);
}
}
>>>]
[output DIFFERENT: 1.00099]
Parce que 0.001 ne peut pas être représenté précisément, cette imprécision affecte le résultat de multiples fois. La sortie suggère que la boucle a été répétée 1001 fois.
]
[ = Comparer des valeurs flottantes
Nous avons vu les comparaisons suivantes pour les entiers : égal à ([c >==]), n'est pas égal à ([c >!=]), plus grand que ([c >>]), inférieur ou égal à ([c <=]) et supérieur ou égal à ([c >>=]). Les types flottants ont beaucoup plus d'opérateurs de comparaison.
Comme la valeur spéciale [c .nan] représente des valeurs flottantes invalides, il ne peut être comparé avec aucune autre valeur. Par exemple, il n'y a pas de sens à demander lequel de [c .nan] ou de 1 est plus grand que l'autre.
Pour cette raison, les valeurs flottantes ne sont pas toutes ordonnées. Si deux valeurs sont non ordonnées, alors au moins une des valeurs est [c .nan]
Le tableau suivant montre tous les opérateurs de comparaison sur les flottants. Tous les opérateurs sont binaires (ce qui veut dire qu'ils prennent deux opérandes) et sont utilisés comme dans [c > gauche == droite]). Les colonnes qui contiennent [c false] et [c true] sont les résultats des opérations de comparaison.
La dernière colonne indique si l'opération fait encore sens lorsque l'une des opérandes est [c .nan]. Par exemple, même si le résultat de [c 1.2 < real.nan] est [c false], ce résultat n'a pas de sens parce que l'un des opérandes est [c real.nan]. Le résultat de la comparaison inverse [c real.nan < 1.2] est également fausse. L'abréviation «~ cg~ » veut dire «~ côté gauche~ », désignant l'expression qui est à gauche de chaque opérateur.
[deftag=cfalse [c color=red false]]
[deftag=ctrue [c color=green true]]
[deftag=non [c color=red non]]
[deftag=oui [c color=green oui]]
|><= Opérateur |><= Signification |><= Si cg est plus grand |><= si cg est plus petit |><= S'ils sont égaux |><= Si l'un des deux est [c .nan] |><= Sensé avec [c .nan]
|>< [c > ==] |>< est égal à |>< [cfalse] |>< [cfalse] |>< [ctrue] |>< [cfalse] |>< [oui]
|>< [c !=] |>< n'est pas égal à |>< [ctrue] |>< [ctrue] |>< [cfalse] |>< [ctrue] |>< [oui]
|>< [c > >] |>< est plus grand que |>< [ctrue] |>< [cfalse] |>< [cfalse] |>< [cfalse] |>< [non]
|>< [c >=] |>< est supérieur ou égal à |>< [ctrue] |>< [cfalse] |>< [ctrue] |>< [cfalse] |>< [non]
|>< [c <] |>< est plut petit que |>< [cfalse] |>< [ctrue] |>< [cfalse] |>< [cfalse] |>< [non]
|>< [c <=] |>< est inférieur ou égal à |>< [cfalse] |>< [ctrue] |>< [ctrue] |>< [cfalse] |>< [non]
|>< [c !<>=] |>< n'est ni supérieur, ni inférieur, ni égal à |>< [cfalse] |>< [cfalse] |>< [cfalse] |>< [ctrue] |>< [oui]
|>< [c <>] |>< plus grand ou plus petit que |>< [ctrue] |>< [ctrue] |>< [cfalse] |>< [cfalse] |>< [non]
|>< [c <>=] |>< est inférieur, supérieur, ou égal à |>< [ctrue] |>< [ctrue] |>< [ctrue] |>< [cfalse] |>< [non]
|>< [c !<=] |>< n'est ni inférieur, ni égal à |>< [ctrue] |>< [cfalse] |>< [cfalse] |>< [ctrue] |>< [oui]
|>< [c !<] |>< n'est pas inférieur à |>< [ctrue] |>< [cfalse] |>< [ctrue] |>< [ctrue] |>< [oui]
|>< [c !>=] |>< n'est pas supérieur ou égal à |>< [cfalse] |>< [ctrue] |>< [cfalse] |>< [ctrue] |>< [oui]
|>< [c !>] |>< n'est pas plus grand que |>< [cfalse] |>< [ctrue] |>< [ctrue] |>< [ctrue] |>< [oui]
|>< [c !<>] |>< n'est ni inférieur, ni supérieur à |>< [cfalse] |>< [cfalse] |>< [ctrue] |>< [ctrue] |>< [oui]
Notez qu'il est sensé d'utiliser [c .nan] avec n'importe quel opérateur qui contient «~ n'est pas~ » dans sa signification et le résultat est toujours vrai. [c .nan] n'étant pas une valeur valide, le résultat de la plupart des comparaisons n'a pas de sens.
]
[ = [c isnan()] pour tester [c .nan]
Parce que le résultat est toujours [c false] pour l'égalité avec [c .nan] selon le tableau précédent, il n'est pas possible d'utiliser l'opérateur [c > ==] pour déterminer si la valeur d'une variable flottante est [c .nan]~ :
[code=d <<<
if (variable == double.nan) { // ← FAUX
// ...
}
>>>]
Pour cette raison, la fonction [c isnan()] du module [c std.math] doit être utilisée~ :
[code=d <<<
import std.math;
// ...
if (isnan(variable)) { // ← correct
// ...
}
>>>]
De même, pour déterminer si une valeurs n'est pas [c .nan], [c !isnan()] doit être utilisé parce que l'opérateur [c !=] donnerait toujours [c true].
]
[ = Exercices
# Modifiez la calculatrice du chapitre précédent pour prendre en charge les valeurs flottantes. La nouvelle calculatrice doit fonctionner plus précisément avec cette modification. Quand vous essayerez la calculatrice, vous pourrez entrer des valeurs flottantes dans des formats divers comme [c 1000], [c 1.23] et [c 1.23e4]
# Écrivez un programme qui lit 5 valeurs flottantes depuis l'entrée. Le programme doit afficher le double de ces valeurs et ensuite le cinquième de ces valeurs. Cet exercice est une introduction à l'idée des tableaux du chapitre suivant. Si vous écrivez ce programme avec ce que vous avez vu jusqu'à maintenant, vous comprendrez les tableaux et vous les approprierez plus facilement.
[[part:corrections/virgule_flottante | … Les solutions]]
]