L’optimisation d’un code

 

 

1 • Fonctionnement d'optimisation
2 • Comment optimiser
3 • Exemples

 

 

L'optimisation en programmation consiste à améliorer l'efficacité d'un code, d'un programme ou d'une librairie. Ce dernier permet l'exécution plus rapide d'un programme en prenant moins de mémoire et de ressources.

 

1 • Fonctionnement d'optimisation

Contrairement au test unitaire, l'optimisation est nécessaire une fois le programme fonctionnel. Optimiser en plein milieu d'un code est le plus souvent une perte de temps et s'avère néfaste à la clarté de ce dernier et au bon fonctionnement du programme.Une fois le choix des algorithmes ou l'architecture d'un projet optimisé, c'est au tour du code (vers la fin de l'écriture d'un programme) si ce dernier est nécessaire de le modifier.

Mais, au contraire pour un projet volumineux, l'optimisation de ces algorithmes ou de son architecture est difficile et coûteux, voire impossible à effectuer.

La plupart des compilateurs récents pratiquent de façon automatique un certain nombre d'optimisations qu'il serait fastidieux d'effectuer manuellement et qui rendraient le code source moins lisible.

L'optimisation manuelle locale peut s'avérer nécessaire dans des cas très spécifiques. l'optimiseur d'un compilateur C fournit souvent un code plus efficace que celui qui serait écrit en assembleur par un programmeur expérimenté. Et de surcroit ce code est bien plus facile à maintenir, car les instructions en C restent dans un ordre lié à la seule intelligibilité du code et non aux spécificités de la machine. Dans les optimiseurs actuels, en effet, les ordres machines associés à une instruction ne se trouvent plus nécessairement en position contigue, pour des raisons d'efficacité d'exécution. Cela rend le code assembleur généré particulièrement indéchiffrable.

 

2 • Comment optimiser

  • 2.1 • Optimisation manuelle

il faut mesurer le temps passé dans chacune des parties du programme.Pour cela il faut choisir un paramètre, de préférence simple et mesurable. Ceci peut être par exemple le temps de traitement sur un jeu de donnée précis, ou le nombre d'images affichées par seconde, ou encore le nombre de requêtes traitées par minute. Il n'est pas rare que 80 % à 90 % du temps soit consacré à l'exécution de 10 % du code (les boucles critiques). Les chiffres varient en fonction de la taille et de la complexité des projets. Il faut localiser ces 10 % de code pour être le plus rentable dans ses optimisations. Cette étape de localisation peut être réalisée à l'aide d'outils spécialisés d'instrumentation du code nommés profilers. Ils sont chargés de compter le nombre d'exécutions de chaque fonction et de cycles du microprocesseur correspondants au cours de l'exécution.

On itère sur la section la plus consommatrice de ressource autant de fois que nécessaire cette boucle :

  • optimisation d'une partie du code.
  • mesure du gain de performances.

On peut aussi optimiser un programme à plusieurs niveaux :

  • au niveau algorithmique, en choisissant un algorithme de complexité inférieure (au sens mathématique) et des structures de données adaptées.
  • au niveau du langage de développement, en ordonnant au mieux les instructions et en utilisant les bibliothèques disponibles.
  • en utilisant localement un langage de bas niveau, qui peut être le langage C ou, pour les besoins les plus critiques, le langage assembleur.

On ne passe au niveau supérieur d'optimisation qu'une fois qu'on a épuisé les possibilités d'un niveau. L'utilisation d'un langage de bas niveau sur l'ensemble d'un projet pour des raisons de rapidité est l'une des erreurs les plus communes et les plus coûteuses que puisse faire un projet industriel.

Une bonne connaissance des techniques de structures de données ainsi que des algorithmes se montre bien plus féconde que celle d'un langage d'assemblage. Lorsqu'on a déterminé l'algorithme le plus adéquat, les optimisations les plus efficaces peuvent être obtenues en utilisant l'écriture du code critique dans un langage de haut niveau, pour ensuite faire l'application de transformations mathématiques successives qui préservent la spécification du programme tout en réduisant la consommation des ressources puis traduction du code transformé dans un langage de bas niveau (langage C).

 

  • 2.2 • Optimisation automatique

Les compilateurs sont souvent capables de faire des optimisations locales, auxquelles aucun développeur ne penserait en première approche.

Pour le langage C, cela peut considérer :

  • les variables locales et les registres.
  • les fonctions non implémentées en assembleur en tant que fonction.
  • les switch, qui sont optimum.

Toutefois, on peut grandement aider le compilateur en déclarant les variables avec les mots-clefs const et/ou restrict quand c'est possible autrement, le compilateur ne peut savoir si une zone mémoire est accessible par d'autres références, et désactivera des optimisations (phénomène dit d'alias de mémoire).

 

3 • Exemples

  • 3.1 • Les variables locales

Le code C++ suivant sera en général peu optimisé par le compilateur car il est souvent incapable de savoir si le code de la boucle modifie ou non le compteur d'itérations : un pointeur ou une référence pourrait le modifier.

void MyClass::MyFunc1(int *entier)
{
for(int i = 0; i < 10; i++)
{
MyFunc2(*entier, i);
}
}

Dans cette version, on indique clairement qu'on utilise un entier fixé à l'avance et qui ne sera jamais modifié, autorisant le compilateur à effectuer des optimisations plus agressives :

void MyClass::MyFunc1(int *entier)
{
int localEntier = *entier;
for(int i = 0; i < 10; i++)
{
MyFunc2(localEntier, i);
}
}

 

  • 3.2 • L'extension inline

l'extension inline, ou inlining, est une optimisation d'un compilateur qui remplace un appel de fonction par le code de cette fonction. Cette optimisation vise à réduire le temps d'exécution ainsi que la consommation mémoire. Toutefois, l'extension inline peut augmenter la taille du programme (par la répétition du code d'une fonction). Dès que le compilateur décide d'étendre une fonction, l'opération est assez simple. La modification peut se faire au niveau du code source, d'une représentation intermédiaire (comme un arbre syntaxique abstrait) ou une représentation de bas niveau (bytecode ou autre). Le principe est le calcul des valeurs des arguments, leur stockage dans des variables et finalement l'insertion du code de la fonction en lieu et place de l'appel.

Les fonctions membres d'une classe peuvent être déclarées inline en utilisant le mot clé inline ou en plaçant la définition de la fonction dans la définition de la classe.

inline int max(int a, int b)
{
if(a > b)
{
return a;
}
return b;
}

Les fonctions peuvent être déclarées inline lors d'une expression lambda.

#include <iostream>
using namespace std;
class MyClass
{
public:
void print() { cout << i << ' ' ; } // inline implicite
private:
int i;
};

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *