Les compilateurs sont souvent capable de faire des optimisations locales, auxquelles aucun développeur ne penserait en première approche.
Pour le langage C, cela peut considérer :
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'aliasing mémoire).
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::DoSomething() const { for( int i=0; i<'''m_nbrElements'''; ++i ) { void *ptr = GetSomePtr(); .... } }
Dans cette version, on indique clairement qu'on utilise un nombre d'itérations fixé à l'avance et qui ne sera jamais modifié, autorisant le compilateur à effectuer des optimisations plus agressives:
void MyClass::DoSomething() { '''const int nbrElements = m_nbrElements;''' for( int i=0; i<'''nbrElements'''; ++i ) { .... } }
Une des toutes premières optimisations a été celle de la division et de la multiplication par une puissance de 2.
En effet, l'informatique actuelle repose sur le binaire, puisqu'elle utilise comme élément de base le transistor (et historiquement, auparavant le relais) qui n'autorise que deux valeurs différentes.
On a donc logiquement implémenté en langage machine les opérations de décalage à gauche et décalage à droite.
En effet, en binaire, le décalage d'un nombre d'un cran vers la gauche le multiplie par 2.
Ceci marche aussi pour la division, en décalant les bits vers la droite.
La division (en dehors de ce cas) est une instruction coûteuse en temps machine, et n'est d'ailleurs toujours pas disponible sur la grande majorité des processeurs de type RISC.
Le code C suivant:
inline int f(int a, int b) { return a * b; } int g (int a) { switch (a) { case 10: return f(a, a); case 11: case 12: return f(a - 2, a); case 1200: return f(a - 2, a); default: return f(a, a); } }
Une compilation avec gcc -O4 -S donne:
.file "opt.c" .text .p2align 4,,15 .globl g .type g, @function g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %edx cmpl $12, %edx jg .L14 leal -2(%edx), %eax cmpl $11, %edx jge .L15 movl $100, %eax cmpl $10, %edx .L17: je .L2 movl %edx, %eax .L15: imull %edx, %eax .L2: popl %ebp ret .p2align 4,,7 .L14: movl $1437600, %eax cmpl $1200, %edx jmp .L17 .size g, .-g .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.2 (Mandrake Linux 10.0 3.3.2-6mdk)"
Ce qui pourrait se traduire, pour une compréhension plus aisée, par le code C suivant:
int g(int a) { int eax, b; if (a > 12) /* cas a == 1200 */ goto L14; eax = a - 2; if (a >= 11) /* cas a == 11 ou a == 12 */ goto L15; eax=100; /* = 10 * 10 */ b=10; L17: if (a == b) /* cas a == 10 */ goto L2; /* cas "default" */ eax=a; L15: eax=eax*a; L2: return eax; L14: eax = 1437600; /* = 1200*(1200-2) */ b = 1200; goto L17; }
On peut remarquer par exemple que la fonction 'f' n'a pas été générée, mais que son code a directement été incorporé dans la fonction 'g' (le mot clef 'inline' permet de forcer ce type d'optimisation en C )