Au cours d'une revue de code, j'ai pu lire le code Java suivant :
public static boolean multEquals(double factor1, double factor2, double expected){
return factor1*factor2 == expected;
}
Tout le monde sait bien qu'il vaut mieux ne pas utiliser == pour comparer des nombres décimaux en informatique (selon cette règle Sonar).
Il est d'usage, par exemple, d'utiliser une précision à la comparaison.
J'en entends déjà me dire : "oui mais, et si on veut avoir une égalité véritablement stricte, sans imprécision ?"
Il faut savoir qu'une égalité mathématique n'a pas forcément d'équivalent en informatique. Pourquoi ?
- Dans le monde mathématique, la précision des nombres est "infinie".
- Dans le monde courant, la base 10 est utilisée alors qu'en informatique, nous basculons dans le monde du binaire, ce qui implique un changement de base.
Et un nombre fini en base décimale ne l'est pas forcément en base binaire.
Tout ceci est un peu obscur pour vous ?
N'ayez pas peur, cet article va vous permettre d'y voir un peu plus clair !
Qu'est-ce qu'une base en mathématiques ?
Quelques notions préalables.
Un chiffre : On peut comparer un chiffre à une lettre et un nombre à un mot.
Un chiffre est l'élément qui compose le nombre afin de le représenter.
Exemple : 93 est composé des chiffres 9 et 3.
Le nombre 9 est composé du seul chiffre 9.
Un nombre : C'est la représentation d'une quantité. Le nombre répond à la question "Combien ?"
Une base est un ensemble restreint de chiffres. Plus exactement, une base N consiste en N chiffres.
Par exemple, une base 2 (base binaire) disposera de 2 chiffres : 0 et 1.
Une base 8 (base octale) disposera de 8 chiffres (0 à 7) tandis qu'une base 10 (base décimale) disposera de 10 chiffres (0 à 9) et une base 16 (base hexadécimale) disposera de 16 chiffres : 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E et F.
Quelle que soit la base utilisée (pour rester générique, notons-la "base N"), le système communément utilisé est "positionnel". Cela signifie que plus le chiffre est placé à gauche du nombre, plus il a de poids. Voici quelques exemples afin d'illustrer ce propos.
- Base 10 : 246 = 2x102 + 4x101 + 6x100
- Base 8 : 246 = 2x82 + 4x81 + 6x80
- Base 16 : 246 = 2x162 + 4x161 + 6x160
Et pour les nombre à virgule ?
La mécanique est strictement identique. Les exposants après la virgule sont négatifs.
- Base 10 : 13,37 = 1x101 + 3x100 + 3x10-1 + 7x10-2
- Base 8 : 13,37 = 1x81 + 3x80 + 3x8-1 + 7x8-2
- Base 16 : 13,37 = 1x161 + 3x160 + 3x16-1 + 7x16-2
Revenons à notre méthode multEquals
Exécutons le code suivant :
public static void main(String[] args) {
System.out.println(multEquals(0.2,3.0,0.6));
}
En Java, la sortie affichera "false".
Le mystère réside dans le changement de base !
Le théorème mathématique qui explique cela est le suivant :
Si x/y est la fraction irréductible d'un nombre en base b, alors ce nombre se termine si et seulement si tous les facteurs premiers qui divisent y divisent également b.
Dans notre exemple, 0.6 en décimal s'écrit 3/5. Le nombre 5 divise bien 10 (qui est la base décimale), et 3/5 a bien un nombre fini de chiffres après la virgule.
En revanche, nos chers ordinateurs comptent en binaire, et 5 ne divise pas 2.
3/5 a donc un nombre infini de chiffres après la virgule ! Il en va de même pour 0.2 qui s'écrit également 1/5.
Voici le début de 0.2 en binaire : 0.00110011001100110011...
Et malheureusement, nos machines n'ont pas une précision "infinie".
Se pose alors la question de la représentation de tels nombres dans nos programmes.
La représentation des nombres décimaux en binaire
La norme IEEE 754 définit la représentation des nombres à virgule flottante en binaire.
Cette norme repose sur la notation "scientifique" pour représenter un nombre :
(±) [1; base[ ,mantisse x baseexposant
Voici quelques exemples en base décimale
-5923,42 = -5,92342 x 103
0.01337 = 1,37 x 10-2
123 = 1,23 x 102
Sur un nombre en double précision (qui suit notre exemple), un tel nombre est encodé sur 64 bits dans cet ordre :
- 1 bit de signe
- 11 bits d'exposant
- 52 bits de mantisse
Un nombre décimal se représente alors sous la forme :
(signe) mantisse ⋅ 2exposant
Remarque : Il n'est pas nécessaire de représenter ou de stocker le chiffre avant la virgule, car en binaire ce chiffre sera toujours 1.
Le signe
0 pour un nombre positif et 1 pour un nombre négatif.
L'exposant
Il s'agit d'un entier relatif. Il est codé sous une forme "biaisée" afin de le représenter comme un entier non signé (un entier naturel). Pour aboutir à cette représentation (toujours en double précision), on rajoute à notre exposant 1023, ce qui donne sur les 11 bits de notre exposant le nombre binaire 01111111111.
Cela permet de comparer plus simplement deux nombres décimaux. Que l'exposant soit négatif ou non, pour comparer deux nombre décimaux, il suffit désormais de les comparer comme s'il s'agissait d'entiers.
La mantisse
Il s'agit simplement de la valeur du nombre après la virgule. On remarque que la mantisse repose sur un nombre limité de bits. De ce fait, les calculs sur ces nombres font perdre de la précision.
Conséquence ?
Si on reprend notre exemple initial, et qu'on tente d'afficher dans la console le résultat de 0.2 x 3, on obtient la valeur 0.6000000000000001. Cette valeur est donc considérée comme différente de 0.6.
Conclusion
Lorsqu'on compare des nombres à virgules, il est toujours préférable d'utiliser une fonction clé en main capable de le faire. Si cela n'est pas possible alors il faut penser à définir une précision sur la comparaison afin de pouvoir affirmer que deux nombres sont égaux à un epsilon près.