Warum wurde die virtuelle Java-Maschine so konzipiert, dass sie keine Register zur Speicherung von Zwischenwerten enthält? Stattdessen funktioniert alles auf dem Stack. Gibt es einen besonderen Vorteil einer Stack-basierten Architektur anstelle von Registern?
Antworten
Zu viele Anzeigen?Ich muss den vorherigen Antworten respektvoll widersprechen.
Die Annahme, dass es einen Ausdrucksstapel gibt, ist keine bessere Annahme als die Existenz von Registern. Normalerweise können Registermaschinen keine Stack-Opcodes direkt ausführen, und Stackmaschinen können keine Register-Opcodes direkt ausführen. Sie müssen gemappt werden.
EJP sagt "Wenn der Host-Rechner einen Stack hat, wie sie es alle tun, gibt es nichts zu tun " . Dies ist eine falsche Aussage. Die Behauptung, dass alle Maschinen über Stapel verfügen, die in der Lage sind, Berechnungen auszuführen, zeigt die Verwirrung darüber, was eine Stapelmaschine wirklich ist.
- Eine stapelbasierte Maschine hat einen Befehlssatz mit impliziten Operanden am Anfang eines "Ausdrucksstapels". Kein Allzweck-Stack. Ein Allzweck-Stack macht noch keine Stack-Maschine aus. Sie können Werte speichern und wiederherstellen. Alle Plattformen haben das. Aber sie können keine Berechnungen ausführen.
- Eine registerbasierte Maschine führt typische Opcodes mit expliziten, virtuellen Operanden aus Register, die im Befehlsstrom kodiert sind. Diese VMs haben immer noch Allzweckstapel und Opcodes zum Speichern und Wiederherstellen von Registern. Sie können nicht Karte Stapelberechnungsoperationen auf einen Datenstapel. Die Operanden des Kernbefehlssatzes sind Register, Speicheradressen oder Unmittelbarkeiten (kodiert im Opcode).
Es gibt also sicherlich mehr als "nichts zu tun", um Opcodes einer Maschinenarchitektur auf eine andere zu übertragen. Solange Sie nicht auf einem Chip mit nativer Java-Opcode-Beschleunigung arbeiten, sollten Sie das besser nicht annehmen.
Ich bestreite nicht, dass ein Stapelrechner eine gute Wahl für die Portabilität ist. Ich sage nur, dass es "etwas zu tun" gibt, um Anweisungen von Stack zu Register abzubilden ou registrieren-zu-stapeln. Beides ist möglich.
Vieles davon ist jedoch akademisch. Unter Praxis Im Jahr 2014 ist die vorherrschende Plattform das Register. Selbst stapelbasierte "Prozessoren" sind oft Softchips, die über einem Registerchip implementiert sind. Schauen Sie sich die Java Card 3-Spezifikation und die Implementierungen an, um herauszufinden, welche Prozessoren tatsächlich verwendet werden. Ich lasse das als Übung stehen.
Wenn wir nicht gerade eine VM für eine ganz bestimmte Plattform entwerfen (z.B. wenn wir eine JVM entwerfen sollen, die nur auf einem GreenSpaces-Prozessor läuft, was ich nicht sehe), dann gehe ich davon aus, dass der Kontext allgemeine Anwendungen, eingebettete Anwendungen, Set-Top-Boxen, Firmware-Controller, Spielzeuge und sogar Smart Cards sind. Für all diese Anwendungen sehe ich 8-32-Bit-Prozessoren wie ARM, die entweder verwendet werden oder verfügbar sind. Die gängige JVM ist in C geschrieben. Mit C ist es möglich, einen Interpreter mit virtuellem Stack oder virtuellen Register-Opcodes zu entwickeln. In den 90er Jahren wurde viel über stackbasierte Java-Prozessoren diskutiert, die JVM-Opcodes direkt unterstützen. Im Jahr 2014 sehen wir diese Implementierungen auf registerbasierter Hardware oder als zusätzliche Befehle wie Jazelle (Java-Beschleunigung) auf dem ARM926EJ-S, wo es auch Register und Unterstützung für den ARM-Befehlssatz gibt.
Para allgemeine Anwendung Stack-basierte VMs lassen sich in der Praxis sicherlich nicht auf Stacks abbilden. Diese Maschinen verwenden alle registerbasierte Primärinstruktionen, und die Stack-Ops dienen zum Speichern und Wiederherstellen von Registern oder Call-Stacks, nicht für Berechnungen, Logik oder Verzweigungen. Die Stack-VMs verwenden den nativen Befehlssatz der Maschine, unabhängig davon, ob es sich um einen Stack- oder Register-Befehlssatz handelt. Seit 1980 war praktisch jede neue Prozessorarchitektur ein Register, nach Hennessy und Patterson - "Computerarchitektur - ein quantitativer Ansatz" . Das bedeutet Register-Register, Register-Speicher oder Register-Immediate-Befehlssätze. Auf einem Rechner, der keine stapelbasierte Addierfunktion hat, können Sie keine 2 Werte auf dem Stack addieren. Auf x86 könnten die stapelbasierten Opcodes für eine ADD-Operation folgendermaßen übersetzt werden:
push a
push b
add
zu nativem Registercode:
mov eax, [addra]
mov ebx, [addrb]
add eax, ebx
Zweitens kann ein JIT den Opcode-Stream unabhängig davon, ob es sich um Stack oder Register handelt, in nativen Code kompilieren. Die Wahl des VM-Modells ist also einfach ein Softwaremodell. Registermaschinen sind genauso virtuell. Sie kodieren keine nativen Registerinformationen, die Register in den Opcodes sind virtuelle Symbole.
Als Java entwickelt wurde, dachte man an kleine Opcodes und 8-Bit-Prozessorkompatibilität. Stack-basierte Opcodes sind kleiner als Register-Opcodes. Es machte also Sinn. Ich bin mir sicher, dass ich irgendwo gelesen habe, dass dies neben der Einfachheit einer der Hauptgründe war, warum James Gosling dies für Oak (der ursprüngliche Name von Java) gewählt hat. Ich habe nur keine Referenz zur Hand. In diesem Punkt stimme ich mit Péter Török überein.
- Der Stack-Opcode hat erkennbare Vorteile. Die Codestreams sind oft kleiner / dichter. In Bezug auf die JVM und die CLR ist zu beobachten, dass stapelbasierte Bytecodes 15-20 % kleiner sein können als bei anderen Maschinen. Stack-Bytecodes können leicht in <= 8 Bit kodiert werden. (Forth-Maschinen können bis zu 20 oder mehr Opcodes haben). Der Opstream ist einfacher zu kodieren/dekodieren. Versuchen Sie, einen Assembler oder Disassembler für Java und nicht für x86 zu schreiben.
- Die Registrierung von Opcode hat spürbare Vorteile. Weniger Opcodes zur Codierung eines Ausdrucks = bessere IPC = weniger Verzweigungen auf hoher Ebene im Interpreter. Außerdem kann eine kleine Anzahl von Registern (8 bis 16) direkt auf praktisch jeden modernen Prozessor abgebildet werden. Durch die Verwendung von Registern wird aufgrund der besseren Cache-Lokalität der Referenz ein viel höherer Durchsatz erzielt. Im Gegensatz dazu verbrauchen Stack-Maschinen eine Menge Speicherbandbreite.
In VMs verwenden die Register-Bytecodes oft einen großen virtuellen Registersatz, was größere Bytecodes erfordert. Auf der meisten realen Hardware werden die Register typischerweise mit ca. 3 Bit (Intel x86) bis 5 Bit (Sparc) kodiert, so dass die Dichte von VM zu VM oder CPU zu CPU oder VM zu CPU unterschiedlich sein kann. Dalvik verwendet 4 bis 16 Bits zur Darstellung von Registern, während Parrot 8 Bits pro Register für alle Opcodes verwendet (zumindest das v2 Bytecode-Format, das ich verwendet habe). Dalvik erreicht eine bessere durchschnittliche Dichte. Nach meiner Erfahrung ist es schwierig, eine Allzweck-Registermaschine mit 8-Bit-Bytecode zu bauen, es sei denn, man hält sich strikt an Primitive und verwendet eine kleine Registerdatei. Das mag unintuitiv erscheinen, aber typischerweise hat ein einziger Opcode tatsächlich mehrere Kodierungen mit verschiedenen Registertypen.
Ein letzter Punkt: Ein Großteil der Soft-Core-Optimierung wird durch den Einsatz von JIT möglicherweise zunichte gemacht.
Das Hauptargument, dass sich Stack-Maschinen besser auf Hardware abbilden lassen, ist, dass es ignoriert, wo die JVM läuft und/oder wohin sich die Technologie entwickelt. Abgesehen von Chuck Moore kenne ich niemanden, der stapelbasierte Prozessoren entwickelt (IGNITE und GreenSpaces GA144), und die meisten neuen Entwicklungen sind Registermaschinen. Die Argumente für Stapelprozessoren sind überwiegend akademischer Natur. Für jedes Argument eines 8-Bit-Stapelprozessors kann ich Ihnen mehrere Registermaschinen (Hitachi H8 mit Registern, ARM926 mit Registern, Intel 8051) mit einem C-Compiler zeigen. Es ist wahrscheinlicher, dass Sie auf einem reinen Stack-Prozessor in Forth schreiben, als in Java. Für eine neue Plattform ist es sinnvoller, einen billigen ARM-Prozessor zu verwenden, auf dem es C-Compiler, eine vollständige JVM usw. gibt. Diese arbeiten mit Register-Befehlssätzen.
- ARM926 / ARM926EJ-S - http://www.arm.com/products/processors/classic/arm9/arm926.php
- H8 - http://en.wikipedia.org/wiki/H8_Family
Also, wenn das wahr ist? Spielt das eine Rolle? Meine Meinung, basierend auf meiner Erfahrung, ist "nicht so sehr wie die Leute denken". Denken Sie daran, dass Bytecode nur eine Zwischendarstellung ist. Der rein interpretierte Kern einer Maschine ist oft ein Sprungbrett, eine Brücke, ein ausfallsicherer Kern. Das endgültige Ziel ist die eventuelle Version 2 mit einem JITter für native Leistung. Viele, die das schon ein- oder zweimal gemacht haben, vertreten daher den Standpunkt, dass es sinnvoll ist, den Kern so einfach wie möglich zu halten und zum JIT überzugehen, wobei 90 % der Optimierungen dort vorgenommen werden. Jeder vergeudete Aufwand für das Tuning des interpetierten Kerns könnte als verfrühte Optimierung angesehen werden. Wenn Sie hingegen keinen JITter planen oder JIT aufgrund von Speicherbeschränkungen unpraktisch ist, dann optimieren Sie auf dem virtuellen Kern oder implementieren Sie die VM auf einem Chip.
Java wurde von Grund auf für die Portabilität konzipiert. Aber wie können Sie Ihren Bytecode portabel halten, wenn er davon abhängt, dass bestimmte Register auf der Plattform vorhanden sind, auf der Sie ihn ausführen? Vor allem, wenn man bedenkt, dass er ursprünglich (auch) auf Set-Top-Boxen laufen sollte, die eine ganz andere Prozessorarchitektur haben als herkömmliche PCs.
Erst zur Laufzeit kennt die JVM die verfügbaren Register und andere hardwarespezifische Informationen. Dann kann (und wird) der JIT-Compiler gegebenenfalls für diese optimieren.
Beim Entwurf einer virtuellen Maschine ist es einfach viel einfacher, einen Stack statt einer Reihe von virtuellen Registern zu verwenden. Wenn der Host-Rechner einen Stack hat, wie sie alle, gibt es nichts zu tun, und wenn er Register hat, kann man sie immer noch für andere Dinge verwenden: temporäre Daten usw. Wenn der Host-Rechner jedoch keine Register, sondern nur einen Stack hat und Sie Ihre VM auf der Grundlage von Registern entwerfen, haben Sie ein großes Implementierungsproblem, da Sie den nativen Stack in einer Weise verwenden müssen, die seine Verwendung als virtuellen Stack beeinträchtigt.