Les branchements sont une véritable plaie pour les processeurs modernes dotés d'un pipeline. Certes, les processeurs modernes disposent de techniques pour réduire leur impact sur les performances, la plus connue étant de loin la prédiction de branchements. Cependant, éliminer des branchements a quand même des effets sur les performances. Elle libère de la place dans les tampons de l'unité de prédiction de branchement (les Brnch Target Buffer), elle améliore l'utilisation du pipeline, etc. Mais surtout, supprimer des branchements permet aux autres optimisations du compilateur de mieux faire leur travail. En effet, les branchements entrainent l'apparition de dépendances de contrôles, qui empêchent certains déplacements de calculs : il est impossible de déplacer certains calculs avant un branchement, de les faire sortir d'une fonction, etc. Pour comprendre cela, rappelons que la majorité des optimisations travaillent sur des suites d'instructions linéaires, libres de tout branchements, ces suites portant le nom de blocs de base ("basic blocs", en anglais). Les ré-ordonnancements d'instructions, ainsi que certaines optimisations, ne peuvent se faire qu'à l'intérieur d'un de ces bloc de base. Éliminer des branchements permet d'obtenir des blocs de base plus gros, donnant plus de possibilités de ré-ordonnancement ou de simplifications au compilateur. Bref, éliminer des branchements est toujours une bonne chose, et nous allons voir comment le compilateur s'y prend pour ce faire.
L"'inlining".
Une autre optimisation bien connue est l'inlining. Cette technique consiste à éliminer les appels de fonction d'un programme : le corps de la fonction est intégralement recopié à chaque endroit où celle-ci est appelée. Les gains liés à cette optimisation ont plusieurs origines :
Il faut remarquer que cette technique a tendance à fortement augmenter la taille du code : au lieu d'un seul exemplaire de la fonction, celle-ci est recopiée en autant d'exemplaires qu'il y a de sites d'appel. Le programme final prend donc plus de place. Il faut signaler que cette augmentation de la taille du code peut avoir un effet négatif sur les performances, en perturbant l'usage de la mémoire cache. En effet, qui dit code plus gros dit plus de mémoire cache utilisée. On comprend pourquoi cette optimisation n'est effectuée que pour des fonctions relativement petites, pour éviter de trop faire augmenter la taille du code et de nuire aux performances.
Le "Bound Cheking".
Dans certains langages de programmation, les accès à un tableau avec un indice invalide vont lever une exception ou faire planter le programme. Pour cela, le compilateur insère des comparaisons qui vérifient la validité de l'indice. Ces comparaisons prennent évidemment du temps de calcul, et le compilateur dispose d'optimisations pour en éviter qui seraient inutiles. Il peut notamment prouver que certains morceaux de code ne peuvent pas donner lieu à des accès en dehors du tableau, pour éviter d'ajouter les tests de validité de l'indice. C'est notamment le cas avec ce code :
int sum = 0;
for (int i = 0; i < Array.size(); ++i)
 sum += Array[i];
La fusion de branchements.
Maintenant, prenons le cas d'un branchement A, dont l'instruction/adresse de destination est un autre branchement B, qui lui-même pointe sur une instruction C. Au cours de l'exécution, le branchement va envoyer le processeur sur B, qui va aussitôt brancher vers C : autant brancher directement vers C. Dans ce cas, le compilateur va modifier l'adresse de destination du branchement A pour qu'il pointe directement sur l'instruction C.

Optimisation des compilateurs/Les optimisations de la taille du code

Si la vitesse des programmes peut être optimisée, les compilateurs peuvent aussi jouer sur la taille du programme. La taille d'un programme en elle-même n'est pas importante pour des programmes PC puisque la majorité d'entre eux possèdent plusieurs gibioctets de RAM, ce qui laisse une grande marge. Par contre, diminuer la taille du code a des effets indirects sur les performances de ces machines en économisant de la mémoire cache. L'optimisation de la taille du code a cependant une plus grande importance sur certaines applications embarquées ou destinées à l'informatique industrielle, qui doivent faire avec des machines peu puissantes.
Il faut signaler que, parmi les optimisations précédentes, certaines ont un effet sur la taille du code. C'est le cas des simplifications de calculs, des optimisations qui permettent de supprimer des calculs inutiles, la propagation des constantes, le "constant folding", la fusion de boucle, l’élimination des branchements inutiles, etc. Mais l'inverse est aussi vrai : certaines optimisations qui rendent les programmes plus rapides augmentent la taille du code. C'est notamment le cas de l"'inlining", du déroulage de boucles, etc. Aussi, ne vous étonnez pas si les optimisations que nous allons voir sont, pour certaines, l'exact inverse de certaines optimisations vues précédemment.
Le choix des instructions.
La première optimisation consiste à choisir les instructions les plus courtes quand on peut utiliser plusieurs instructions pour une même opération. L'exemple classique sur les processeurs x86 est la mise à zéro d'un registre : il vaut mieux effectuer un XOR qu'un MOV. Par exemple, XOR EAX EAX est plus court que MOV EAX 0. Le seul problème, c'est que les instructions courtes ne sont pas forcément les plus rapides, or le compilateur a tendance à choisir des instructions plus longues, mais plus rapides.
La jonction de branchements.
Il est possible que dans un if…else, certaines lignes de code soient recopiées à la fois dans le if et dans le else. Il arrive de plus que le résultat de ces lignes de code soit totalement indépendant du contenu du else ou du if.
Prenez cet exemple :
if (a > 0)
 --a;
 ++b;
else 
 ++a;
 ++b;
On voit bien que la ligne ++b peut être factorisée vu qu'elle est totalement indépendante du contenu du if et du else. Un compilateur peut effectuer automatiquement cette transformation, ce qui donne :
if (a > 0)
 --a;
else 
 ++a;
L'abstraction de procédure.
Une autre technique très utile est l'abstraction de procédure, aussi appelé factorisation de code. Ce nom traduit bien ce que fait cette technique : elle factorise des morceaux de code identique en un seul. Plus précisémment, elle détecte des morceaux de code présents en plusieurs exemplaires et les transforme en fonction : les exemplaires sont alors retirés du programme, et sont remplacés par un appel à la fonction créée. On peut voir cette technique comme l'exact inverse de l'inlining.
Autant vous dire que décider quand inliner ou utiliser l'abstraction de procédure est généralement un véritable casse-tête. Généralement, les compilateurs disposent de paramètres de compilation pour spécifier s'il faut optimiser pour la taille du code ou pour la vitesse : l'abstraction de procédure est utilisée quand on choisit l'option taille du code, alors que l'inlining est utilisé quand on choisit l'option vitesse.
La suppression de code mort.
Un compilateur peut aussi détecter les morceaux de code qui ne seront jamais exécutés. Par exemple, il est capable de remarquer que le code d'un else ou d'un if dont la condition est constante ne sera jamais exécuté et peut alors supprimer ce code. Cela permet de gagner de la place assez facilement, surtout quand la propagation de constante fait son petit effet.
De même, le code situé après une boucle infinie (en C ou Java : while(true){...} ou for(;;){...}) ne contenant aucune instruction d'interruption (break;) ne sera jamais exécuté et peut donc être supprimé.

Optimisation des compilateurs/Version imprimable

Photographie/Fabricants/Rowi
Rowi était un vendeur d'accessoires photo.

Rowi
Optimisation des compilateurs/Les optimisations liées aux constantes