it-swarm.com.de

Kompilierung zu Bytecode gegen Maschinencode

Ist die Kompilierung, die einen Zwischenbytecode erzeugt (wie bei Java), im Allgemeinen weniger komplex (und nimmt daher wahrscheinlich weniger Zeit in Anspruch), anstatt "vollständig" zum Maschinencode überzugehen?

13
Julian A.

Ja, das Kompilieren auf Java Bytecode ist einfacher als das Kompilieren auf Maschinencode. Dies liegt teilweise daran, dass nur ein Format als Ziel ausgewählt werden muss (wie Mandrill erwähnt, obwohl dies nur die Komplexität des Compilers und nicht die Kompilierungszeit verringert). Zum Teil, weil die JVM eine viel einfachere Maschine ist und bequemer zu programmieren ist als echte CPUs - da sie zusammen mit der Sprache Java, die meisten Java) Operationen entwickelt wurde Zuordnung zu genau einer Bytecode-Operation auf sehr einfache Weise. Ein weiterer sehr wichtiger Grund ist, dass praktisch nein Optimierung stattfindet. Fast alle Effizienzprobleme bleiben dem JIT-Compiler (oder der JVM als Ganzes) überlassen. Das gesamte mittlere Ende normaler Compiler verschwindet. Grundsätzlich kann es einmal durch die AST] gehen und für jeden Knoten vorgefertigte Bytecode-Sequenzen generieren. Das Generieren von Methodentabellen ist mit einem gewissen "Verwaltungsaufwand" verbunden , konstante Pools usw., aber das ist nichts im Vergleich zu den Komplexitäten von beispielsweise LLVM.

22
user7043

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

Welche allgemeinen Schritte müssen unternommen werden, um ein Programm zu kompilieren?

  1. Scannen, Parsen und Validieren des Quellcodes.
  2. Konvertieren der Quelle in einen abstrakten Syntaxbaum.
  3. Optional: Verarbeiten und verbessern Sie das AST, wenn die Sprachspezifikation dies zulässt (z. B. Entfernen von totem Code, Neuordnen von Vorgängen, andere Optimierungen).
  4. Konvertieren von 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 könnte sich darüber Gedanken machen, ob das Verknüpfen Teil des Kompilierens im Kontext dieser Frage ist. In diesem Fall wäre die Kompilierung von Bytecode etwas einfacher. Die Komplexität der Verknüpfung wird jedoch zur Laufzeit wieder wettgemacht, wenn viele Verknüpfungsprobleme vom VM behandelt werden (siehe meinen Hinweis unten).

  • Bytecode-Compiler neigen dazu, nicht so viel zu optimieren, da VM dies im laufenden Betrieb besser kann (JIT-Compiler sind heutzutage eine ziemlich standardmäßige Ergänzung zu VMs).

Daraus schließe ich, dass Bytecode-Compiler die Komplexität der meisten Optimierungen und der gesamten Verknüpfung weglassen können, indem beide auf die Laufzeit VM verschoben werden. Bytecode-Compiler sind in der Praxis einfacher, da sie viele Komplexitäten auf das VM schaufeln, das Maschinencode-Compiler auf sich nehmen.

1Nicht gezählt esoterische Sprachen

7
user22815

Ich würde sagen, dass dies das Compiler-Design vereinfacht, da die Kompilierung immer Java für generischen Code der virtuellen Maschine) ist. Das bedeutet auch, dass Sie den Code nur einmal kompilieren müssen und er auf jeder Plattform ausgeführt wird (anstelle von Ich bin mir nicht sicher, ob die Kompilierungszeit kürzer sein wird, da Sie die virtuelle Maschine 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" interpretieren kann (dies ist der Code der virtuellen Maschine, der aus Java Code-Kompilierung), übersetzen Sie es in den tatsächlichen Maschinencode und führen Sie es aus.

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

4
Mandrill

Die Komplexität der Kompilierung hängt weitgehend von der semantischen Lücke zwischen der Ausgangssprache und der Zielsprache sowie dem Optimierungsgrad ab, den Sie anwenden möchten, während Sie diese Lücke schließen.

Zum Beispiel ist das Kompilieren von Java Quellcode zu JVM-Bytecode relativ einfach, da es eine Kernuntermenge von Java gibt, die einer Teilmenge ziemlich direkt zugeordnet ist) Es gibt einige Unterschiede: Java hat Schleifen, aber keine GOTO, die JVM hat GOTO, aber keine Schleifen, Java hat Generika, die JVM nicht, aber diese können leicht behandelt werden (die Transformation von Schleifen zu bedingten Sprüngen ist trivial, Typlöschung etwas weniger, aber immer noch beherrschbar). Es gibt andere Unterschiede, aber weniger schwerwiegend.

Das Kompilieren von Ruby Quellcode zu JVM-Bytecode ist viel komplizierter (insbesondere bevor invokedynamic und MethodHandles in Java) eingeführt wurden = 7 oder genauer in der 3. Ausgabe der JVM-Spezifikation) In Ruby können Methoden zur Laufzeit ersetzt werden. In der JVM ist die kleinste Codeeinheit, die zur Laufzeit ersetzt werden kann, eine Klasse, also Ruby Methoden müssen nicht zu JVM-Methoden, sondern zu JVM-Klassen kompiliert werden. Ruby Methodenversand stimmt nicht mit JVM-Methodenversand überein und vor invokedynamic gab es Es gibt keine Möglichkeit, einen eigenen Methoden-Dispatching-Mechanismus in die JVM einzufügen. Ruby verfügt über Fortsetzungen und Coroutinen, aber der JVM fehlen die Möglichkeiten, diese zu implementieren. (Die JVM GOTO ist auf beschränkt Sprungziele innerhalb der Methode.) Das einzige Kontrollfluss-Grundelement der JVM, das leistungsfähig genug wäre, um Fortsetzungen zu implementieren, sind Ausnahmen und das Implementieren von Coroutinen-Threads, die beide extrem schwer sind, während der gesamte Zweck von Koroutinen darin besteht, ve zu sein ry leicht.

OTOH, das Kompilieren von Ruby Quellcode zu Rubinius-Bytecode oder YARV-Bytecode ist wieder trivial, da beide explizit als Kompilierungsziel für Ruby ( 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 zu JVM-Bytecode nicht einfach. Auch hier besteht eine ziemlich große semantische Lücke.

Haskell ist ein weiteres gutes Beispiel: Mit Haskell gibt es mehrere produktionsfertige Hochleistungs-Compiler mit industrieller Stärke, die nativen x86-Maschinencode erzeugen. Bis heute 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. Dies ist also ein Beispiel, bei dem die Kompilierung in nativen Maschinencode tatsächlich weniger komplex ist als die Kompilierung in JVM- oder CIL-Bytecode. Dies liegt daran, dass nativer Maschinencode viel niedrigere Grundelemente (GOTO, Zeiger,…) enthält, die einfacher "gezwungen" werden können, das zu tun, was Sie möchten, als übergeordnete Grundelemente 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 Ausgangssprache übereinstimmen, um die Komplexität des Compilers zu verringern.

3
Jörg W Mittag

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

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 innerhalb 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 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 nicht sicher, ob die kombinierte Komplexität von JVM + Java zum Bytecode-Compiler) erheblich geringer ist als die Komplexität von vorzeitige Compiler.

Beachten Sie auch, dass die meisten herkömmlichen Compiler (wie GCC oder Clang/LLVM ) die Eingabe C (oder C++ oder Ada) transformieren. ...) Quellcode in eine interne Darstellung ( Gimple für GCC, LLVM für Clang), die einem Bytecode ziemlich ähnlich ist . Dann transformieren sie diese internen Darstellungen (zuerst wird sie in sich selbst optimiert, d. H. Die meisten GCC-Optimierungsdurchläufe nehmen Gimple als Eingabe und erzeugen Gimple als Ausgabe; geben später Assembler- oder Maschinencode daraus aus) in Objektcode.

Übrigens, mit der jüngsten 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 vielen Optimierungsmöglichkeiten von zu profitieren die mittleren und hinteren Teile dieser Compiler.