Concernant les optimisations faites par les compilateurs, il y a toute une littérature sur le sujet.
Globalement, on peut distinguer les optimisations locales et les optimisations globales. Les premières sont évidemment les moins difficiles à mettre en oeuvre.
Les optimisations locales sont généralement appelées des optimisations "à lucarne", c'est-à-dire qu'on regarde chaque petit bout de code généré à la loupe (ou par la lucarne, d'où l'expression), pour voir si on ne peut pas reconnaître des motifs facilement optimisables. Typiquement, des sous-expressions constantes, ou du code constant exécuté plusieurs fois (au sein d'une boucle par exemple). Ou bien, une valeur affectée à une variable, qu'on n'utilise pas ensuite (on peut alors la supprimer).
Tu remarqueras que ces optimisations sont valables quel que soit le processeur. D'autres optimisations, encore plus locales, et éventuellement propre au processeur pour lequel on génère du code machine, peuvent être intéressantes. Par exemple sur l'Intel 8086, l'instruction assembleur "MOV AX, 0" était moins rapide (et en plus était plus grosse) que "XOR AX, AX". Comme ces 2 instructions ont le même effet, on utilisait plutôt la deuxième à la place de la première.
Enfin, d'autres optimisations font du réordonnancement. Par exemple, si tu écris : "i = 0; j = 0; i = i + 1;", le compilateur peut constater que les instructions 2 et 3 sont indépendantes et peuvent donc être échangées. Comme les variables i et j vont être mises dans un registre pour être manipulées, il peut être plus efficace de réordonnancer ce code en "i = 0; i = i + 1; j = 0;", ce qui évitera d'écrire i (valant 0) en mémoire, puis de re-rapatrier i depuis la mémoire vers un registre (pour lui affecter 1). Deux échanges mémoire <--> registre processeur économisés, c'est toujours bon à prendre. Surtout si ce code est exécuté plein de fois...
Le principal avantage des optimisations à lucarne est que l'on peut les enchaîner. Par exemple, avec une analyse par graphe, un compilateur (assez intelligent, il faut le reconnaître) peut voir que le code ci-dessus, une fois réordonnancé, peut être réécrit "i = 0 + 1; j = 0;", puisque i a une valeur prévisible dans la 2ème affectation (zéro). Or, une 3ème optimisation peut être apportée, puis que 0 + 1 est une expression constante, et on va se retrouver avec le code généré "i = 1; j = 0;".
Autre type d'optimisation, permise seulement par certains langages. Il s'agit du code rajouté par les compilateurs.
Par exemple, en Pascal, pour parcourir un tableau tab, on écrit:
var i: integer;
for i := 0 to N do begin
tab[ i] := ...;
end;
Si le compilateur est "sûr", il va générer, à chaque fois qu'on accède au tableau, un peu de code pour vérifier qu'on ne déborde pas du tableau (histoire d'éviter des bugs mémoire tordus). En Ada, on va plutôt écrire:
for i in tab'Range loop
tab(i) := ...;
end loop;
Ici, le compilateur a la garantie que l'indice de boucle ne sortira pas des bornes du tableau, donc le code de vérification de l'indice peut être supprimé !
Les optimisations globales sont beaucoup plus délicates (et je connais nettement moins). Dans cette catégorie peut être aussi rangé le réordonnancement de code, mais en général, l'échelle est toute autre. Par exemple, soit la boucle C suivante :
for (int i = 0; i < 1000; i++) {
for (int k = 0; k < 5; k++) {
tab[k][ i] = 0;
}
}
Si la mémoire est paginée par pages de 1000 mots par exemple, chaque itération de la boucle extérieure accèdera à toutes les pages, ce qui est coûteux. Le code suivant lui sera strictement équivalent, mais chaque page ne sera accédée qu'une seule fois avant d'être chassée par la suivante :
for (int k = 0; k < 5; k++) {
for (int i = 0; i < 1000; i++) {
tab[k][ i] = 0;
}
}
Il sera donc (de loin) beaucoup plus rapide.
Enfin, pour les compilateurs les plus performants (genre compilateurs générant du code parallèle), on peut essayer de voir quels sont les instructions qui sont complètement indépendantes pour essayer de les faire exécuter en même temps par le processeur (ce qui est possible si, en particulier, les fonctions du processeur utilisées par ces instructions sont aussi différentes. Genre, une instruction mathémtique en même temps qu'une instruction arithmétique sur des entiers).
C'est d'ailleurs, si je ne me trompe pas, ce qu'essaient de faire les processeurs les plus récents (avec leur architecture pipelinée et leur analyseur prédictif).
En espérant que cela t'aura éclairé un peu...