Génération de code natif
La génération de code natif est l'étape du processus de compilation transformant l'arbre syntaxique abstrait enrichi d'informations sémantiques en code machine ou en bytecode spécialisé pour la plateforme cible. C'est l'avant-dernière étape du processus de compilation qui se situe avant l'édition des liens.
La phase de génération de code natif inclut généralement :
- Le choix des instructions à émettre ;
- L'ordonnancement des instructions : dans quel ordre émettre les instructions. L'ordonnancement est une optimisation de la vitesse d'exécution qui peut être critique pour les architectures pipelinées ;
- L'allocation de registres : l'allocation des variables aux registres du processeur.
Adaptation à la plateforme cible
La phase de génération de code natif doit prendre en compte au mieux les caractéristiques de la plateforme d'exécution (microprocesseur, machine virtuelle) pour générer du code qui s'y exécute le plus vite possible. Dans le cas d'un processeur, sa microarchitecture joue un rôle déterminant :
- Le compilateur doit favoriser l'unité de prédiction de branchement pour éviter les erreurs de branchements. Celles-ci sont très couteuses en cycles d'horloge car elles nécessitent de vider le pipeline et de le remplir à nouveau avant de pouvoir continuer l'exécution d'un programme. GCC permet aux programmeurs de préciser sur une instruction
if
quel résultat a le plus de chances d'arriver afin de privilégier une branche pour l'instruction de branchement suivante ; - Le compilateur doit réarranger les instructions pour profiter au mieux du pipeline et éviter autant que possible les pauses dues aux dépendances de données ;
- Le compilateur doit réarranger les instructions afin d'éviter les cache miss qui nécessitent de récupérer des informations dans la mémoire vive, beaucoup plus lente que la mémoire cache.
Compilation à la volée
Dans le cas de la compilation à la volée, la génération de code natif doit être rapide et consommer peu de mémoire afin de ne pas pénaliser l'exécution du programme compilé. Il faut alors utiliser des algorithmes différents que pour une compilation séparée de l'exécution. De plus, un compilateur JIT peut profiter des informations de profilage obtenues lors de l'exécution pour choisir quelles parties du code sont à optimiser au maximum.