Il semble y avoir au moins deux questions différentes possibles ici. Il s’agit essentiellement des compilateurs en général, Java étant simplement un exemple du genre. L'autre est plus spécifique à Java les codes d'octet spécifiques qu'il utilise.
Compilateurs en général
Considérons d’abord la question générale: pourquoi un compilateur utiliserait-il une représentation intermédiaire dans le processus de compilation du code source pour s’exécuter sur un processeur particulier?
Réduction de la complexité
Une réponse à cette question est assez simple: il convertit un problème O (N * M) en un problème O (N + M).
Si on nous donne N langues sources, et M cibles, et que chaque compilateur est complètement indépendant, alors nous avons besoin des compilateurs N * M pour traduire tous ces langages sources en cibles (où une "cible" est quelque chose comme une combinaison d'un processeur et OS).
Si, toutefois, tous ces compilateurs s'accordent sur une représentation intermédiaire commune, nous pouvons avoir N frontaux de compilateur qui traduisent les langues source en représentation intermédiaire et M arrière-plans de compilateur qui traduisent la représentation intermédiaire en un élément approprié pour une cible spécifique.
Segmentation du problème
Mieux encore, il sépare le problème en deux domaines plus ou moins exclusifs. Les personnes qui connaissent / se soucient de la conception, de l'analyse syntaxique et autres choses du langage peuvent se concentrer sur les front-ends du compilateur, tandis que celles qui connaissent les jeux d'instructions, la conception des processeurs, etc., peuvent se concentrer sur le back-end.
Ainsi, par exemple, pour quelque chose comme LLVM, nous avons beaucoup de frontaux pour différentes langues. Nous avons également des interfaces pour de nombreux processeurs. Un mec de la langue peut écrire un nouveau frontal pour sa langue et prendre en charge rapidement de nombreuses cibles. Un responsable du traitement peut écrire un nouveau back-end pour sa cible sans s’occuper de la conception, de l’analyse, etc. du langage.
Séparer les compilateurs en deux parties, avec une représentation intermédiaire pour communiquer entre eux n'est pas original avec Java. C'est une pratique assez courante depuis longtemps (bien avant l'arrivée de Java, en tout cas).
Modèles de distribution
Dans la mesure où Java a ajouté quelque chose de nouveau à cet égard, c'était dans le modèle de distribution. En particulier, même si les compilateurs ont longtemps été séparés en parties front-end et back-end, ils ont généralement été distribués en tant que produit unique. Par exemple, si vous avez acheté un compilateur Microsoft C, il possédait en interne un "C1" et un "C2", qui étaient respectivement le front-end et le back-end - mais ce que vous avez acheté était simplement "Microsoft C" qui incluait à la fois morceaux (avec un "pilote de compilateur" qui coordonne les opérations entre les deux). Même si le compilateur a été construit en deux parties, pour un développeur normal utilisant le compilateur, il ne s'agissait que d'une seule chose qui traduisait le code source en code objet, sans rien de visible entre les deux.
Au lieu de cela, Java a distribué le serveur frontal dans le kit de développement Java et le serveur principal dans la machine virtuelle Java. Chaque utilisateur Java disposait d'un back-end du compilateur pour cibler le système qu'il utilisait. Les développeurs Java ont distribué le code dans le format intermédiaire. Ainsi, lorsqu'un utilisateur le chargeait, la machine virtuelle Java faisait le nécessaire pour l'exécuter sur son ordinateur.
Précédents
Notez que ce modèle de distribution n'était pas entièrement nouveau non plus. À titre d’exemple, le système P d’UCSD fonctionnait de la même manière: les frontaux des compilateurs produisaient du code P et chaque copie du système P incluait une machine virtuelle qui faisait le nécessaire pour exécuter le code P sur cette cible 1 .
Java byte-code
Java byte code est assez similaire à P-code. Ce sont essentiellement des instructions pour une machine assez simple. Cette machine est censée être une abstraction des machines existantes, il est donc assez facile de traduire rapidement vers presque n'importe quelle cible spécifique. La facilité de la traduction était importante dès le départ, car l'intention initiale était d'interpréter les codes d'octet, un peu comme l'avait fait P-System (et, oui, c'est exactement comme cela que les premières implémentations ont fonctionné).
Forces
Le code d'octet Java est facile à produire pour un frontal du compilateur. Si (par exemple) vous avez un arbre assez typique représentant une expression, il est généralement assez facile de le parcourir et de générer du code assez directement à partir de ce que vous trouvez sur chaque nœud.
Les codes d'octets Java sont assez compacts - dans la plupart des cas, beaucoup plus compact que le code source ou le code machine pour la plupart des processeurs classiques (et en particulier pour la plupart des processeurs RISC, tels que le SPARC vendu par Sun lors de la conception de Java). Cela était particulièrement important à l'époque, car l'un des principaux objectifs de Java était de prendre en charge les applets (code incorporé dans des pages Web qui seraient téléchargées avant exécution), à un moment où la plupart des utilisateurs accédaient au modem via des lignes téléphoniques à environ 28,8. kilobits par seconde (bien que, bien sûr, quelques personnes utilisaient encore des modems plus anciens et plus lents).
Faiblesses
La principale faiblesse des codes d'octets Java est qu'ils ne sont pas particulièrement expressifs. Bien qu'ils puissent très bien exprimer les concepts présents dans Java, ils ne fonctionnent pas aussi bien pour exprimer des concepts qui ne font pas partie de Java. De même, s'il est facile d'exécuter des codes d'octets sur la plupart des machines, il est beaucoup plus difficile de le faire d'une manière qui exploite pleinement les avantages d'une machine particulière.
Par exemple, il est assez courant que si vous voulez vraiment optimiser les codes d'octets Java, vous devez faire du reverse engineering pour les convertir en arrière à partir d'une représentation de type code machine, puis les transformer en instructions SSA (ou quelque chose de similaire) 2 . Vous manipulez ensuite les instructions SSA pour effectuer votre optimisation, puis vous traduisez à partir de là un élément qui cible l'architecture qui vous tient à cœur. Même avec ce processus assez complexe, cependant, certains concepts étrangers à Java sont suffisamment difficiles à exprimer, de sorte qu'il est difficile de traduire certains langages sources en un code machine qui fonctionne (même presque) de manière optimale sur la plupart des machines classiques.
Sommaire
Si vous vous demandez pourquoi utiliser les représentations intermédiaires en général, voici deux facteurs principaux:
- Réduire un problème O (N * M) à un problème O (N + M), et
- Casser le problème en morceaux plus gérables.
Si vous vous interrogez sur les spécificités des codes d'octets Java et sur les raisons pour lesquelles ils ont choisi cette représentation particulière plutôt qu'une autre, je dirais que la réponse revient en grande partie à leur intention initiale et aux limites du Web à l'époque. , menant aux priorités suivantes:
- Représentation compacte.
- Rapide et facile à décoder et à exécuter.
- Rapide et facile à mettre en œuvre sur les machines les plus courantes.
Pouvoir représenter plusieurs langues ou exécuter de manière optimale une grande variété de cibles constituait une priorité beaucoup plus basse (si elles étaient considérées comme des priorités).
- Alors, pourquoi le système P est-il le plus souvent oublié? Surtout une situation de prix. Le système P s'est assez bien vendu sur les ordinateurs Apple II, Commodore SuperPets, etc. Lorsque le PC IBM est sorti, le système P était un système d'exploitation pris en charge, mais MS-DOS coûtait moins cher (du point de vue de la plupart des gens, il était essentiellement gratuit). rapidement eu plus de programmes disponibles, puisque c'est ce que Microsoft et IBM (entre autres) ont écrit pour.
- Par exemple, voici comment fonctionne la suie .