Optimisation des compilateurs/Les optimisations liées aux constantes

Un livre de Wikilivres.

Beaucoup des optimisations des compilateurs sont liées à des expressions qui manipulent des constantes. Par exemple, certains calculs dont une opérande est constante peuvent être simplifiés. Nous allons les détailler ci-dessous.

La propagation des constantes[modifier | modifier le wikicode]

La première optimisation de ce type, la plus connue, est la propagation des constantes. Ce n'est pas un optimisation à proprement parler, vu qu'elle n'est pas directement à l'origine de gains de performance. Mais elle permet aux autres optimisations liés aux constants de donner le meilleur d'elles-mêmes. Sans propagation des constantes, l'élimination des calculs constants ou des conditions inutiles ne pourrait pas se faire. C’est pourquoi nous en parlons dès le début de ce chapitre.

Elle consiste à remplacer les variables dont la valeur est constante par la constante elle-même.

Par exemple, le code suivant :

const int a = 2;
const int b = 6;
const int c = 8;

int delta = b*b - 4*a*c;

peut être remplacé par :

const int a = 2;
const int b = 6;
const int c = 8;

int delta = 6 * 6 - 4 * 2 * 8;

L'élimination des calculs/conditions constants[modifier | modifier le wikicode]

Quand une expression a tous ses opérandes constants, on sait qu'elle va systématiquement renvoyer le même résultat (sauf dans quelques cas particuliers, que nous aborderons plus bas). Le compilateur peut alors calculer ce résultat et remplacer l'expression par celui-ci. Cette optimisation porte un nom : on parle de constant folding. Il en existe plusieurs cas particuliers, le plus simple étant celui des expressions arithmétiques. Nous n'allons pas l'aborder, mais allons cependant parler de quelques cas particuliers, assez intéressants à connaitre.

L'élimination des expressions arithmétiques[modifier | modifier le wikicode]

Le cas le plus simple est clairement celui où toutes les opérandes sont constantes : le compilateur peut alors faire le calcul lui-même et simplement mettre le résultat directement dans le code du programme. Par exemple, prenons le code suivant. Vous remarquerez qu'il s'agit de l'exemple obtenu dans la section précédente, le résultat de la propagation des constantes dans l'exemple cité.

const int a = 2;
const int b = 6;
const int c = 8;

int delta = 6 * 6 - 4 * 2 * 8;

Après application du constant folding, ce code devient tout simplement :

const int a = 2;
const int b = 6;
const int c = 8;

int delta = - 28;

Cette optimisation est fiable quand la déclaration de la constante et son utilisation se trouve dans le même fichier ou module. Dans le cas où ce n'est pas le cas, après compilation des deux modules (celui qui déclare la constante, et celui qui l'utilise), la modification ultérieure de la valeur de la constante requiert la recompilation du module l'utilisant ; sinon le module continue d'utiliser une version obsolète de la valeur de la constante.

L'élimination des conditions constantes[modifier | modifier le wikicode]

Le second cas particulier élimine les branchements de type if...else dont la condition est constante. Si jamais une condition se retrouve être toujours vraie ou fausse, alors le bloc if..else qui correspond est supprimé et seule la portion qui sera toujours exécutée sera alors intégrée dans le programme.

Par exemple, le code suivant :

int a;

if (true)
{
    a += 6;
}
else
{
    a += 9;
}

sera transformé en :

int a;
a += 6;

Évidemment, les conditions constantes (toujours vraies ou fausses) ne sont jamais écrites telles qu'elles dans un programme. Elles apparaissent aussi suite à l'application d'autres optimisations liées aux constantes, que nous verrons plus loin.

Les appels de fonctions avec des arguments constants[modifier | modifier le wikicode]

Un autre cas particulier est celui de l'appel du fonction avec des arguments constants. On précise que tous les arguments de la fonction doivent être constants pour que la technique fonctionne. Dans ce cas , le compilateur peut simplement calculer le résultat renvoyé par la fonction. Par exemple prenons ce code :

int abs (int n)
{
  if (n > 0)
    return n ;
  else
    return -n ;
}

int main(int n)
{
  int a = -4 ;
  return abs(a) ;
}

On voit que la fonction est appelée avec tous ses arguments constants. Le compilateur peut alors calculer son résultat et remplacer l'appel par une simple affectation du résultat. Le code se simplifie alors en :

int abs (int n)
{
  if (n > 0)
    return n ;
  else
    return -n ;
}

int main(int n)
{
  return 4 ;
}

Mais attention, cette optimisation n'est valable que dans un cas bien précis : celui des expressions et fonctions qui donnent le même résultat pour des opérandes identiques, c'est-à-dire les expressions et fonctions sans effet de bord. Il existe des calculs qui ne respectent pas cette règle, notamment certaines fonctions : ce sont des expressions ou fonctions dites impures.

Un compilateur n'a pas vraiment de moyens pour savoir si une fonction est pure ou impure : il devrait regarder et analyser le code de la fonction, ce qui demanderait trop de ressources pour un résultat qui n'en vaut pas le coup. Dans ces conditions, le compilateur considère toutes les fonctions comme étant impures. La conséquence, c'est que de très nombreuses optimisations seront rendues impossibles si jamais on trouve une fonction au mauvais endroit dans le code source.

Les simplifications de calculs constants[modifier | modifier le wikicode]

Dans la section précédente, nous avons le cas où, suite à la propagation des constantes, une expression voit l'intégralité de ses opérandes devenir constantes. Mais un tel cas de figure est malheureusement assez rare et il arrive souvent que certaines expressions ne soient pas simplifiables ainsi. Seules certaines opérandes vont devenir constantes, alors que d'autres vont rester des variables. Mais diverses optimisations sont possibles malgré tout. Si on ne peut pas éliminer les calculs, le fait que quelques opérandes soient constantes permet quelques simplifications : on peut remplacer le calcul par un autre, plus rapide.

Les simplifications arithmétiques liées aux constantes[modifier | modifier le wikicode]

Le cas principal est celui des expressions arithmétiques, voire de certaines opérations. Pour en donner un exemple, une multiplication par deux peut être efficacement exécutée par un décalage à gauche ou par une simple addition de la valeur à elle-même. Comme autre exemple, multiplier une expression par 0 ou par 1 est inutile, le résultat étant connu à l'avance. Même chose pour une addition ou soustraction avec 0, ou une division par 1. Le compilateur peut reconnaitre de tels cas et supprimer l'instruction inutile.

Les simplifications d'appels de fonction[modifier | modifier le wikicode]