Réponses:
Oui, la compilation en Java bytecode est plus facile que la compilation en code machine. C'est en partie parce qu'il n'y a qu'un seul format à cibler (comme le mentionne Mandrill, bien que cela ne fasse que réduire la complexité du compilateur, pas le temps de compilation), en partie parce que la JVM est une machine beaucoup plus simple et plus pratique à programmer que les CPU réels - comme elle a été conçue dans en tandem avec le langage Java, la plupart des opérations Java correspondent à une seule opération de bytecode d'une manière très simple. Une autre raison très importante est que pratiquement aucunl'optimisation a lieu. Presque tous les problèmes d'efficacité sont laissés au compilateur JIT (ou à la JVM dans son ensemble), de sorte que tout le milieu des compilateurs normaux disparaît. Il peut essentiellement parcourir l'AST une fois et générer des séquences de bytecode prêtes à l'emploi pour chaque nœud. Il y a un "surcoût administratif" de génération de tables de méthodes, de pools constants, etc. mais ce n'est rien comparé à la complexité de, disons, LLVM.
Un compilateur est simplement un programme qui prend des fichiers texte 1 lisibles par l'homme et les traduit en instructions binaires pour une machine. Si vous prenez du recul et réfléchissez à votre question dans cette perspective théorique, la complexité est à peu près la même. Cependant, à un niveau plus pratique, les compilateurs de code d'octets sont plus simples.
Quelles sont les grandes étapes à franchir pour compiler un programme?
Il n'y a que deux différences réelles entre les deux.
En général, un programme avec plusieurs unités de compilation nécessite une liaison lors de la compilation avec du code machine et généralement pas avec du code octet. On pourrait diviser les cheveux pour savoir si la liaison fait partie de la compilation dans le contexte de cette question. Si c'est le cas, la compilation de code d'octets serait légèrement plus simple. Cependant, la complexité de la liaison est compensée au moment de l'exécution lorsque de nombreux problèmes de liaison sont traités par la machine virtuelle (voir ma note ci-dessous).
Les compilateurs de code d'octet ont tendance à ne pas optimiser autant car la machine virtuelle peut faire mieux à la volée (les compilateurs JIT sont un ajout assez standard aux machines virtuelles de nos jours).
De cela, je conclus que les compilateurs de code d'octets peuvent omettre la complexité de la plupart des optimisations et de toutes les liaisons, en reportant les deux à l'exécution de la machine virtuelle. Les compilateurs de code d'octet sont plus simples à mettre en pratique car ils pelletent de nombreuses complexités sur la machine virtuelle que les compilateurs de code machine assument.
1 Sans compter les langues ésotériques
Je dirais que cela simplifie la conception du compilateur puisque la compilation est toujours Java en code machine virtuelle générique. Cela signifie également que vous n'avez besoin de compiler le code qu'une seule fois et qu'il s'exécutera sur n'importe quelle plate-forme (au lieu d'avoir à compiler sur chaque machine). Je ne suis pas sûr que le temps de compilation soit plus bas car vous pouvez considérer la machine virtuelle comme une machine standardisée.
D'un autre côté, chaque machine devra avoir la machine virtuelle Java chargée pour pouvoir interpréter le "code d'octet" (qui est le code de la machine virtuelle résultant de la compilation du code java), le traduire en code machine réel et l'exécuter .
Imo c'est bon pour les très gros programmes mais très mauvais pour les petits (car la machine virtuelle est un gaspillage de mémoire).
La complexité de la compilation dépend en grande partie de l'écart sémantique entre la langue source et la langue cible et du niveau d'optimisation que vous souhaitez appliquer tout en comblant cet écart.
Par exemple, la compilation de code source Java en code d'octet JVM est relativement simple, car il existe un sous-ensemble de base de Java qui correspond à peu près directement à un sous-ensemble de code d'octet JVM. Il y a quelques différences: Java a des boucles mais pas GOTO
, la JVM a GOTO
mais pas de boucles, Java a des génériques, pas la JVM, mais ceux-ci peuvent être facilement traités (la transformation des boucles en sauts conditionnels est triviale, l'effacement de type est légèrement moins donc, mais toujours gérable). Il existe d'autres différences mais moins graves.
La compilation du code source Ruby en code octet JVM est beaucoup plus complexe (en particulier avant invokedynamic
et a MethodHandles
été introduite dans Java 7, ou plus précisément dans la 3e édition de la spécification JVM). Dans Ruby, les méthodes peuvent être remplacées lors de l'exécution. Sur la JVM, la plus petite unité de code qui peut être remplacée au moment de l'exécution est une classe, donc les méthodes Ruby doivent être compilées non pas en méthodes JVM mais en classes JVM. La répartition de méthode Ruby ne correspond pas à la répartition de méthode JVM et auparavant invokedynamic
, il n'y avait aucun moyen d'injecter votre propre mécanisme de répartition de méthode dans la JVM. Ruby a des continuations et des coroutines, mais la JVM n'a pas les moyens de les implémenter. (Les JVMGOTO
est limité aux sauts de cibles au sein de la méthode.) La seule primitive de flux de contrôle de la JVM, qui serait suffisamment puissante pour implémenter des continuations, sont des exceptions et pour implémenter des threads coroutines, qui sont tous deux extrêmement lourds, alors que le but principal des coroutines est de être très léger.
OTOH, la compilation du code source Ruby en code octet Rubinius ou en code octet YARV est à nouveau triviale, car les deux sont explicitement conçus comme une cible de compilation pour Ruby (bien que Rubinius ait également été utilisé pour d'autres langages tels que CoffeeScript, et le plus célèbre Fancy) .
De même, la compilation de code natif x86 en code d'octet JVM n'est pas simple, encore une fois, il existe un écart sémantique assez important.
Haskell est un autre bon exemple: avec Haskell, il existe plusieurs compilateurs prêts à la production à haute performance industrielle qui produisent du code machine natif x86, mais à ce jour, il n'y a pas de compilateur fonctionnel pour la JVM ou la CLI, car la sémantique l'écart est si grand qu'il est très complexe de le combler. Il s'agit donc d'un exemple où la compilation en code machine natif est en fait moins complexe que la compilation en code octet JVM ou CIL. Cela est dû au fait que le code machine natif a des primitives de niveau beaucoup plus bas ( GOTO
, pointeurs,…) qui peuvent être plus «forcés» de faire ce que vous voulez que d'utiliser des primitives de niveau supérieur telles que des appels de méthode ou des exceptions.
Donc, on pourrait dire que plus le langage cible est élevé, plus il doit correspondre étroitement à la sémantique du langage source afin de réduire la complexité du compilateur.
Dans la pratique, la plupart des machines virtuelles Java sont aujourd'hui des logiciels très complexes, effectuant une compilation JIT (le bytecode est donc traduit dynamiquement en code machine par la machine virtuelle Java ).
Ainsi, alors que la compilation du code source Java (ou du code source Clojure) en code octet JVM est en effet plus simple, la JVM elle-même effectue une traduction complexe en code machine.
Le fait que cette traduction JIT à l'intérieur de la JVM soit dynamique permet à la JVM de se concentrer sur les parties les plus pertinentes du bytecode. Concrètement, la plupart des JVM optimisent davantage les parties les plus chaudes (par exemple les méthodes les plus appelées ou les blocs de base les plus exécutés) du bytecode JVM.
Je ne suis pas sûr que la complexité combinée de JVM + Java au compilateur de bytecode soit nettement inférieure à la complexité des compilateurs avancés.
Notez également que la plupart des compilateurs traditionnels (comme GCC ou Clang / LLVM ) transforment le code source d'entrée C (ou C ++, ou Ada, ...) en une représentation interne ( Gimple pour GCC, LLVM pour Clang) qui est assez similaire à certains bytecode. Ensuite, ils transforment ces représentations internes (d'abord en les optimisant en elles-mêmes, c'est-à-dire que la plupart des passes d'optimisation GCC prennent Gimple en entrée et produisent Gimple en sortie; en émettant ensuite du code assembleur ou machine) en code objet.
BTW, avec les récentes infrastructures GCC (notamment libgccjit ) et LLVM, vous pouvez les utiliser pour compiler un autre (ou le vôtre) langage dans leurs représentations internes Gimple ou LLVM, puis profiter des nombreuses capacités d'optimisation du milieu de gamme et du back- parties terminales de ces compilateurs.