Un peu de culture : le bytecode
On ne saurait passer sous silence le bytecode : le compilateur ne produit pas de code pour un processeur réel, mais pour un processeur conventionnel, une machine virtuelle. Les instructions, sont bien représentées par une suite d'entiers, mais c'est un programme qui les lira et les interprétera. Ce dernier programme est bien évidemment écrit dans un langage d'assez bas niveau, typiquement en C. L'avantage de cette technique est la portabilité, pour obtenir un système fonctionnant sur une nouvelle architecture, il n'y a pas besoin de modifier le compilateur, il suffit a priori de porter le programme qui implémente la machine virtuelle. On peut aller jusqu'à considérer que la portabilité s'applique aussi aux programmes compilés : « Compile once, run everywhere » comme on dit pour Java. C'est un peu exagéré en pratique, car un environnement d'exécution ne se compose pas, dans le cas de Java, seulement d'un processeur, mais aussi de nombreuses fonctions de librairie chargées dynamiquement qui doivent alors se trouver à la fois sur le lieu de compilation et sur celui de l'exécution. On comprend cependant bien le principe, qui autorise les applets de Java.
Évidemment, l'exécution de bytecode est plus lente que l'exécution directe de code du processeur, dit aussi code natif, car il y a, entre autres, un surcoût dû à la mécanique d'interprétation des instructions.
Des exemples de cette technique sont le système de Java (compilateur javac, machine virtuelle java) et le système Objective Caml (compilateur ocamlc, machine virtuelle ocamlrun). Notons que certains compilateurs Java produisent du code natif, tandis que Caml propose un compilateur natif (ocamlopt). Notons également qu'il est possible, lors, disons de la première exécution d'une fonction, de transformer le bytecode en instructions de la machine hôte, on parle alors de compilation à la volée (Just In Time ou JIT). Cela revient un peu à déléguer une partie de la compilation au moment de l'exécution et ne se justifie vraiment qu'en cas de chargement de code à l'exécution entre machines hétérogènes.
Dans le cas du bytecode, le concepteur du langage a le choix de la machine cible. Il va donc l'adapter au langage. C'est par exemple le cas de la machine Java qui fournit des instructions d'appel de méthode et de la machine Caml qui fournit des instructions d'appel de fermeture et des opérations arithmétiques sur les 31 bits de poids fort des entiers.
Pourtant une machine virtuelle peut à priori fournir une plate-forme d'exécution indépendante à la fois du langage (c'est bien ce que fait une machine réelle après tout) et de la machine réelle. C'est un peu le sens du projet .NET de Microsoft, mais il y a loin de la coupe au lèvres, le modèle de la machine .NET étant spécifiquement objet, et bien plus complexe qu'une machine réelle. Des développement sont d'ailleurs en cours dans le sens de l'extension de la machine machine .NET pour s'adapter aux langages fonctionnels.
Une référence intéressante sur la conception d'une machine virtuelle (celle de Caml-Light) est le rapport ZINC.