Hier scheint es mindestens zwei verschiedene mögliche Fragen zu geben. Man geht wirklich um Compiler im Allgemeinen, wobei Java im Grunde nur ein Beispiel für das Genre ist. Die andere ist spezifischer für Java als die spezifischen Byte-Codes, die es verwendet.
Compiler im Allgemeinen
Betrachten wir zunächst die allgemeine Frage: Warum sollte ein Compiler beim Kompilieren von Quellcode eine Zwischendarstellung verwenden, um auf einem bestimmten Prozessor ausgeführt zu werden?
Komplexitätsreduzierung
Eine Antwort darauf ist ziemlich einfach: Es wandelt ein O (N * M) -Problem in ein O (N + M) -Problem um.
Wenn wir N Ausgangssprachen und M Ziele haben und jeder Compiler völlig unabhängig ist, brauchen wir N * M Compiler, um alle diese Ausgangssprachen in alle diese Ziele zu übersetzen (wobei ein "Ziel" so etwas wie eine Kombination von a ist Prozessor und Betriebssystem).
Wenn sich jedoch alle diese Compiler auf eine gemeinsame Zwischendarstellung einigen, können wir N Compiler-Frontends haben, die die Ausgangssprachen in die Zwischendarstellung übersetzen, und M Compiler-Backends, die die Zwischendarstellung in etwas übersetzen, das für ein bestimmtes Ziel geeignet ist.
Problemsegmentierung
Besser noch, es unterteilt das Problem in zwei mehr oder weniger exklusive Domänen. Leute, die sich mit Sprachdesign, Parsing und ähnlichen Dingen auskennen / auskennen, können sich auf Compiler-Frontends konzentrieren, während Leute, die sich mit Befehlssätzen, Prozessordesign und ähnlichen Dingen auskennen, sich auf das Backend konzentrieren können.
So haben wir zum Beispiel für LLVM viele Frontends für verschiedene Sprachen. Wir haben auch Backends für viele verschiedene Prozessoren. Ein Sprachtyp kann ein neues Frontend für seine Sprache schreiben und schnell viele Ziele unterstützen. Ein Prozessor-Typ kann ein neues Back-End für sein Ziel schreiben, ohne sich mit Sprachdesign, Parsen usw. zu befassen.
Die Trennung von Compilern in ein Front-End und ein Back-End mit einer Zwischendarstellung für die Kommunikation zwischen beiden ist mit Java nicht original. Es ist schon lange üblich (jedenfalls lange bevor Java hinzukam).
Verteilungsmodelle
Soweit Java diesbezüglich etwas Neues hinzufügte, befand es sich im Verteilungsmodell. Insbesondere wurden Compiler, obwohl sie intern lange Zeit in Front-End- und Back-End-Teile unterteilt waren, in der Regel als einzelnes Produkt vertrieben. Wenn Sie beispielsweise einen Microsoft C-Compiler gekauft haben, hatte dieser intern ein "C1" und ein "C2", die jeweils das Front-End und das Back-End waren. Sie haben jedoch nur "Microsoft C" gekauft, das beide enthielt Stücke (mit einem "Compiler-Treiber", der Operationen zwischen den beiden koordiniert). Obwohl der Compiler zweiteilig aufgebaut war, war es für einen normalen Entwickler, der den Compiler verwendete, nur eine einzige Sache, die vom Quellcode in den Objektcode übersetzt wurde, wobei dazwischen nichts sichtbar war.
Java verteilte stattdessen das Front-End im Java Development Kit und das Back-End in der Java Virtual Machine. Jeder Java-Benutzer hatte ein Compiler-Back-End, um auf das von ihm verwendete System zuzugreifen. Java-Entwickler verteilten Code im Zwischenformat, sodass die JVM beim Laden alles Notwendige tat, um ihn auf ihrem jeweiligen Computer auszuführen.
Präzedenzfälle
Beachten Sie, dass dieses Verteilungsmodell auch nicht ganz neu war. Nur zum Beispiel funktionierte das UCSD-P-System ähnlich: Compiler-Frontends erzeugten P-Code, und jede Kopie des P-Systems enthielt eine virtuelle Maschine, die das tat, was notwendig war, um den P-Code auf diesem bestimmten Ziel 1 auszuführen .
Java-Bytecode
Java byte code ist hinreichend ähnlich zu P-code. Grundsätzlich handelt es sich um Anweisungen für eine relativ einfache Maschine. Diese Maschine soll eine Abstraktion bestehender Maschinen sein, so dass es ziemlich einfach ist, sie schnell auf fast jedes spezifische Ziel zu übersetzen. Die einfache Übersetzung war von Anfang an wichtig, da die ursprüngliche Absicht darin bestand, Bytecodes zu interpretieren, ähnlich wie es P-System getan hatte (und ja, genau so funktionierten die frühen Implementierungen).
Stärken
Java-Bytecode ist für ein Compiler-Front-End einfach zu erstellen. Wenn Sie (zum Beispiel) einen ziemlich typischen Baum haben, der einen Ausdruck darstellt, ist es normalerweise ziemlich einfach, den Baum zu durchlaufen und Code ziemlich direkt von dem zu generieren, was Sie an jedem Knoten finden.
Java-Bytecodes sind recht kompakt - in den meisten Fällen viel kompakter als der Quellcode oder der Maschinencode für die meisten typischen Prozessoren (und insbesondere für die meisten RISC-Prozessoren, wie den SPARC, den Sun bei der Entwicklung von Java verkauft hat). Dies war zu dieser Zeit besonders wichtig, da Java vor allem Applets unterstützen wollte - Code, der in Webseiten eingebettet war, die vor der Ausführung heruntergeladen wurden - zu einer Zeit, als die meisten Leute um ca. 28.8 Uhr über Modems über Telefonleitungen auf das we zugegriffen haben Kilobit pro Sekunde (obwohl es natürlich immer noch einige Leute gab, die ältere, langsamere Modems verwendeten).
Schwächen
Die größte Schwäche von Java-Bytecodes besteht darin, dass sie nicht besonders aussagekräftig sind. Obwohl sie die in Java vorhandenen Konzepte ziemlich gut ausdrücken können, funktionieren sie nicht annähernd so gut, um Konzepte auszudrücken, die nicht Teil von Java sind. Während es auf den meisten Computern einfach ist, Byte-Codes auszuführen, ist dies auf eine Weise, die die Vorteile eines bestimmten Computers voll ausnutzt, viel schwieriger.
Wenn Sie beispielsweise Java-Bytecodes wirklich optimieren möchten, müssen Sie im Grunde ein Reverse Engineering durchführen, um sie von einer maschinencodeähnlichen Darstellung rückwärts zu übersetzen und sie wieder in SSA-Anweisungen (oder etwas Ähnliches) umzuwandeln . Sie manipulieren dann die SSA-Anweisungen, um Ihre Optimierung durchzuführen, und übersetzen von dort in etwas, das auf die Architektur abzielt, die Ihnen wirklich am Herzen liegt. Selbst bei diesem ziemlich komplexen Prozess sind einige Java-fremde Konzepte so schwierig auszudrücken, dass es schwierig ist, aus einigen Quellensprachen in Maschinencode zu übersetzen, der auf den meisten typischen Maschinen (sogar nahezu) optimal ausgeführt wird.
Zusammenfassung
Wenn Sie nach dem Grund für die Verwendung von Zwischendarstellungen im Allgemeinen fragen, sind zwei Hauptfaktoren:
- Reduzieren Sie ein O (N * M) -Problem auf ein O (N + M) -Problem, und
- Teilen Sie das Problem in handlichere Teile auf.
Wenn Sie nach den Besonderheiten der Java-Bytecodes fragen und wissen, warum diese bestimmte Darstellung anstelle einer anderen gewählt wurde, würde ich sagen, dass die Antwort weitgehend auf ihre ursprüngliche Absicht und die damaligen Einschränkungen des Webs zurückgeht , was zu folgenden Prioritäten führt:
- Kompakte Darstellung.
- Einfach und schnell zu dekodieren und auszuführen.
- Schnell und einfach auf den meisten gängigen Maschinen zu implementieren.
In der Lage zu sein, viele Sprachen zu repräsentieren oder eine Vielzahl von Zielen optimal zu erfüllen, waren viel niedrigere Prioritäten (wenn sie überhaupt als Prioritäten angesehen wurden).
- Warum wird das P-System so oft vergessen? Meist eine Preissituation. Das P-System verkaufte sich recht gut für Apple II, Commodore SuperPets usw. Als der IBM-PC herauskam, war das P-System ein unterstütztes Betriebssystem, aber MS-DOS kostete weniger (aus der Sicht der meisten Leute war es im Wesentlichen kostenlos) und Hatte schnell mehr Programme zur Verfügung, da es das ist, wofür Microsoft und IBM (unter anderem) geschrieben haben.
- So funktioniert beispielsweise Ruß .