Calculs avec virgules flottantes


L'article sur les calculs flottants souligne les limites des types float, quel que soit le langage utilisé. La conclusion de cette démonstration est de garder des marges de manoeuvre conséquentes par rapport aux types utilisés. Elle souligne aussi l'intérêt d'utiliser des types double plutôt que float.

Rappel Java

Pour manipuler des valeurs numériques, avec décimale, java nous propose les types float et double. Le type float permet de gérer des valeurs entre -3.40x1038 et 3.40x1038, avec une valeur absolue minimale de 1.17x10-38. Le type double est plus volumineux, puisqu'il prend en compte les nombres entre -1.80x10308 et 1.80x10308, avec une valeur absolue minimale de 2.22x10-308.

Le réflexe habituel est de se contenter de float lorsqu'on est dans la fourchette supportée, ce qui est le cas le plus courant, avec pour objectif louable d'économiser de la mémoire. Ce réflexe va à l'encontre de la simplicité avec java puisque pour que le compilateur interprète un nombre à décimales comme un float, il faut le suffixer par f, sinon il sera considéré comme un double.

 float monNombre = 1.2;  // Ne compile pas car 1.2 est un double
 float monNombre = 1.2f; // Compile car 1.2f est un float

Calculs avec les float

Le risque qu'on court en essayant d'économiser de la mémoire est d'obtenir des résultats eronnés pour cause d'arrondis. Les erreurs de calculs peuvent être relativement importantes, et pour des valeurs bien inférieures au limites théoriques.

La classe de test unitaire suivante, exécutée dans jUnit 3.8, fonctionne sans failure :

 import junit.framework.TestCase;
 
 public class AdditionTest extends TestCase {
   public void testPlus() {
     float operande1 = 16777216;
     assertTrue(operande1 + 1.0f == operande1);
     assertTrue(++operande1 == operande1);
   }
 }

Dans cet exemple, additionner 1 à nombre, ou incrémenter ce nombre, est sans effet !!!

Si on retire le f en suffixe de 1.0, celui-ci devient un double et le calcul précédent donne un résultat plus conforme aux attentes. La valeur 16777216 n'est pas choisie au hasard puisque toutes les valeurs supérieures à celles-ci reproduisent l'anomalie.

Un exemple de calcul divergent peut être montré avec des multiplications :

 public void testFois() {
   float x = (3.10f * 2.30f) * 1.5f;
   float y = 3.10f * (2.30f * 1.5f);
   System.out.println( x );  // 10.695
   System.out.println( y );  // 10.694999
   assertTrue(x == y); 
 }

L'assertion échoue ; l'ordre des multiplications a donc une importance !

Pour peu que ce calcul soit à objectif financier, les arrondis peuvent faire basculer le montant vers le centime inférieur.


Calculs avec les double

L'article cité en introduction nous montre un exemple de calcul avec double assez parlant. Il fait des multiplications, additions et soustraction qui devraient toujours donner 1, mais qui diverge assez rapidement :

 double b = 4095.1;
 double a = b + 1;
 double x = 1;
 
 for (int index = 1; index <= 9; index++) {
   x = (a * x) - b;
   System.out.printf("%01d => %.6f\n", index, x);
 }

Le résultat de cette boucle est assez surprenant :

 1 => 1,000000
 2 => 1,000000
 3 => 1,000008
 4 => 1,031259
 5 => 129,040637
 6 => 524468,255009
 7 => 2148270324,241572
 8 => 8799530071030,805000
 9 => 36043755123945184,000000

Il est bien évident que le nombre 4095.1 n'est pas choisi au hasard, puisqu'en prenant d'autres nombres au hasard, on obtiendra systématiquement 1.0000. Le plus étonnant est que la même boucle avec des float fonctionnera parfaitement.

Autre bizarrerie avec Double. Essayez ceci :

 Double.parseDouble("2.2250738585072012e-308")

Il ne reste plus qu'à espérer ne jamais tomber sur ce nombre dans un programme.


Conclusions

La conclusion de ces démonstrations est que dans le cadre de calcul financiers ou d'autres calculs qui demandent une précision particulière, il est peut-être plus prudent de passer par des entiers ou des BigDecimal... Je ne parle évident pas du calcul scientifique dont les contraintes sont beaucoup plus poussées et que je laisse aux spécialistes.

Il faut noter que ces résultats ne sont pas liés au langage java, mais au fonctionnement par virgule flottante de nos processeurs. D'ailleurs, les exemples cités dans l'article de référence sont en C.