Dieser Artikel ist der zweite Artikel in der Reihe zur JVM-Leistungsoptimierung (der erste Artikel: Portal), und der Java-Compiler wird der Kerninhalt dieses Artikels sein.
In diesem Artikel stellt die Autorin (Eva Andreasson) zunächst verschiedene Compilertypen vor und vergleicht die Laufleistung der clientseitigen Kompilierung, der serverseitigen Kompilierung und der mehrschichtigen Kompilierung. Am Ende des Artikels werden dann mehrere gängige JVM-Optimierungsmethoden vorgestellt, z. B. die Eliminierung von totem Code, die Code-Einbettung und die Optimierung des Schleifenkörpers.
Javas stolzeste Funktion, die „Plattformunabhängigkeit“, stammt vom Java-Compiler. Softwareentwickler tun ihr Bestes, um die bestmöglichen Java-Anwendungen zu schreiben, und ein Compiler läuft hinter den Kulissen, um effizienten ausführbaren Code basierend auf der Zielplattform zu erstellen. Unterschiedliche Compiler eignen sich für unterschiedliche Anwendungsanforderungen und führen somit zu unterschiedlichen Optimierungsergebnissen. Wenn Sie also die Funktionsweise von Compilern besser verstehen und mehr Compilertypen kennen, können Sie Ihr Java-Programm besser optimieren.
In diesem Artikel werden die Unterschiede zwischen den verschiedenen Java Virtual Machine-Compilern hervorgehoben und erläutert. Gleichzeitig werde ich auch einige Optimierungslösungen diskutieren, die häufig von Just-in-Time-Compilern (JIT) verwendet werden.
Was ist ein Compiler?
Einfach ausgedrückt: Ein Compiler verwendet ein Programmiersprachenprogramm als Eingabe und ein anderes ausführbares Sprachprogramm als Ausgabe. Javac ist der am häufigsten verwendete Compiler. Es existiert in allen JDKs. Javac nimmt Java-Code als Ausgabe und konvertiert ihn in ausführbaren JVM-Code – Bytecode. Diese Bytecodes werden in Dateien mit der Endung .class gespeichert und beim Start des Java-Programms in die Java-Laufzeitumgebung geladen.
Der Bytecode kann nicht direkt von der CPU gelesen werden. Er muss außerdem in eine Maschinenbefehlssprache übersetzt werden, die die aktuelle Plattform verstehen kann. In der JVM gibt es einen weiteren Compiler, der für die Übersetzung des Bytecodes in Anweisungen verantwortlich ist, die von der Zielplattform ausführbar sind. Einige JVM-Compiler erfordern mehrere Ebenen von Bytecode-Codestufen. Beispielsweise muss ein Compiler möglicherweise mehrere verschiedene Zwischenstufen durchlaufen, bevor er Bytecode in Maschinenanweisungen übersetzt.
Aus plattformunabhängiger Sicht möchten wir, dass unser Code so plattformunabhängig wie möglich ist.
Um dies zu erreichen, arbeiten wir auf der letzten Ebene der Übersetzung – von der Darstellung des niedrigsten Bytecodes bis zum echten Maschinencode –, die den ausführbaren Code wirklich an die Architektur einer bestimmten Plattform bindet. Auf der höchsten Ebene können wir Compiler in statische Compiler und dynamische Compiler unterteilen. Wir können den geeigneten Compiler basierend auf unserer Zielausführungsumgebung, den gewünschten Optimierungsergebnissen und den Ressourcenbeschränkungen, die wir erfüllen müssen, auswählen. Im vorherigen Artikel haben wir statische Compiler und dynamische Compiler kurz besprochen und in den folgenden Abschnitten werden wir sie ausführlicher erläutern.
Statische Kompilierung vs. dynamische Kompilierung
Das zuvor erwähnte Javac ist ein Beispiel für eine statische Kompilierung. Bei einem statischen Compiler wird der Eingabecode einmal interpretiert und die Ausgabe ist die Form, in der das Programm in Zukunft ausgeführt wird. Sofern Sie den Quellcode nicht aktualisieren und neu kompilieren (über den Compiler), wird sich das Ausführungsergebnis des Programms nie ändern: Dies liegt daran, dass es sich bei der Eingabe um eine statische Eingabe und beim Compiler um einen statischen Compiler handelt.
Bei statischer Kompilierung das folgende Programm:
Kopieren Sie den Codecode wie folgt:
staticint add7(int x ){ return x+7;}
wird in einen Bytecode ähnlich dem folgenden umgewandelt:
Kopieren Sie den Codecode wie folgt:
iload0 bipush 7 iadd ireturn
Ein dynamischer Compiler kompiliert dynamisch eine Sprache in eine andere. Die sogenannte Dynamik bezieht sich auf das Kompilieren während der Ausführung des Programms! Der Vorteil der dynamischen Kompilierung und Optimierung besteht darin, dass einige Änderungen beim Laden der Anwendung verarbeitet werden können. Die Java-Laufzeit läuft oft in unvorhersehbaren oder sich sogar ändernden Umgebungen, daher eignet sich die dynamische Kompilierung sehr gut für die Java-Laufzeit. Die meisten JVMs verwenden dynamische Compiler, beispielsweise JIT-Compiler. Es ist zu beachten, dass die dynamische Kompilierung und Codeoptimierung die Verwendung einiger zusätzlicher Datenstrukturen, Threads und CPU-Ressourcen erfordert. Je fortgeschrittener der Optimierer oder Bytecode-Kontextanalysator ist, desto mehr Ressourcen verbraucht er. Diese Kosten sind jedoch im Vergleich zu den erheblichen Leistungsverbesserungen vernachlässigbar.
JVM-Typen und Plattformunabhängigkeit von Java
Ein gemeinsames Merkmal aller JVM-Implementierungen ist die Kompilierung von Bytecode in Maschinenanweisungen. Einige JVMs interpretieren den Code beim Laden der Anwendung und verwenden Leistungsindikatoren, um „heißen“ Code zu finden; andere tun dies durch Kompilierung. Das Hauptproblem bei der Kompilierung besteht darin, dass die Zentralisierung viele Ressourcen erfordert, aber auch zu besseren Leistungsoptimierungen führt.
Wenn Sie Java-Neuling sind, werden Sie die Feinheiten der JVM definitiv verwirren. Aber die gute Nachricht ist, dass Sie es nicht herausfinden müssen! Die JVM verwaltet die Kompilierung und Optimierung des Codes, und Sie müssen sich keine Gedanken über Maschinenanweisungen und darüber machen, wie der Code so geschrieben wird, dass er am besten zur Architektur der Plattform passt, auf der das Programm ausgeführt wird.
Vom Java-Bytecode zur ausführbaren Datei
Sobald Ihr Java-Code in Bytecode kompiliert ist, besteht der nächste Schritt darin, die Bytecode-Anweisungen in Maschinencode zu übersetzen. Dieser Schritt kann über einen Interpreter oder einen Compiler implementiert werden.
erklären
Interpretation ist die einfachste Möglichkeit, Bytecode zu kompilieren. Der Interpreter findet die Hardware-Anweisung, die jeder Bytecode-Anweisung entspricht, in Form einer Nachschlagetabelle und sendet sie dann zur Ausführung an die CPU.
Sie können sich den Interpreter wie ein Wörterbuch vorstellen: Für jedes spezifische Wort (Bytecode-Anweisung) gibt es eine entsprechende spezifische Übersetzung (Maschinencode-Anweisung). Da der Interpreter eine Anweisung jedes Mal sofort ausführt, wenn er sie liest, kann diese Methode einen Befehlssatz nicht optimieren. Gleichzeitig muss ein Bytecode bei jedem Aufruf sofort interpretiert werden, sodass der Interpreter sehr langsam läuft. Der Interpreter führt Code sehr genau aus, aber da der Ausgabebefehlssatz nicht optimiert ist, liefert er möglicherweise keine optimalen Ergebnisse für den Prozessor der Zielplattform.
kompilieren
Der Compiler lädt den gesamten auszuführenden Code in die Laufzeit. Auf diese Weise kann es bei der Übersetzung des Bytecodes auf den gesamten oder einen Teil des Laufzeitkontexts verweisen. Die getroffenen Entscheidungen basieren auf den Ergebnissen der Code-Graph-Analyse. Vergleichen Sie beispielsweise verschiedene Ausführungszweige und referenzieren Sie Laufzeitkontextdaten.
Nachdem die Bytecode-Sequenz in einen Maschinencode-Befehlssatz übersetzt wurde, kann eine Optimierung basierend auf diesem Maschinencode-Befehlssatz durchgeführt werden. Der optimierte Befehlssatz wird in einer Struktur namens Codepuffer gespeichert. Wenn diese Bytecodes erneut ausgeführt werden, kann der optimierte Code direkt aus diesem Codepuffer abgerufen und ausgeführt werden. In einigen Fällen verwendet der Compiler nicht den Optimierer, um den Code zu optimieren, sondern eine neue Optimierungssequenz – „Leistungszählung“.
Der Vorteil der Verwendung eines Code-Cache besteht darin, dass die Ergebnismengenanweisungen sofort ausgeführt werden können, ohne dass eine Neuinterpretation oder Kompilierung erforderlich ist!
Dies kann die Ausführungszeit erheblich verkürzen, insbesondere bei Java-Anwendungen, bei denen eine Methode mehrmals aufgerufen wird.
Optimierung
Mit der Einführung der dynamischen Kompilierung haben wir die Möglichkeit, Leistungsindikatoren einzufügen. Beispielsweise fügt der Compiler einen Leistungsindikator ein, der jedes Mal erhöht wird, wenn ein Bytecodeblock (entsprechend einer bestimmten Methode) aufgerufen wird. Der Compiler verwendet diese Zähler, um „Hot Blocks“ zu finden, sodass er bestimmen kann, welche Codeblöcke optimiert werden können, um die größte Leistungsverbesserung für die Anwendung zu erzielen. Daten zur Laufzeitleistungsanalyse können dem Compiler helfen, im Online-Zustand mehr Optimierungsentscheidungen zu treffen und so die Effizienz der Codeausführung weiter zu verbessern. Da wir immer genauere Daten zur Codeleistungsanalyse erhalten, können wir mehr Optimierungspunkte finden und bessere Optimierungsentscheidungen treffen, z. B. wie Befehle besser sequenziert werden und ob ein effizienterer Befehlssatz verwendet werden soll ob redundante Vorgänge usw. beseitigt werden sollen.
Zum Beispiel
Betrachten Sie den folgenden Java-Code. Kopieren Sie den Code. Der Code lautet wie folgt:
staticint add7(int x ){ return x+7;}
Javac übersetzt es statisch in den folgenden Bytecode:
Kopieren Sie den Codecode wie folgt:
iload0
Bipush 7
iadd
ichkehre zurück
Wenn diese Methode aufgerufen wird, wird der Bytecode dynamisch in Maschinenanweisungen kompiliert. Die Methode kann optimiert werden, wenn der Leistungsindikator (sofern vorhanden) einen angegebenen Schwellenwert erreicht. Die optimierten Ergebnisse könnten wie der folgende Maschinenbefehlssatz aussehen:
Kopieren Sie den Codecode wie folgt:
lea rax,[rdx+7] ret
Für unterschiedliche Anwendungen eignen sich unterschiedliche Compiler
Unterschiedliche Anwendungen haben unterschiedliche Anforderungen. Serverseitige Anwendungen in Unternehmen müssen in der Regel über einen längeren Zeitraum ausgeführt werden, sodass sie in der Regel eine stärkere Leistungsoptimierung wünschen, während clientseitige Applets möglicherweise schnellere Reaktionszeiten und einen geringeren Ressourcenverbrauch wünschen. Lassen Sie uns drei verschiedene Compiler und ihre Vor- und Nachteile besprechen.
Clientseitige Compiler
C1 ist ein bekannter optimierender Compiler. Fügen Sie beim Starten der JVM den Parameter -client hinzu, um den Compiler zu starten. Anhand seines Namens können wir erkennen, dass C1 ein Client-Compiler ist. Es ist ideal für Client-Anwendungen, die über wenige verfügbare Systemressourcen verfügen oder einen schnellen Start erfordern. C1 führt die Codeoptimierung mithilfe von Leistungsindikatoren durch. Dies ist eine einfache Optimierungsmethode mit weniger Eingriffen in den Quellcode.
Serverseitige Compiler
Für Anwendungen mit langer Laufzeit (z. B. serverseitige Unternehmensanwendungen) reicht die Verwendung eines clientseitigen Compilers möglicherweise nicht aus. Zu diesem Zeitpunkt sollten wir einen serverseitigen Compiler wie C2 wählen. Der Optimierer kann durch Hinzufügen von server zur JVM-Startzeile gestartet werden. Da die meisten serverseitigen Anwendungen in der Regel eine lange Laufzeit haben, können Sie mit dem C2-Compiler mehr Daten zur Leistungsoptimierung sammeln als mit kurz laufenden, schlanken clientseitigen Anwendungen. Daher können Sie auch fortgeschrittenere Optimierungstechniken und -algorithmen anwenden.
Tipp: Machen Sie Ihren serverseitigen Compiler warm
Bei serverseitigen Bereitstellungen kann es einige Zeit dauern, bis der Compiler diese „heißen“ Codes optimiert. Daher erfordert die serverseitige Bereitstellung häufig eine „Aufwärmphase“. Stellen Sie daher bei Leistungsmessungen bei serverseitigen Bereitstellungen immer sicher, dass Ihre Anwendung einen stabilen Zustand erreicht hat! Wenn Sie dem Compiler genügend Zeit zum Kompilieren geben, bringt das viele Vorteile für Ihre Anwendung.
Der serverseitige Compiler kann mehr Leistungsoptimierungsdaten erhalten als der clientseitige Compiler, sodass er komplexere Verzweigungsanalysen durchführen und Optimierungspfade mit besserer Leistung finden kann. Je mehr Leistungsanalysedaten Sie haben, desto besser sind die Ergebnisse Ihrer Anwendungsanalyse. Natürlich erfordert die Durchführung einer umfassenden Leistungsanalyse mehr Compiler-Ressourcen. Wenn die JVM beispielsweise den C2-Compiler verwendet, muss sie mehr CPU-Zyklen, einen größeren Code-Cache usw. verwenden.
Kompilierung auf mehreren Ebenen
Die mehrstufige Kompilierung mischt clientseitige Kompilierung und serverseitige Kompilierung. Azul war der erste, der eine mehrschichtige Kompilierung in seiner Zing-JVM implementierte. Kürzlich wurde diese Technologie von Oracle Java Hotspot JVM (nach Java SE7) übernommen. Die mehrstufige Kompilierung vereint die Vorteile clientseitiger und serverseitiger Compiler. Der Client-Compiler ist in zwei Situationen aktiv: beim Start der Anwendung und wenn Leistungsindikatoren niedrigere Schwellenwerte erreichen, um Leistungsoptimierungen durchzuführen. Der Client-Compiler fügt außerdem Leistungsindikatoren ein und bereitet den Befehlssatz für die spätere Verwendung durch den serverseitigen Compiler zur erweiterten Optimierung vor. Die mehrschichtige Kompilierung ist eine Leistungsanalysemethode mit hoher Ressourcenauslastung. Da Daten während Compileraktivitäten mit geringer Auswirkung erfasst werden, können diese Daten später in fortgeschritteneren Optimierungen verwendet werden. Dieser Ansatz liefert mehr Informationen als die Analyse von Zählern mithilfe von Interpretationscode.
Abbildung 1 beschreibt den Leistungsvergleich von Interpretern, clientseitiger Kompilierung, serverseitiger Kompilierung und mehrschichtiger Kompilierung. Die X-Achse ist die Ausführungszeit (Zeiteinheit) und die Y-Achse ist die Leistung (Anzahl der Vorgänge pro Zeiteinheit).
Abbildung 1. Compiler-Leistungsvergleich
Im Vergleich zu rein interpretiertem Code kann die Verwendung eines clientseitigen Compilers etwa fünf- bis zehnfache Leistungsverbesserungen bewirken. Der Umfang der Leistungssteigerung, die Sie erzielen, hängt von der Effizienz des Compilers, den verfügbaren Optimierertypen und davon ab, wie gut das Design der Anwendung zur Zielplattform passt. Letzteres kann für Programmentwickler jedoch oft vernachlässigt werden.
Im Vergleich zu clientseitigen Compilern können serverseitige Compiler häufig eine Leistungsverbesserung von 30 bis 50 % bewirken. In den meisten Fällen gehen Leistungsverbesserungen oft auf Kosten des Ressourcenverbrauchs.
Die mehrstufige Kompilierung vereint die Vorteile beider Compiler. Die clientseitige Kompilierung hat eine kürzere Startzeit und kann eine schnelle Optimierung durchführen; die serverseitige Kompilierung kann während des nachfolgenden Ausführungsprozesses erweiterte Optimierungsvorgänge durchführen.
Einige gängige Compiler-Optimierungen
Bisher haben wir besprochen, was es bedeutet, Code zu optimieren und wie und wann die JVM die Codeoptimierung durchführt. Als Nächstes beende ich diesen Artikel mit der Vorstellung einiger Optimierungsmethoden, die tatsächlich von Compilern verwendet werden. Die JVM-Optimierung erfolgt tatsächlich auf der Bytecode-Stufe (oder der Darstellungsstufe einer niedrigeren Sprache), hier wird jedoch die Java-Sprache verwendet, um diese Optimierungsmethoden zu veranschaulichen. Es ist natürlich unmöglich, alle JVM-Optimierungsmethoden in diesem Abschnitt abzudecken. Ich hoffe jedoch, dass diese Einführungen Sie dazu inspirieren, Hunderte fortgeschrittenere Optimierungsmethoden zu erlernen und Innovationen in der Compiler-Technologie einzuführen.
Eliminierung von totem Code
Bei der Eliminierung von totem Code geht es, wie der Name schon sagt, um die Eliminierung von Code, der niemals ausgeführt wird – also „toten“ Code.
Wenn der Compiler während des Betriebs einige redundante Anweisungen findet, entfernt er diese Anweisungen aus dem Ausführungsbefehlssatz. Beispielsweise wird in Listing 1 eine der Variablen nach einer Zuweisung nie mehr verwendet, sodass die Zuweisungsanweisung während der Ausführung vollständig ignoriert werden kann. Entsprechend der Operation auf Bytecode-Ebene muss der Variablenwert nie in das Register geladen werden. Nicht laden zu müssen bedeutet, dass weniger CPU-Zeit verbraucht wird, wodurch die Codeausführung beschleunigt wird, was letztendlich zu einer schnelleren Anwendung führt. Wenn der Ladecode mehrmals pro Sekunde aufgerufen wird, ist der Optimierungseffekt deutlicher.
Listing 1 verwendet Java-Code, um ein Beispiel für die Zuweisung eines Werts zu einer Variablen zu veranschaulichen, die niemals verwendet wird.
Listing 1. Der Code zum Kopieren toter Codes lautet wie folgt:
int timeToScaleMyApp(boolean unlimitedOfResources){
int reArchitect =24;
int patchByClustering =15;
int useZing =2;
if(endlessOfResources)
return reArchitect + useZing;
anders
return useZing;
}
Wenn während der Bytecode-Phase eine Variable geladen, aber nie verwendet wird, kann der Compiler den toten Code erkennen und entfernen, wie in Listing 2 gezeigt. Wenn Sie diesen Ladevorgang nie durchführen, können Sie CPU-Zeit sparen und die Ausführungsgeschwindigkeit des Programms verbessern.
Listing 2. Der optimierte Code-Kopiercode lautet wie folgt:
int timeToScaleMyApp(boolean unlimitedOfResources){
int reArchitect =24; //unnötige Operation hier entfernt…
int useZing =2;
if(endlessOfResources)
return reArchitect + useZing;
anders
return useZing;
}
Redundanzbeseitigung ist eine Optimierungsmethode, die die Anwendungsleistung durch Entfernen doppelter Anweisungen verbessert.
Bei vielen Optimierungen wird versucht, Sprungbefehle auf Maschinenbefehlsebene zu eliminieren (z. B. JMP in der x86-Architektur). Sprungbefehle ändern das Befehlszeigerregister und lenken so den Programmausführungsfluss um. Diese Sprunganweisung ist im Vergleich zu anderen ASSEMBLY-Anweisungen ein sehr ressourcenintensiver Befehl. Deshalb wollen wir diese Art von Unterricht reduzieren oder ganz eliminieren. Code-Einbettung ist eine sehr praktische und bekannte Optimierungsmethode zur Eliminierung von Übertragungsanweisungen. Da die Ausführung von Sprunganweisungen teuer ist, bringt die Einbettung einiger häufig aufgerufener kleiner Methoden in den Funktionskörper viele Vorteile. Listing 3-5 zeigt die Vorteile der Einbettung.
Listing 3. Code zum Kopieren der Aufrufmethode Der Code lautet wie folgt:
int whenToEvaluateZing(int y){ return daysLeft(y)+ daysLeft(0)+ daysLeft(y+1);}
Listing 4. Der Code zum Kopieren der aufgerufenen Methode lautet wie folgt:
int daysLeft(int x){ if(x ==0) return0; else return x -1;}
Listing 5. Der Code zum Kopieren der Inline-Methode lautet wie folgt:
int whenToEvaluateZing(int y){
int temp =0;
if(y==0)
Temperatur +=0;
anders
temp += y -1;
if(0==0)
Temperatur +=0;
anders
Temperatur +=0-1;
if(y+1==0)
Temperatur +=0;
anders
temp +=(y +1)-1;
Rücklauftemperatur;
}
In Listing 3-5 können wir sehen, dass eine kleine Methode dreimal in einem anderen Methodenkörper aufgerufen wird. Was wir veranschaulichen möchten, ist: Die Kosten für die direkte Einbettung der aufgerufenen Methode in den Code sind geringer als für die Ausführung von drei Sprüngen Anweisungen übertragen.
Das Einbetten einer Methode, die nicht oft aufgerufen wird, macht vielleicht keinen großen Unterschied, aber das Einbetten einer sogenannten „heißen“ Methode (eine Methode, die oft aufgerufen wird) kann viele Leistungsverbesserungen bringen. Der eingebettete Code kann oft noch weiter optimiert werden, wie in Listing 6 gezeigt.
Listing 6. Nachdem der Code eingebettet wurde, kann eine weitere Optimierung erreicht werden, indem der Code wie folgt kopiert wird:
int whenToEvaluateZing(int y){ if(y ==0)return y; elseif(y ==-1)return y -1;}
Schleifenoptimierung
Die Schleifenoptimierung spielt eine wichtige Rolle bei der Reduzierung der zusätzlichen Kosten für die Ausführung des Schleifenkörpers. Die zusätzlichen Kosten beziehen sich hier auf teure Sprünge, viele Zustandsprüfungen und nicht optimierte Pipelines (d. h. eine Reihe von Befehlssätzen, die keine tatsächlichen Vorgänge ausführen und zusätzliche CPU-Zyklen verbrauchen). Es gibt viele Arten von Schleifenoptimierungen. Hier sind einige der beliebtesten Schleifenoptimierungen:
Zusammenführen von Schleifenkörpern: Wenn zwei benachbarte Schleifenkörper die gleiche Anzahl von Schleifen ausführen, versucht der Compiler, die beiden Schleifenkörper zusammenzuführen. Wenn zwei Schleifenkörper völlig unabhängig voneinander sind, können sie auch gleichzeitig (parallel) ausgeführt werden.
Inversionsschleife: Im einfachsten Fall ersetzen Sie eine While-Schleife durch eine Do-While-Schleife. Diese do-while-Schleife wird in eine if-Anweisung eingefügt. Durch diesen Ersatz werden zwei Sprungoperationen reduziert, aber die bedingte Beurteilung erhöht, wodurch sich die Codemenge erhöht. Diese Art der Optimierung ist ein großartiges Beispiel für den Handel mit mehr Ressourcen gegen effizienteren Code – der Compiler wägt Kosten und Nutzen ab und trifft Entscheidungen dynamisch zur Laufzeit.
Schleifenkörper neu organisieren: Organisieren Sie den Schleifenkörper neu, sodass der gesamte Schleifenkörper im Cache gespeichert werden kann.
Erweitern Sie den Schleifenkörper: Reduzieren Sie die Anzahl der Schleifenbedingungsprüfungen und -sprünge. Sie können sich das so vorstellen, als würden Sie mehrere Iterationen „inline“ ausführen, ohne eine bedingte Prüfung durchführen zu müssen. Das Abrollen des Schleifenkörpers birgt auch gewisse Risiken, da es durch Auswirkungen auf die Pipeline und eine große Anzahl redundanter Befehlsabrufe die Leistung beeinträchtigen kann. Auch hier ist es Sache des Compilers, zu entscheiden, ob der Schleifenkörper zur Laufzeit abgerollt wird, und es lohnt sich, ihn abzurollen, wenn dadurch eine größere Leistungsverbesserung erzielt wird.
Das Obige ist ein Überblick darüber, wie Compiler auf der Bytecode-Ebene (oder einer niedrigeren Ebene) die Leistung von Anwendungen auf der Zielplattform verbessern können. Was wir besprochen haben, sind einige gängige und beliebte Optimierungsmethoden. Aus Platzgründen geben wir nur einige einfache Beispiele. Unser Ziel ist es, durch die obige einfache Diskussion Ihr Interesse an einer eingehenden Untersuchung der Optimierung zu wecken.
Fazit: Reflexionspunkte und Schlüsselpunkte
Wählen Sie je nach Zweck unterschiedliche Compiler.
1. Ein Interpreter ist die einfachste Form der Übersetzung von Bytecode in Maschinenanweisungen. Seine Implementierung basiert auf einer Anweisungs-Nachschlagetabelle.
2. Der Compiler kann basierend auf Leistungsindikatoren optimieren, erfordert jedoch den Verbrauch einiger zusätzlicher Ressourcen (Code-Cache, Optimierungsthread usw.).
3. Der Client-Compiler kann im Vergleich zum Interpreter eine 5- bis 10-fache Leistungssteigerung bringen.
4. Der serverseitige Compiler kann im Vergleich zum clientseitigen Compiler eine Leistungssteigerung von 30 bis 50 % bewirken, erfordert jedoch mehr Ressourcen.
5. Die mehrschichtige Kompilierung vereint die Vorteile beider. Verwenden Sie die clientseitige Kompilierung für schnellere Antwortzeiten und verwenden Sie dann den serverseitigen Compiler, um häufig aufgerufenen Code zu optimieren.
Hier gibt es viele Möglichkeiten, den Code zu optimieren. Eine wichtige Aufgabe des Compilers besteht darin, alle möglichen Optimierungsmethoden zu analysieren und dann die Kosten verschiedener Optimierungsmethoden gegen die Leistungsverbesserung abzuwägen, die durch die endgültigen Maschinenanweisungen erzielt wird.