Kompilierung nach Bytecode vs. Maschinencode

12

Enthält die Kompilierung, die einen Zwischenbytecode erzeugt (wie bei Java), im Allgemeinen weniger Komplexität (und nimmt daher wahrscheinlich weniger Zeit in Anspruch), anstatt den gesamten Weg zum Maschinencode zurückzulegen?

Julian A.
quelle

Antworten:

21

Ja, das Kompilieren in Java-Bytecode ist einfacher als das Kompilieren in Maschinencode. Dies liegt zum einen daran, dass es nur ein Format für das Targeting gibt (wie Mandrill erwähnt, obwohl dies nur die Komplexität des Compilers und nicht die Kompilierungszeit verringert), zum anderen daran, dass die JVM eine viel einfachere Maschine ist und bequemer zu programmieren ist als echte CPUs - wie sie entwickelt wurden Zusammen mit der Java-Sprache lassen sich die meisten Java-Operationen auf sehr einfache Weise auf genau eine Bytecode-Operation abbilden. Ein weiterer sehr wichtiger Grund ist, dass praktisch keineOptimierung erfolgt. Fast alle Effizienzprobleme bleiben dem JIT-Compiler (oder der gesamten JVM) überlassen, sodass das gesamte mittlere Ende der normalen Compiler verschwindet. Grundsätzlich kann es den AST einmal durchlaufen und fertige Bytecode-Sequenzen für jeden Knoten erzeugen. Das Generieren von Methodentabellen, Konstantenpools usw. ist mit einem gewissen Verwaltungsaufwand verbunden, der jedoch nichts mit der Komplexität von beispielsweise LLVM zu tun hat.

Robert Harvey
quelle
Sie haben "... Mitte von ..." geschrieben. Meinten Sie "... Mitte bis Ende von ..."? Oder vielleicht "... mittlerer Teil von ..."?
Julian A.
5
@Julian "mittleres Ende" ist ein realer Begriff, der in Analogie zu "vorderes Ende" und "
7

Ein Compiler ist einfach ein Programm, das lesbare 1 Textdateien aufnimmt und sie in binäre Anweisungen für eine Maschine übersetzt. Wenn Sie einen Schritt zurücktreten und Ihre Frage aus dieser theoretischen Perspektive betrachten, ist die Komplexität ungefähr gleich. Auf einer praktischeren Ebene sind Bytecode-Compiler jedoch einfacher.

Welche umfassenden Schritte müssen zur Erstellung eines Programms unternommen werden?

  1. Scannen, Parsen und Validieren von Quellcode.
  2. Konvertieren der Quelle in einen abstrakten Syntaxbaum.
  3. Optional: AST verarbeiten und verbessern, wenn die Sprachspezifikation dies zulässt (z. B. Entfernen von totem Code, Nachbestellen von Vorgängen, andere Optimierungen)
  4. Umwandlung des AST in eine Form, die eine Maschine versteht.

Es gibt nur zwei echte Unterschiede zwischen den beiden.

  • Im Allgemeinen erfordert ein Programm mit mehreren Kompilierungseinheiten eine Verknüpfung beim Kompilieren mit Maschinencode und im Allgemeinen nicht mit Bytecode. Man kann sich die Frage stellen, ob das Verknüpfen Teil des Kompilierens im Zusammenhang mit dieser Frage ist. In diesem Fall wäre die Bytecode-Kompilierung etwas einfacher. Die Komplexität der Verknüpfung wird jedoch zur Laufzeit ausgeglichen, wenn viele Verknüpfungsprobleme von der VM behandelt werden (siehe meinen Hinweis unten).

  • Bytecode-Compiler optimieren in der Regel nicht so stark, da die VM dies im laufenden Betrieb besser kann (JIT-Compiler sind heutzutage eine ziemlich standardmäßige Ergänzung für VMs).

Daraus schließe ich, dass Bytecode-Compiler die Komplexität der meisten Optimierungen und das gesamte Verknüpfen weglassen können, wodurch beide auf die VM-Laufzeit verschoben werden. Bytecode-Compiler sind in der Praxis einfacher, weil sie der VM viele Komplexitäten aufbürden, die Maschinencode-Compiler selbst übernehmen.

1 Ohne esoterische Sprachen


quelle
3
Optimierungen und ähnliches zu ignorieren ist albern. Diese "optionalen Schritte" machen einen großen Teil der Codebasis, der Komplexität und der Kompilierzeit der meisten Compiler aus.
In der Praxis ist das richtig. Ich wurde hier akademisch, ich habe meine Antwort aktualisiert.
Gibt es eine Sprachspezifikation, die Optimierungen verbietet? Ich verstehe, dass einige Sprachen es schwierig machen, aber nicht zulassen, dass irgendwelche anfangen?
Davidmh
@ Davidmh Mir sind keine Spezifikationen bekannt, die sie verbieten . Meines Wissens sagen die meisten, dass der Compiler dazu berechtigt ist, aber nicht auf Details eingeht. Jede Implementierung ist anders, da viele Optimierungen von Details der CPU, des Betriebssystems und der Zielarchitektur im Allgemeinen abhängen. Aus diesem Grund ist es weniger wahrscheinlich, dass ein Bytecode-Compiler das optimiert und stattdessen an die VM weitergibt, die die zugrunde liegende Architektur kennt.
4

Ich würde sagen, das vereinfacht das Compiler-Design, da die Kompilierung immer von Java zu generischem Code für virtuelle Maschinen erfolgt. Das bedeutet auch, dass Sie den Code nur einmal kompilieren müssen und er auf jeder Plattform ausgeführt wird (anstatt auf jedem Computer kompilieren zu müssen). Ich bin mir nicht sicher, ob die Kompilierungszeit kürzer sein wird, da Sie die virtuelle Maschine genau wie eine standardisierte Maschine betrachten können.

Auf der anderen Seite muss auf jeder Maschine die Java Virtual Machine geladen sein, damit sie den "Byte-Code" (der aus der Java-Code-Kompilierung resultierende Code der virtuellen Maschine) interpretieren, in den tatsächlichen Maschinencode übersetzen und ausführen kann .

Imo das ist gut für sehr große Programme, aber sehr schlecht für kleine (weil die virtuelle Maschine eine Verschwendung von Speicher ist).

Mandrill
quelle
Aha. Sie glauben also, dass die Komplexität des Mappings des Bytecodes auf den Standardcomputer (dh die JVM) mit der des Mappings des Quellcodes auf einen physischen Computer übereinstimmen würde, sodass kein Grund zu der Annahme besteht, dass Bytecode zu einer kürzeren Kompilierungszeit führen würde?
Julian A.,
Das habe ich nicht gesagt. Ich sagte, das Zuordnen von Java-Code zu Byte-Code (das ist der Virtual Machine Assembler) würde dem Zuordnen des Quellcodes (Java) zum Code der physischen Maschine entsprechen.
Mandrill
3

Die Komplexität der Kompilierung hängt weitgehend von der semantischen Lücke zwischen der Quell- und der Zielsprache und dem Grad der Optimierung ab, den Sie anwenden möchten, um diese Lücke zu schließen.

Zum Beispiel ist das Kompilieren von Java-Quellcode in JVM-Bytecode relativ einfach, da es eine Kernuntermenge von Java gibt, die so ziemlich direkt einer Untermenge von JVM-Bytecode zugeordnet ist. Es gibt einige Unterschiede: Java hat Schleifen, aber keine GOTO, die JVM hat GOTOaber keine Schleifen, Java hat Generika, die JVM nicht, aber diese können leicht behandelt werden (die Umwandlung von Schleifen in bedingte Sprünge ist trivial, Löschung etwas weniger so, aber immer noch überschaubar). Es gibt andere Unterschiede, aber weniger gravierend.

Das Kompilieren von Ruby-Quellcode in JVM-Bytecode ist sehr viel komplizierter (insbesondere bevor invokedynamicund MethodHandleswurden in Java 7 oder genauer in der 3. Edition der JVM-Spezifikation eingeführt). In Ruby können Methoden zur Laufzeit ersetzt werden. In der JVM ist die kleinste Codeeinheit, die zur Laufzeit ersetzt werden kann, eine Klasse. Ruby-Methoden müssen daher nicht zu JVM-Methoden, sondern zu JVM-Klassen kompiliert werden. Der Ruby-Methodenversand stimmt nicht mit dem JVM-Methodenversand überein, und zuvor invokedynamickonnte kein eigener Methodenversandmechanismus in die JVM eingefügt werden. Ruby hat Fortsetzungen und Koroutinen, aber der JVM fehlen die Möglichkeiten, diese umzusetzen. (Die JVM'sGOTO ist auf Sprungziele innerhalb der Methode beschränkt.) Das einzige Kontrollflussprimitiv der JVM, das leistungsfähig genug wäre, um Fortsetzungen zu implementieren, sind Ausnahmen und Coroutinen-Threads, die beide extrem schwer sind, während der gesamte Zweck von Coroutinen darin besteht, Ausnahmen zu implementieren sehr leicht sein.

OTOH, das Kompilieren von Ruby-Quellcode in Rubinius-Bytecode oder YARV-Bytecode ist ebenfalls trivial, da beide explizit als Kompilierungsziel für Ruby entwickelt wurden (obwohl Rubinius auch für andere Sprachen wie CoffeeScript und vor allem für Fancy verwendet wurde). .

Ebenso ist das Kompilieren von nativem x86-Code in JVM-Bytecode nicht einfach, da es wiederum eine ziemlich große semantische Lücke gibt.

Haskell ist ein weiteres gutes Beispiel: Mit Haskell gibt es mehrere produktionsfertige Hochleistungscompiler, die nativen x86-Maschinencode produzieren. Bislang gibt es jedoch weder für die JVM noch für die CLI einen funktionierenden Compiler, da die Semantik Die Lücke ist so groß, dass es sehr komplex ist, sie zu überbrücken. In diesem Beispiel ist die Kompilierung in nativen Maschinencode weniger komplex als die Kompilierung in JVM- oder CIL-Bytecode. Dies liegt daran, dass nativer Maschinencode viel niedrigere Primitive ( GOTO, Zeiger, ...) hat, die einfacher "gezwungen" werden können, das zu tun, was Sie wollen, als Primitive höherer Ebenen wie Methodenaufrufe oder Ausnahmen zu verwenden.

Man könnte also sagen, je höher die Zielsprache ist, desto genauer muss sie mit der Semantik der Quellsprache übereinstimmen, um die Komplexität des Compilers zu verringern.

Jörg W. Mittag
quelle
0

In der Praxis sind die meisten JVMs heutzutage sehr komplexe Software, die eine JIT-Kompilierung durchführt (daher wird der Bytecode von der JVM dynamisch in Maschinencode übersetzt).

Während die Kompilierung von Java-Quellcode (oder Clojure-Quellcode) zu JVM-Bytecode in der Tat einfacher ist, führt die JVM selbst eine komplexe Übersetzung in Maschinencode durch.

Die Tatsache, dass diese JIT-Übersetzung in der JVM dynamisch ist, ermöglicht es der JVM, sich auf die relevantesten Teile des Bytecodes zu konzentrieren. In der Praxis optimieren die meisten JVM mehr die heißesten Teile (z. B. die am häufigsten aufgerufenen Methoden oder die am häufigsten ausgeführten Basisblöcke) des JVM-Bytecodes.

Ich bin mir nicht sicher, ob die kombinierte Komplexität von JVM + Java zum Bytecode-Compiler wesentlich geringer ist als die Komplexität von vorzeitigen Compilern.

Beachten Sie auch, dass die meisten herkömmlichen Compiler (wie GCC oder Clang / LLVM ) den eingegebenen C-Quellcode (oder C ++ oder Ada, ...) in eine interne Darstellung umwandeln ( Gimple für GCC, LLVM für Clang), die ziemlich ähnlich ist etwas Bytecode. Dann transformieren sie diese internen Repräsentationen (sie optimieren sie zuerst in sich selbst, dh die meisten GCC-Optimierungsläufe nehmen Gimple als Eingabe und erzeugen Gimple als Ausgabe; später geben sie Assembler- oder Maschinencode daraus aus) in Objektcode.

Übrigens, mit der neuesten GCC- (insbesondere libgccjit ) und LLVM-Infrastruktur können Sie sie verwenden, um eine andere (oder Ihre eigene) Sprache in ihre internen Gimple- oder LLVM-Darstellungen zu kompilieren, und dann von den zahlreichen Optimierungsmöglichkeiten des Middle-End und Back-End profitieren. Endteile dieser Compiler.

Basile Starynkevitch
quelle