Lisibilité contre optimisation

L'une des choses qui amuse beaucoup le bidouilleur, c'est de faire preuve d'un maximum de ruse pour faire aller plus vite son programme. Pour mener à bien cette tache, le bidouilleur dispose en gros de trois techniques :

La première méthode est la plus noble, et bien souvent la plus difficile. En effet elle nécessite souvent de ne pas passer son temps devant sa machine, mais dans les livres. Elle est malheureusement très souvent délaissé par les bidouilleurs.

La deuxième méthode tient plus de la bonne maitrise du langage de programmation. L'expérience et la lecture régulière de livre, de magazine et de code produit par d'autre personne permet d'améliorer cela.

La dernière méthode est très souvent la méthode la plus utilisée, et je souhaites vous démontrer à tord. Elle est la plus utilisér car c'est bien souvent celle qui permet le plus de s'approcher du mythe du bidouilleur qui passe ses nuits sur sa machine à améliorer des programmes.

Etude d'un cas

Dans notre cas, nous allons prendre pour exemple un programme de calcul de l'ensemble de Mandelbrot. En général, un programme de calcul de l'ensemble de Mandelbrot, ça ressemble à cela:
mandelbrot.c

/*
  Programme de bench
  calcul de l'ensemble de Mandelbrot

  Emmanuel DUMAS (emmanuel.dumas@free.fr)
  25/10/2001

*/

/* il y a volontairement aucun include,
 cela simplifie la compilation*/

#define XMIN -2.5
#define XMAX 1.5
#define YMIN -1.5
#define YMAX 1.5

#define NMAX 1024
#define SEUIL 4.0

#define LX 1024
#define LY 768

#ifndef NUMBER
#define NUMBER double
#endif


/* Fonction de calcul de l'ensemble de Mandelbrot

rappel du principe :

on calcule la suite zn+1=zn²+z0
si la suite diverge, le point n'appartient pas à l'ensemble
sinon il appartient

*/

int mandelpoint(NUMBER z0_r,
		NUMBER z0_i,
		int nmax)
{
  int cp=0;
  NUMBER zn_r,zn_i,a,b;

  /* initialisation */
  zn_r=z0_r;
  zn_i=z0_i;
  a=zn_r*zn_r;
  b=zn_i*zn_i;
  /* et on fait tourner le tout */
  while ( (cp<nmax) &&( (a+b) < SEUIL ) )
    {
      zn_i=2.0*zn_r*zn_i+z0_i;
      zn_r=a-b+z0_r;
      a=zn_r*zn_r;
      b=zn_i*zn_i;
      cp++;
    }
  return cp;
}

/* Pour obtenir une image on calcul cette fct
   pour tout un plan */
void mandelbrot(const NUMBER xmin,
		const NUMBER xmax,
		const NUMBER ymin,
		const NUMBER ymax,
		const int lx,
		const int ly,
		const int nmax,
		int *res)
{
  int x,y;

  for(x=0;x<lx;x++)
    for(y=0;y<ly;y++)
      {
	NUMBER z0_r,z0_i;
	/* calcul de z0 */
	z0_r=xmin+(xmax-xmin)*x/lx;
	z0_i=ymin+(ymax-ymin)*y/ly;

	/* on sauve le résultat */
	res[x+y*lx]=mandelpoint(z0_r,z0_i,nmax);
      }
}


main()
{
  int i;

  /* On fait le calcul 10 fois pour moyenner le résultat */
  for(i=0;i<10;i++)
    {
      int res[LX*LY];
      mandelbrot(XMIN,XMAX,YMIN,YMAX,LX,LY,NMAX,res);
    }

  return 0;

}

Sur un PC équipé d'un processus Athlon 850 Mhz, on obtient en fonction des options de compilation, les temps d'executions suivant:
option de compilationtemps d'execution
aucune46s29
-O123s94
-O222s42
-O323s88
On peut remarquer que pour ce programme l'option -O3 ne sert à rien puisqu'elle a même tendance à dégrader les performances.

Pour notre deuxième étape nous allons prendre un programme de calcul de l'ensemble de Mandelbrot en priviliégeant la lisibilité du code à la performance, ce qui donne ceci :
mandelbrot_2.c
/*
  Programme de bench
  calcul de l'ensemble de Mandelbrot

  Emmanuel DUMAS (emmanuel point dumas arobase free point france)
  25/10/2001   première version
  05/11/2001   version pas du tout optimisée
*/

/* il y a volontairement aucun include,
 cela simplifie la compilation*/

#define XMIN -2.5
#define XMAX 1.5
#define YMIN -1.5
#define YMAX 1.5

#define NMAX 1024
#define SEUIL 10.0

#define LX 1024
#define LY 768

#ifndef NUMBER
#define NUMBER double
#endif


NUMBER module(NUMBER r,NUMBER i)
{
  return r*r+i*i;
}

/* Fonction de calcul de l'ensemble de Mandelbrot

rappel du principe :

on calcule la suite zn+1=zn²+z0
si la suite diverge, le point n'appartient pas à l'ensemble
sinon il appartient

*/

int mandelpoint(NUMBER z0_r,
		NUMBER z0_i,
		int nmax)
{
  int cp=0;
  NUMBER zn_r,zn_i;

  /* initialisation */
  zn_r=z0_r;
  zn_i=z0_i;

  /* et on fait tourner le tout */
  while ( (cp<nmax) &&( module(zn_r,zn_i) < SEUIL ) )
    {
      NUMBER zn1_r,zn1_i;

      /* zn+1=zn^2+z0 */
      zn1_r= zn_r*zn_r - zn_i*zn_i + z0_r;
      zn1_i= 2.0*zn_r*zn_i         + z0_i;

      
      zn_r=zn1_r;
      zn_i=zn1_i;

      cp++;
    }
  return cp;
}

/* Pour obtenir une image on calcul cette fct
   pour tout un plan */
void mandelbrot(const NUMBER xmin,
		const NUMBER xmax,
		const NUMBER ymin,
		const NUMBER ymax,
		const int lx,
		const int ly,
		const int nmax,
		int *res)
{
  int x,y;

  for(x=0;x<lx;x++)
    for(y=0;y<ly;y++)
      {
	NUMBER z0_r,z0_i;
	/* calcul de z0 */
	z0_r=xmin+(xmax-xmin)*x/lx;
	z0_i=ymin+(ymax-ymin)*y/ly;

	/* on sauve le résultat */
	res[x+y*lx]=mandelpoint(z0_r,z0_i,nmax);
      }
}


main()
{
  int i;

  /* On fait le calcul 10 fois pour moyenner le résultat */
  for(i=0;i<10;i++)
    {
      int res[LX*LY];
      mandelbrot(XMIN,XMAX,YMIN,YMAX,LX,LY,NMAX,res);
    }

  return 0;

}

Avec ce programme, toujours sur le même PC équipé d'un processus Athlon 850 Mhz, on obtient en fonction des options de compilation, les temps d'executions suivant:
option de compilation temps d'execution du programme précédent (pour mémoire) temps d'execution
aucune46s29110s20
-O123s9474s03
-O222s4269s36
-O323s8825s14

Sur cette exemple on constate qu'avec une optimisation -O3, les deux programmes sont à une vitesse comparable.

Conclusion

A de très rare exception près, quand on écrit du code, il faut priviliéger la lisibilité du code à son optimisation. Les compilateurs modernes sont beaucoup plus aptes à optimiser le code que beaucoup de developpeurs actuels.

retour


Emmanuel DUMAS, emmanuel point dumas arobase free point france
Last modified: Thu May 23 11:40:56 CEST 2002