Quelques résultats sur la précision des nombres flottants sous Linux

Gazette Linux n°53 — Mai 2000

Xavier Serpaggi

Adaptation française 

Frédéric Marchal

Correction du DocBook 

Article paru dans le n°53 de la Gazette Linux de mai 2000.

Cet article est publié selon les termes de la Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.


Table des matières

Fonctions de May
Notre exemple
Programmation en Turbo-Pascal
p2c sous Linux
gcc sous Linux
Java
Pour en savoir plus

Résumé

Dans cet article je vous propose une découverte expérimentale du comportement de Linux quand il effectue des calculs en simple ou double précision. J'utilise une fonction chaotique pour montrer combien peut varier le calcul résultant d'un même programme selon qu'il est exécuté sous Linux ou sous un système d'exploitation de Microsoft.

Les publics concernés sont les étudiants en math et physique ainsi que les professeurs, bien que les équations mises en jeu soient accessibles à pratiquement tout le monde.

J'utilise les langages Pascal, C et Java puisque ce sont ceux-là qui sont le plus répandus de nos jours.

Ce papier s'intéresse plus particulièrement aux architectures Intel. Les concepts de base sont les mêmes pour d'autres types de processeurs bien que certains détails puissent changer.

Fonctions de May

Ces fonctions construisent une série de termes de la forme :

x0 appartient à [0,1]
x(k+1) = mu . xk . (1 - xk) où mu est un paramêtre
  • Pour 0 <= mu < 3, le comportement des séries est déterministe.

  • Pour 3 <= mu < 4, le comportement est chaotique.

En simplifiant un peu, la différence entre un système chaotique et déterministe est leur sensibilité aux conditions initiales. Un système chaotique est très sensible : une petite variation des valeurs de départ de x0 conduira a des différences croissantes dans les termes suivants. Donc, n'importe quelle erreur qui rôde dans les calculs — comme un manque de précision — donnera éventuellement naissance à des résultats très différents.

D'autres exemples de systèmes chaotiques sont les orbites des satellites et les prévisions météorologiques.

D'un autre côté, un système déterministe n'est pas si sensible. Une petite erreur dans x0 nous ammènera à calculer des termes qui, bien que différents des valeurs exactes, en seront « suffisamment approchés » (quoi que cela puisse signifier).

Un exemple de système déterministe est la trajectoire d'une balle de ping-pong.

Donc, les fonctions chaotiques sont utiles pour tester la précision des calculs de différents systèmes et de différents compilateurs.

Notre exemple

Dans cet exemple je propose d'utiliser les valeurs suivantes :

mu = 3,8
x0 = 0,5

Un calcul précis avec un module spécial permettant une précision de 1000 chiffres donne les résultats suivants :

          k              x(k)
         -----          ---------
           10            0.18509
           20            0.23963
           30            0.90200
           40            0.82492
           50            0.53713
           60            0.66878
           70            0.53202
           80            0.93275
           90            0.79885
          100            0.23161

Comme vous pouvez le voir, les séries fluctuent joyeusement entre 0 et 1.

Programmation en Turbo-Pascal

Un programme pour calculer cette fonction s'écrit simplement en Turbo Pascal pour MS-DOS : version texte.

program caos;

{$n+}       { vous devez activer le calcul materiel des flottants pour pouvoir
              utiliser le type etendu }

uses
   crt;

var
   s : single;    { 32-bit real }
   r : real;      { 48-bit real }
   d : double;    { 64-bit real }
   e : extended;  { 80-bit real }

   i : integer;  

begin
   clrscr;

   s := 0.5;
   r := 0.5;
   d := 0.5;
   e := 0.5;

   for i := 1 to 100 do begin
      s := 3.8 * s * (1 - s);
      r := 3.8 * r * (1 - r);
      d := 3.8 * d * (1 - d);
      e := 3.8 * e * (1 - e);

      if (i/10 = int(i/10)) then begin
         writeln (i:10, s:16:5, r:16:5, d:16:5, e:16:5);
      end;
   end;

   readln;
end.

Comme vous pouvez le voir, Turbo Pascal a un certain nombre de types de flottants, chacun sur un nombre différent de bits. Dans chaque cas des bits sont réservés pour :

  • le signe : un bit indique une valeur positive ou négative,

  • la magnitude (ou mantisse) : le nombre lui-même codé en binaire,

  • l'exposant : la puissance de 2 par laquelle il faut multiplier la mantisse pour obtenir la vraie valeur du nombre. Remarquez que ça peut très bien être négatif.

Par exemple, sur un 386 un flottant de 80 bits est codé par :

  • bits 0 à 55 : la mantisse,

  • bits 56 à 78 : l'exposant,

  • bit 79 : le signe.

Naturellement, le codage des flottants à l'intérieur de la machine est déterminé par le fabricant. Cependant, le compilateur peut spécifier des codages différents pour des calculs internes. Si l'émulation des flottants n'est pas utilisée, le compilateur doit fournir un moyen de transformer les codages du compilateur au matériel. C'est le cas du Turbo Pascal.

Les résultats du programme ci-dessus sont :

     k       single        real         double     extended 
    ----    ---------    ---------    ---------   ----------
     10      0.18510      0.18510      0.18510      0.18510
     20      0.23951      0.23963      0.23963      0.23963
     30      0.88423      0.90200      0.90200      0.90200
     40      0.23013      0.82492      0.82493      0.82493
     50      0.76654      0.53751      0.53714      0.53714
     60      0.42039      0.64771      0.66878      0.66879
     70      0.93075      0.57290      0.53190      0.53203
     80      0.28754      0.72695      0.93557      0.93275
     90      0.82584      0.39954      0.69203      0.79884
    100      0.38775      0.48231      0.41983      0.23138

Dans tous les cas, les premiers termes sont assez proches, étant donné que les pertes de précision (par troncature) dues aux calculs intenses n'ont jamais eu lieu. Il en résulte que le format le moins précis (single) perd tout contact avec la réalité aux alentours de x30, alors que le format réel s'en tire jusqu'à environ x60 et que le format double n'est pas encore complètement faux à x90. Ce sont tous des codages flottants du compilateur.

Le format étendu — qui est le format de codage natif du matériel — garde une précision suffisante jusqu'à x100. L'expérience me fait dire qu'il va probablement diverger aux alentours de x110.

p2c sous Linux

Le programme ci-dessus peut-être compilé pratiquement sans changement en utilisant le programme de traduction p2c disponible sous Linux :

p2c caos.pas

converti caos.pas en coas.c

cc caos.c -lp2c -o caos

compile coas.c avec la bibliothèque p2c en utilisant gcc

Les résultats deviennent :

     k       single        real         double     extended 
    ----    ---------    ---------    ---------   ----------
     10      0.18510      0.18510      0.18510      0.18510
     20      0.23951      0.23963      0.23963      0.23963
     30      0.88423      0.90200      0.90200      0.90200
     40      0.23013      0.82493      0.82493      0.82493
     50      0.76654      0.53714      0.53714      0.53714
     60      0.42039      0.66878      0.66878      0.66878
     70      0.93075      0.53190      0.53190      0.53190
     80      0.28754      0.93558      0.93558      0.93558
     90      0.82584      0.69174      0.69174      0.69174
    100      0.38775      0.49565      0.49565      0.49565

Il est intéressant de noter que le traducteur p2c convertit les nombres flottants simple précision du Pascal en simple précision du C, alors que les types real, double et extended sont tous transformés en type double du C. C'est le format qui garde sa précision jusqu'aux alentours de x80.

Je ne dispose pas de données pour prouver le bien fondé de ce qui suit, mais mon impression est que le type C double de codage des flottants se fait également sur 64 bits, mais avec une répartition mantisse/exposant différente de celle du Turbo Pascal.

gcc sous Linux

Le programme ci-dessus, réécrit en C et compilé avec gcc donne, de manière assez naturelle, exactement les mêmes résultats qu'avec p2c :

#include <stdio.h>

int main() {

  float f;
  double d;

  int i;

  f = 0.5;
  d = 0.5;

  for (i = 1; i <= 100; i++) {
    f = 3.8 * f * (1 - f);
    d = 3.8 * d * (1 - d);

    if (i % 10 == 0) 
      printf ("%10d  %20.5f %20.5f\n", i, f, d);
  }
}

Java

Le langage de programmation Java est un cas complètement à part, étant donné que, dès le départ, il a été conçu pour fonctionner sur des plateformes aussi nombreuses que différentes.

Un fichier Java .class contient le code source du programme compilé dans le langage d'une Machine Virtuelle. Ce fichier « exécutable » est alors interprété sur un système client par le premier interprête Java disponible.

Cependant, dans les spécifications Java, les problèmes de précision sur les flottants sont pris très au sérieux. Chaque interprête Java devrait obtenir exactement les mêmes résultats sur des calculs en simple ou double précision.

Ceci signifie qu'un même programme devra :

  • être exécuté avec la même précision sur différentes architectures (par exemple Intel, Motorola, Alpha, ...),

  • être exécuté avec la même précision sur la même architecture, même si l'interprête Java est différent.

Le lecteur peut aisément expérimenter cela. L'applet suivante calcule les séries de May dont nous avons parlé. Comparez ses résultats sur votre propre configuration en l'exécutant avec Netscape, HotJava, appletviewer, etc... Vous pouvez également comparez avec les mêmes navigateurs, ou d'autres, sous Windoze. Ouvrez simplement cette page avec chaque navigateur.

Jusqu'à présent je n'ai trouvé qu'une seule exception à cette règle. Vous devinez qui ? Microsoft Explorer 3.0 !

Pour terminer, voilà le code source de l'applet Java :

import java.applet.Applet;
import java.lang.String;
import java.awt.*;

public class caos extends Applet {

    public void paint (Graphics g) {

	float f;
	double d;
	String s;
	
	int i, y;
	
	f = (float)0.5;
	d = 0.5;
	
	g.setColor (Color.black);
	g.drawString ("k", 10, 10);
	g.drawString ("float", 50, 10);
	g.drawString ("double", 150, 10);
	g.setColor (Color.red);
	y = 20;

	for (i = 1; i <= 100; i++) {
	    f = (float)3.8* f * ((float)1.0 - f);
	    d = 3.8 * d * (1.0 - d);
	    
	    if (i % 10 == 0) { 
		y += 12;
		g.drawString (java.lang.String.valueOf(i), 10, y);
		g.drawString (java.lang.String.valueOf(f), 50, y);
		g.drawString (java.lang.String.valueOf(d), 150, y);
	    }
	}
    }

}

Pour en savoir plus

An introduction to Chaotic Dynamical Systems. R.L. Devaney.

Jurassic Park I and II. Michael Crichton. (les livres, pas les films !).

The Intel 386-SX microprocessor data sheet. Intel Corp.. http://developer.intel.com.

Adaptation française de la Gazette Linux

L'adaptation française de ce document a été réalisée dans le cadre du Projet de traduction de la Gazette Linux.

Vous pourrez lire d'autres articles traduits et en apprendre plus sur ce projet en visitant notre site : http://www.traduc.org/Gazette_Linux.

Si vous souhaitez apporter votre contribution, n'hésitez pas à nous rejoindre, nous serons heureux de vous accueillir.