Java Prozessoren Referat Labor Computertechnik WS 2004 Bernd Reiners Mt.Nr.: 496 21 62 21 Inhalt Einführung 2 Realisierungsmöglichkeiten 2 Einsatzgebiete 3 Vergleich: Java Virtual Machine – Java Prozessor 3 Übersicht: Java Prozessoren Context 4 Pico Java 4 Überblick 5 Speicher der Laufzeit Umgebung 6 Mikroarchitektur 6 Pipeline 8 der Befehlssatz 9 Befehlsfomat 9 Funktionale Gruppierung 11 erweiterte Bytecodes 12 Komplexität Handhaben - Gruppierung nach Komplexität 14 Stackorganisation 16 Instruction Folding 19 Hardwareunterstützung der Laufzeitanforderungen 21 Garbage Collection 22 Mark & Sweep (markieren und rausschmeissen) Handles 22 23 Generational Garbage Collection 25 Write Barriers 27 Benchmark 27 Zusammenfassung 28 Quellen 30 Seite 1/ 31 Einführung Es gibt zwei Möglichkeiten Java direkt auszuführen: Die spezielle Softwareimplementierung einer virtuellen Javamaschine (JVM) für Mikrocontroller oder eine Implementierung des JavaStandards direkt in Hardware. Hier soll die zweite Möglichkeit näher betrachtet werden. Java Prozessoren sind auf die direkte Verarbeitung von Java Bytecode optimierte CPU's. Sie können den Bytecode direkt auf der Hardware ausführen, es kann auf das langwierige Umsetzen des Bytecodes in native, dem Prozessor angepasste Befehle verzichtet werden wodurch die langen Ausführungszeiten von softwarebasierten VMs entfallen Java Prozessoren könnten als in Selikon gegossene Java Virtual Machines bezeichet werden. Realisierungsmöglichkeiten Die direkte Ausführung von Bytecode in Hardware kann auf verschiedene Weise realisiert werden. der Java Beschleuniger nutzt einen 'Koprozessor', der zusätzlich zum Prozessor aktivierbar ist. Dieser nimmt der CPU die Verarbeitung von Java Bytecode ab. Er unterstützt die (allen JVMs gemeinsame) Stackarchitektur. Ein Beispiel hierfür ist Nazomis Java Koprozessor JA108 (4k -2fach assoziativer BytecodeCache, 2K DataCache etc). zusätzlicher Befehlsatz bei diesem Konzept unterstützt die CPU zusätzlich einen (Java-VM) Befehlssatz, wodurch durch einfaches umschalten die CPU zu einer Java Virtual Machine wird. Ein Beispielt hierfür ist ARM's Jazelle Technologie, die in den ARMxxJx Prozessoren zum Einsatz kommt. 'reine' Java Prozessoren diese sollen nun näher untersucht werden Seite 2/ 31 Einsatzgebiete Der Einsatz von Java-Prozessoren ist in vielen Bereichen möglich. Insbesondere eignen sie sich in ressourcenkritischen Umgebungen wie z.B: in embedded Systems, Mikrokontroller, in in denen die Java Features genutzt werden sollen. - SmartCards - Mobiltelefone - pagers - Drucker - SetTop Boxen - Navigations Systeme - PDA‘s - IP-Phones, IP-TV, etc Die meisten dieser Systeme arbeiten mit weniger als 100MHz Taktfrequenz und 2 MB Speicher, wodurch Leistungsverbrauch und Hauptspeicherverwendung schnell zu Knock-out Kriterien bei Java Prozessoren werden. Vergleich: Java Virtual Machine – Java Prozessor Eine Java Virtual Machine bekommt den, von einem Java Compiler erzeugten Bytecode übergeben. Dieser ist hardwareneutral, für die VM geschrieben, welche dem Compiler gegenüber einen Prozessor emuliert. Die VM wandelt den erhaltenen Code in native Instruktionen des jeweiligen Prozessors fü den sie gebaut ist. Die VM bietet auf jedem System eine immer gleiche Schnittstelle für den Compiler, eben eine abstrakte, eine virtuelle Maschine (write once, run anywhere). Ein Java Prozessor bekommt (genau wie die JVM) eine class Datei mit Bytecode als Input. Er führt diesen gemäß der JVM Spezifiaktion aus - muss nach aussen genau wie die JVM funktionieren, ist jedoch um Funktionen zur Hardwareverwaltung und zur Ausführung nativer Anweisungen, z.B: in C erweitert. Wie bereits angedeutet ist der Java Prozessor eine Hardware Java 'Virtual' Maschine Seite 3/ 31 Übersicht: Java Prozessoren Context (Sun’s) Java Prozessor Context besteht neben der Spezifikation –picoJava- aus zwei Mikroprozessorlinien – microJava und ultraJava: picoJava: picoJava (picoJava Core Specification ) ist die Spezifikation für einen minimalen Java Prozessor. Die Java Prozessor Linien von Sun basieren auf dieser Spezifikation. Und da die picoJava Technology Lizenz anderen Chip-Herstellern zur Verfügung gestellt wird, dient picoJava auch diesen Prozessoren als Grundlage. Lizenznehmer von picoJava sind unter anderen Firmen wie Fujitsu und IBM . picoJava ist speziell für den Einsatz in Mobiltelefonen, PDA`s, Set-Top-Boxen und anderen Kleingeräten ausgelegt. microJava: ist die 'customized Version' und der erste real existierende Mikroprozessor von Sun. MicroJava erweitert picoJava um anwendungsspezifische E/A, sowie Speicher-, Kommunikations- und Steuerfunktionen. Der Preis von microJava liegt zwischen 25 und 100 Dollar, was microJava für den Einsatz in einer Vielzahl von elektronischen Geräten, der Telekommunikation und anderen nicht vernetzten Anwendungen wie Drucker, Spielkonsolen, etc. auszeichnet. ultraJava: ist Sun’s High-End-Java-Prozessor. ultraJava unterstützt Sun`s Visual Instruction Set (VIS), einer performanten Hardwaregraphikerweiterung. Die ultraJava Prozessorlinie zielt hauptsächlich auf High-End-3D-Graphik- und Multimediaanwendungen ab. Der Preis liegt bei >= 100 Dollar. Pico Java Die Spezifikationeines Java Prozessors, picoJava soll nun näher untersucht werden. Da wie bereits erwähnt der Java Prozessor eine Implementierung der JavaVirtualMachine plus Erweiterungen zur Hardwarekonrolle ist, mag einiges aus der Darstellung der J-VM bereits bekannt sein. Seite 4/ 31 Zunächst Darstellung der JVM, sofern sie von picoJava implementiert ist Überblick Durch die Spezialisierung des Prozessors auf die Sprache Java können beim Entwurf bereits Optimierungen entwickelt werden, die zu einer Leistungssteigerung führen. Besonderheiten sind z.B. die stack-basierte Verarbeitung von Bytecode und die Speicherbereinigung (GC). In picoJava wird daher statt eines frei wählbaren Registersatzes ein Stackregistersatz mit 64 Einträgen genutzt. Die virtual machine (im folgenden VM genannt) ist eine 32-Bit Stack-Architektur. Das bedeutet, Operanden einer Instruktion werden vorher, z.B. durch LOAD/STORE, auf den Stack geholt, durch die Instruktion bearbeitet und das Ergebnis wird wieder auf dem Stack abgelegt, wo es dann für weitere Operationen zur Verfügung steht. Der Stack ist in Analogie zu einer Registerdatei aus der RISC-Architektur zu sehen, denn Operationen können nur Objekte auf dem Stack referenzieren. Ein weiteres Merkmal aus der herkömmlichen RISC-Architektur ist der Speicherzugriff, der bei der VM nur durch LOAD´s/STORE´s möglich ist. Die einzige Ausnahme hierbei stellt die IINC-Instruktion dar, die einen Speicher-zu-Speicher Zugriff darstellt, und vornehmlich zum Inkrementieren von Schleifenvariablen gedacht ist. Die VM hält eine Reihe von Registern, die meist zur Kontrolle spezielle Funktionen im Core dienen oder die Adressen verschiedener Bereiche im Stack (Stackpointer) enthalten. Darüber hinaus besitzt die VM noch vier (general purpose) 32-Bit Register und zwar: - PC (program counter) - VARS (points to base of the current set of local variables) - OPTOP (current top of evaluation stack) - FRAME (points to the base of the current execution environment in memory). Seite 5/ 31 Speicher der Laufzeit Umgebung Der Prozessor einer (virtuellen) Java Machine ist wie erwähnt eine Stackmaschine mit mehreren Stacks1 -jeweils ein JVM Stack pro Thread 2- und dem, von allen Threads gemeinsam genutzen Heap. Im Heap werden sämtliche Objekt-Instanzen sowie Klassen und Interfaces abgelegt, wobei sich Interfaces noch einmal in einem speziellen Bereich, der sog. Method Area befinden. Der Heap wird durch den Garbage Collector automatisch verwaltet / bereinigt. Zur Verwaltung von Prozeduraufrufen und Rücksprungadressen dient der Aufruf-Stack. Er enthält Frames (Aktivierungssätze), die bei jedem Unterprogrammaufruf erzeugt, auf dem Aufruf Stack abgelegt werden und die nötigen Rücksprungadressen, Zellen für lokale Variablen, Parameter, Operanden etc. enthalten. Der JVM-Stack ist eine Vereinigung von Aufruf- und Operanden Stack (s.u.) Zu jedem Stack existiert ein PC Register (Programmzähler), das auf die Adresse des gerade ausgeführten Codes verweist. Der class-constant-pool ist der Daten- und Codebereich eines Java Programms Mikroarchitektur E/A Bus- und Speicherschnittstelle stellt die Schnittstelle zwischen dem picoJava Kern, dem externen Speicher und anderen E/A-Geräten dar. Sie nimmt Daten an und liefert diese an den 1 hat den Vorteil eines kompakteren Befehlssatzes, da Operanden weggelassen werden können (Operanten Stack) 2 für Aufrufe nativer Methoden kann es dazu noch einen Native-Stack pro Thread geben Seite 6/ 31 Instruktions- und den Datencache weiter. Datencache besteht aus dem, in seiner Größe konfigurierbaren 3 Datencache und dem DatencacheKontroller. Die Datencache-Einheit arbeitet die Anfragen des Dripple Managers (s. u.) und der Pipeline ab. Instruktionscache besteht aus dem, in seiner Größe3 konfigurierbaren Instruktionscache und dem Instruktionspuffer. Die Instruktionscache-Einheit speichert hereinkommende Instruktionen und hält sie solange, bis sie der Dekodierblock der Integer-Einheit abholt. Um den Rest der Pipeline von der Fetch-Stufe zu trennen, wird ein 16 Byte Instruktionspuffer benutzt, der die Instruktionen solange im Speicher hält, bis sie von der Integer-Einheit verarbeitet werden können. Stackcache ist für die Kommunikation zwischen Stack und Integer-Einheit (Operanden) verantwortlich und kontrolliert Lesen bzw. Schreiben von Daten zwischen Datencache und Stackcache. Der Dribble Manager (to dribble = tröpfeln) sorgt bei Über- bzw. Unterlauf des Stackcaches für ein Herausschreiben in den Datencache bzw. für Datennachschub (s.u.). Integer-Einheit ist der „Pförtner“ für alle Instruktionen. Sie holt Instruktionen aus der InstruktionscacheEinheit, leitet Floating-Point-Instruktionen an die Floating-Point-Einheit weiter, führt alle Nicht-Floating-Point Instruktionen der JVM und die erweiterten Bytecodes (s.u.) aus, und holt bzw. speichert Daten aus/in der Datencache-Einheit. Die Floating-Point-Einheit führt alle mathematischen Funktionen aus. Sie ist optional. 3 auf 0kb, 1kb, 2kb, 4kb, 8kb, 16kb Seite 7/ 31 Pipeline Sun nutzt eine RISC-ähnliche Pipeline für picoJAVA. Sie hat nur 4 Stufen: Fetch, Decode, Execute und Writeback. Zugriffe auf den Cache (nie auf den Hauptspeicher) finden während der Execute Phase statt. (Stack-basierte Architekturen greifen nach einer LOAD-Instruktion immer auf die durch das LOAD gelieferten Daten zu -> Zugriff auf das Data Cache ist nicht “pipelined”). Seite 8/ 31 der Befehlssatz Jede Anweisung der Java VM besteht typischerweise aus einem ein Byte großem Opcode (256 Instruktionen) plus null oder mehreren Operanden (verschiedener Länge). Die Anzahl oder Größe der Operanden wird dabei implizit durch den Opcode bestimmt. Da der Befehlssatz Java's stackbasiert ist, werden bei vielen Befehlen die Operanden vom Stack geholt und das Ergebnis wieder auf den Stack gelegt. Diese Befehle brauchen keine zusätzlichen Bytes für Operanden und haben somit eine Länge von einem Byte. Befehle mit Operanden haben dagegen eine Länge von zwei, der oder mehr Bytes (s.u. Fehler! Verweisquelle konnte nicht gefunden werden.) Beispiel: iadd definiert eine Integer-Addition komplett, es muss nicht explizit angeben werden was addiert, oder wo das Ergebnis abgelegt werden soll. Die zwei obersten Elemente werden vom Stack geholt, addiert und das Ergebnis wieder zurück auf den Stack gelegt. Befehlsfomat Der Befehlssatz der JavaVM ist TYPISIERT, d.h. für jeden Operandentyp gibt es einen speziellen Befehl (z.B. Addition von Integer-Zahlen: iadd). Sind Operanden länger als ein Byte, werden sie in einer "Big-endian"-Order gespeichert. Seite 9/ 31 opcode byte short int long float double Tipush bipush sipush char reference Tconst iconst lconst fconst dconst aconst Tload iload lload fload Dload aload Tstore Istore lstore fstore dstore astore Tinc Iinc Taload baload saload iaload laload faload daload caload aload Tastore bastore sastore iastore lastore fastore dastore castore aastore Tadd iadd ladd fadd dadd Tsub isub lsub fsub dsub Tmul imul lmul fmul dmul Tdiv idiv ldiv fdiv ddiv Trem irem lrem frem drem Tneg ineg lneg fneg dneg Tshl ishl lshl fshl dshl Tshr ishr lshr Tushr iushr lushr Tand iand land Tor ior lor Txor ixor lxor i2l i2f i2d l2T l2i l2f l2d f2T f2i f2l f2d d2T d2i d2l d2f i2T i2b i2s Tcmp lcmp Tcmpl fcmpl dcmpl Tcmpg fcmpg dcmpg if_TcmpOP if_icmpOP Treturn ireturn if_acmpOP lretrun Seite 10/ 31 freturn dreturn areturn Funktionale Gruppierung Die 226 Befehle der JVM können in 15 funktionale Gruppen aufgeteilt werden. Um die decodierung möglichst effizient zu halten, bzw. um geringe Codedichte zu gewährleisten gibt es variable Codelängen, wobei häufig genutzte Instructions kürzer sind. Die Meisten Instruktionen (62%) sind 1 Byte groß. Die restlichen Befehle sind zumeist 2 Byte (20%) oder 3 (15%) Byte lang. Nur 6 Befehle (3%) sind größer als 3 Byte. Die durchschnittliche Befehlslänge beträgt 1,8 Byte (14-15 Bit). Befehlstyp Konstante auf Stack legen Laden/Speichern lokaler Variabeln Stackverwaltung Arithmetik Schiebe- und Logikoperatoren Verzweigungen und Vergleiche Ausnahmebehandlung Arrayverwaltung Methodenrücksprünge Switch-Table Verzweigungen Konvertierungen Monitore Änderungen von Objektfeldern Methodenaufrufe Sonstige Objektbehandlung Gesamtzahl Anteil (in %) Gesamtzahl Anzahl von Befehlen mit der Länge: 13 82 10 24 12 27 1 20 7 2 15 2 4 4 3 226 100 1 Byte 8 40 10 24 12 5 1 17 7 15 2 141 62 2 Bytes 2 41 1 1 45 20 3 Bytes 3 1 19 1 4 3 3 34 15 Mehr als 3 Bytes 2 1 2 1 6 3 226 Instruktionen: - 141 Instruktionen 1 Byte Länge (62%) - 45 Instruktionen 2 Byte Länge (20%) - 34 Instruktionen 3 Byte Länge (15%) - 6 Instruktionen >3 Byte Länge (3%) Vergleicht man den Befehlssatz von Java z. B. mit dem eines RISC-Prozessors, erscheint der Befehlssatz von Java unvollständig. Es fehlen beispielsweise Befehle, die mit den Zielen von Java nicht vereinbar sind. Das JSM4 zum Beispiel fordert z.B., dass es für den Programmierer nicht ersichtlich sein soll, wo Objekte im Speicher liegen. Deshalb ist es mit Java-Instruktionen nicht möglich direkt und willkürlich auf den Speicher zuzugreifen. JVM4 JSM – Java Security Model Seite 11/ 31 Instruktionen arbeiten stattdessen auf (dem Datentyp) Objektreferenzen, der keine Rückschlüsse auf die tatsächliche Position der Objekte im Speicher zulässt. Der Speicher ist für den Java Programmierer wie eine Black-Box. Es ist die Aufgabe der JVM die Position der referenzierten Objekte im Speicher zu bestimmen. Befehle, die man im Befehlssatz von Java außerdem vermisst sind Befehle zur Hardwarediagnose oder zu low-lewel Hardwaremanagement, Befehle um den CPU-Status zu lesen oder zu schreiben oder die On-Chip-Caches zu verwalten. Diese Befehle haben im Befehlssatz von Java keinen Platz, weil sie von der Beschaffenheit der Hardware abhängen. Außerdem sollte picoJava auch mit Code umgehen können der nicht in Java gechrieben wurde (z.B. C oder C++). Im Befehlssatz der JVM fehlen also Befehle, die für einen real existierenden Prozessor unabdingbar sind. Da man den Befehlssatz nicht einfach verändern kann, ohne damit auch Java zu verändern, hat man in picoJava den Java Befehlssatz erweitert. erweiterte Bytecodes Die folgende Tabelle fasst die 115 Befehle, um die der Befehlssatz der JVM erweitert worden ist, in fünf funktionalen Gruppen zusammen: Befehlstyp Diagnose Lesen/Schreiben von Registern Beliebiges Laden/Speichern Unterstützung anderer Programmiersprachen System Software Unterstützung Gesamtzahl Anteil (in %) Gesamtzahl Anzahl von Befehlen mit der Länge: 8 49 35 6 1 Byte - 2 Bytes 8 49 26 5 3 Bytes 9 1 17 115 100 2 2 2 10 98 85 5 15 13 Für die technische Realisierung dieser Erweiterung stellte sich als Problem: Mit einem Opcode von 8 Bit wie ihn die JVM verwendet, können maximal 28=256 Instruktionen codiert werden. Die Basis Instructions belegen 226 Instruktionen, also ist für weitere 30 Befehle noch Platz. picoJava benötigt allerdings 115 weitere Befehle. Dieses Problem wird durch so genannte Escape Bytes gelöst. Seite 12/ 31 Beim Design der JVM wurden zwei Bytes für implementations-spezielle Angelegenheiten reserviert. Eines dieser zwei Bytes wird als Escape Byte definiert. Stößt die picoJavaDekodierlogik beim Dekodieren eines Befehls auf dieses Escape Byte, so weiß sie, dass es sich bei diesem Befehl um einen erweiterten Befehl handelt, da diese Bytekombination von der JVM ja nicht benutzt wird. Die Dekodierlogik schaut dann auf das zweite Byte, um herauszufinden um welchen Befehl es sich handelt, bzw. was dieser Befehl macht. Diese 341 Instruktionen, die 226 Basis-, und 115 erweiterten Befehle ergeben den kompletten Befehlssatz von picoJava. Der Befehlssatz von picoJava kann nun nicht nur alles was die virtuelle Maschine kann, sondern noch mehr. picoJava kann Hardware kontrollieren, kann auf Speicher zugreifen und es kann effizient Code ausführen, der nicht in Java geschrieben wurde. Dadurch ist der Befehlssatz von picoJava ist nicht mehr länger der Befehlssatz einer Virtual Machine, es ist der komplette Befehlssatz einer reellen Maschine. Per Definition kann kein Programm, das in Java geschrieben und auf herkömmlichem Wege in Bytecode kompiliert wurde, erweiterte Bytecodes enthalten. Oder anders: Ein Programm, das erweiterte Bytecodes enthält kann kein normal kompiliertes Java Programm sein. Stattdessen sind die erweiterten Bytecodes von picoJava für Programme da, die nicht in Java, sondern z.B. in C oder C++, geschrieben wurden und auf picoJava portiert werden sollen. Da nur Plattformen, die auf picoJava basieren die erweiterten Bytecodes verstehen, läuft ein Programm, das erweiterte Bytecodes enthält nicht auf anderen Plattformen. Und da einige der erweiterten Bytecodes Programme ermöglichen, die direkten Zugriff auf den Speicher haben – und damit das JSM unterlaufen – kann kein Programm, das erweiterte Bytecodes enthält ( im Sinne des JSM ) als sicher bezeichnet werden. Nicht-Java-Code, der auf einem Java-Prozessor läuft, verhält sich also genau wie Nicht-Java-Code, der auf einem anderen Prozessor läuft, er ist plattformabhängig und nicht sicher. Ein RISC-Prozessor, hat ca. 100 Befehle. Der Befehlssatz von picoJava hat mit seinen 341 Befehlen mehr als drei Mal so viele Befehle. Um mit dieser Menge an Befehlen effektiv arbeiten zu können, wurden die Befehle in drei Gruppen eingeteilt. Seite 13/ 31 Komplexität Handhaben - Gruppierung nach Komplexität picoJava ist speziell für den Einsatz in eingebetteten Systemen gedacht, und soll deshalb klein und preiswert sein. Dieses Ziel steht in Konflikt mit dem großen Befehlssatz von picoJava. picoJava's Designer lösten dieses Problem, in dem sie nach der 'Schwierigkeit Befehle zu implementieren' die Instructions in drei Gruppen einteilten: einfache, relativ schwierige und sehr schwierige Befehle. Einfache Instruktionen: In diese Kategorie fallen die meisten Befehle der JVM und auch die meisten der erweiterten Befehle. Die Befehle dieser Kategorie sind RISC-ähnlich in dem Sinne, dass sie 'hardwired' – hart verdrahtet sind. Sie werden in nur einem Taktzyklus ausgeführt. Beispiele für solche Instruktionen sind „quick load“ von Objektfeldern und alle Integer Arithmetikoperationen. einfach (RISC) 1 Kürzel Operanden iadd Opcode Funktion 0x60 Ganzzahl Addition iload 8 bit O_set 0x15 lade lokale Variable(Ganzzahl) fload 8 bit O_set 0x17 lade lokale Variable(Flie_komma.) bipush 8 bit Konst. 0x10 lege eine Byte-Konstante ab ifeg 16 bit O_set 0x99 Springe, wenn 0 Relativ schwierige Instruktionen: In diese Kategorie fallen ca. 30 der Befehle der JVM und die restlichen der erweiterten Befehle. In einem typischen Java-Programm kommen wenige Befehle dieser Kategorie vor. Diese Befehle sind eher CISC-ähnlich in dem Sinne, dass sie Komplexität im 'Cisc-Style' handhaben – durch Mikrocode. Vom Hardware -Standpunkt gesehen, sind die Kosten von Seite 14/ 31 Microcode relativ gering. Ein kleiner Mikrocode-ROM kann die nötigen Kontrollsignale für diese Instruktionen enthalten. Der picoJava Kern benützt zwei ungefähr 2 Kilobyte große ROMs: einen in der Integer Einheit und einen in der optionalen Floating-Point-Einheit. Mikrocodierte Instruktionen benötigen zwischen 3 (z. B. iaload) und 21 (z.B. invokesuper_quick) Taktzyklen. Sie bieten eine akzeptable Balance zwischen der Notwendigkeit die Hardwareimplementation einfach zu halten und guter Performance. komplex (Cisc) 1 Lookupswitch – Java Switch Anweisung: byte 1 byte 2 byte 3 opc. 0xAB 0..3 bytes padding byte 4 default - standard Sprungaddresse Anzahl der Vergleichswerte Vergleichswert 1 Sprungaddresse fur Wert 1 • Vergleichswert 2 Sprungaddresse fur Wert 2 • ... Sehr schwierige Instruktionen In diese Kategorie fallen die letzten 30 Befehle der JVM. Die Instruktionen in dieser Kategorie sind entweder sehr schwierig, oder benötigen Dienste vom Betriebssystem, oder beides. Zum Beispiel treffen auf den Befehl new, der zur Erzeugung neuer Objekte benutzt wird, beide Fälle zu: Er ist relativ schwierig, da nachgeschaut werden muss, ob die Klasse des neuen Objekts in der Liste der bereits geladenen Klassen existiert und es muss Speicher allozeiert werden, was eine Koordination mit Betriebssystem und eine gewisse Flexibilität in der Art wie diese Allokation implementiert ist erfordert. Außerdem muss die neue Klasse, wenn sie bisher noch nicht geladen wurde, über ein Netzwerk oder das lokale Filesystem geladen werden. Seite 15/ 31 Da die Instruktionen in dieser Kategorie also entweder sehr komplex sind und/oder eine flexible Im plementierung (wegen ihrer Abhängigkeit vom Betriebssystem) benötigen, sind sie in Software implementiert. Wenn ein Programm einen dieser Bytecodes erfordert, ruft die CPU einen sog. 'instruction emulation' Trap (IET). Abhängig davon welcher Bytecode die IET ausgelöst hat, ruft der Exception-Handler eine spezielle Softwareroutine auf, welche die getrappte Instruktion durch eine Abfolge von mikrocodierten und hart-verdrahteten Instruktionen abarbeitet. Eine solche Verarbeitung von Instruktionen durch Software ist natürlich sehr langsam: Eine Instruktion dieser Kategorie benötigt mehrere hundert-, bis sogar mehreren tausend Taktzyklen. Al lerdings werden diese Instruktionen auch von Interpretern oder dynamischen Compilern anderer Prozessoren durch ähnliche Sequenz kleinerer Routinen ausgeführt. picoJava hat hier also im Vergleich keinen Nachteil. Im Gegenteil, ist picoJava im Vergleich zu anderen Prozessoren bei der Ausführung im Vorteil da die emulations Routinen nicht nur über HardwareTraps sehr schnell erreichbar, sondern auch bereits vorgeladen sind. Befehle dieser Kategorie kommen -im Vergleich zu Befehlen der anderen Kategorien- in einem typischen Java-Programm jedoch nicht oft vor. Kürzel Operanden Op-Code Funktion instanceof Klasse 16b. 0xC1 Klassenzugehörigkeit athrow 0xBF Ausnahmebehandlung new Klasse 16b. 0xBB ein neues Objekt newarray atype 8b. 0xBC eine neue Tabelle 0xC2 Erwerben der Kontrolle monitorenter über ein Objekt monitorexit 0xC3 Abgeben der Kontrolle Stackorganisation Wie bereits beschrieben ist die JVM und damit auch picoJava / der Befehlssatz von picoJava stackbasiert. Dies bedeutet, dass bei vielen Befehlen implizit angenommen werden kann, dass Operanden vom Stack geholt und das Resultat des Befehls wieder auf den Stack gelegt wird. Ein stackbasierter Befehlssatz ermöglicht kleine, sichere und portable Programme, hat Seite 16/ 31 aber den Nachteil, dass stackbasiertes Verarbeiten von Befehlen sehr ineffizient ist. Eine Stackmaschine benötigt Zeit (Taktzyklen) um Operanden auf den Stack zu legen, wo Rechenoperationen sie dann verarbeiten können. Nachdem die Rechenoperationen ihr Ergebnis wieder auf den Stack gelegt haben, benötigt die Stackmaschine wieder Zeit um die Resultate vom Stack zu nehmen und sie abzuspeichern. Eine Registermaschine braucht keine Zeit um Operanden hin und her zu bewegen, da hier direkter Zugriff auf die jeweiligen Register möglich ist. Im Vergleich zu Registermaschinen benötigen Stackmaschinen für die gleiche Anzahl von Berechnungen 30 Prozent mehr Operationen 5 (durch die zusätzlichen Stackzugriffe). Beispiel / Vergleich: Registerbasierte Addition Stackbasierte Addition ADD R3, R2, R1 ILOAD_1 ILOAD_2 IADD ISTORE_3 Bei einer registerbasierten RISC-Maschine Die stackbasierte Addition benötigt vier kann eine Addition in einer einzigen Instruktionen, die jeweils einen Taktzyklus Instruktion ausgeführt werden. In den benötigen. Die ersten beiden Befehle Registern R1 und R2 sind die beiden bewegen die Summanden zum Stack. Summanden gespeichert, während im Diese werden mit dem dritten Befehl Register R3 die Summe gespeichert addiert. Das Resultat wird dann schließlich werden soll. Die Operation kann in einem mit dem vierten Befehl Taktzyklus ausgeführt werden. abgespeichert. wieder Wie ist dies aufzulösen? picoJava hat bereits einen stackbasierten Befehlssatz, ein registerbasierter Befehlssatz kommt also nicht mehr in Frage. Trotzdem wollte man auf die Effizienz einer Registermaschine nicht verzichten. Die Lösung dieses Problems ist eine Registermaschine, die so organisiert ist, dass sie ein stackbasiertes Verarbeiten der Befehle unterstützt. 5 W. Wulf et al., 'The Design of an Optimizing Compiler', American Elsevier, New York, 1973. Seite 17/ 31 picoJava's Stackorganisation 1 Die picoJava Registermaschine hat 64 Register, welche genutzt werden um die obersten 64 Einträge des Stacks zu cachen. picoJava behandelt diese Register als „circular Buffer“, was bedeutet, dass das unterste Element und das oberste Element im Stack benachbart sind. picoJava hält sich einen Zeiger auf das oberste Element im Stack. Wenn die Ausführungseinheit Elemente vom Stack holt, schrumpft der Stack und der StackPointer wird inkrementiert. Wenn die Ausführungseinheit dagegen Elemente auf den Stack legt, wächst der Stack und der Pointer wird dekrementiert. Wie die meisten Stacks wächst der picoJava Stack von höherwertigen Adressen zu niederwertigeren. Legt man 65 Elemente auf den Stack, überschreibt dies das erste Element im Stack. Hier ist dann der DribbleManager gefordert . Die Registermaschine hat drei Lese- und zwei Schreibports. Rechenoperationen können gleichzeitig zwei Operanden lesen und ein Resultat zurück schreiben. Die restlichen Ports (ein Lese- und ein Schreibport) belegt der Dripple Manager, der durch ständige im Hintergrund laufende Spill- und Fill-Operationen 6 den Stackcache konsistent hält. 6 Spill – schreibt Einträge vom Register File in den Datencache un schafft so Platz für neue Werte Seite 18/ 31 High- und Low Watermarks Einträge, die noch nicht in den Datencache geschrieben wurden, werden als 'dirty' bezeichnet. Überschreitet bei wiederholtem Ablegen von Elementen auf dem Stack die Anzahl der 'dirty' Einträge eine obere Grenze (High-Water-Mark), schreibt der SpillMechanismus (im Hintergrund) diese Einträge in den Datencache heraus – beginnend mit dem Ältesten. Bereits in den Cache kopierte Werte werden als 'clean' betrachtet. Obwohl der Stackcache nur 64 Einträge hat können - indem clean'e, bereits in den Datencache geschriebene Werte überschrieben werden - viel mehr Elemente auf dem Stack liegen. Wenn dagegen der Stack durch mehrmaliges auslesen von Daten schrumpft und unter die untere Grenze (Low-Water-Mark) fällt, holt der Fill-Mechanismus des Dripple-Managers (im Hintergrund) Einträge aus dem Datencache und füllt den Stack damit wieder auf. Die Spill- und Fill-Mechanismen erwecken so den Eindruck, dass der Stackcache eine unendliche Anzahl von vollen Registern hat, die belesen und beschrieben werden können. Eine der wichtigsten Merkmale des picoJava Caches ist, das er eine Lösung für das klassische Problem des ineffektiven Stackzugriffs anbietet. Instruction Folding Zusammenfassen von Befehlen Da der Stack in Wirklichkeit ein Register File mit Random Access ist, hat picoJava's Pipeline nicht nur direkten Zugriff auf die obersten zwei, sondern auf die obersten 64 Stackelemente, was das Instruction Folding – das Zusammenfassen von Instruktionen – ermöglicht. Dabei geht picoJava folgendermaßen vor: Basierend auf folgenden Gruppierungsregeln scannt picoJava's Instruction Decoder den eingehenden Bytecode-Strom und untersucht ihn nach Befehls-Sequenzen, die zu einer Fill – liest Einträge vom Datencache in das Register File und sorgt so für die nächsten Berechnungen vor Seite 19/ 31 einzelnen Instruktion zusammengefasst werden können. Diese zu 'faltenden' Sequenzen können innerhalb von bis zu 4 Bytecode Instructions - lokale Daten direkt vor den Rechenoperationen die diese Daten nutzen oben auf den Stack bewegen und/oder - Berechnungen direkt gefolgt vom lokalen Speichern der Berechnungsresultate enthalten. Wenn solch eine Sequenz gefunden wird, erzeugt der Core daraus eine einzelne, Registerbasierte RISC-artige Operation, indem die - auszuführende Rechenoperation und - ihre Operanden, sprich o die Adresse der lokalen zu ladenden Variablen (Quellregister) und o die Adresse der lokal zu Speichernden Variablen (Zielregister) kombiniert werden. Einfacher gesagt, kombiniert picoJava die in Stackarchitekturen für eine Rechenoperation nötigen zusätzlichen Stackmanipulationen mit der Rechenoperation selbst. Dies vermindert den rechentechnischen Overhead einer Stackmaschine für die 'gefalteten' Operationen drastisch. Es wird eine 'Single Cycle' Execution erreicht und damit die, von der Risc Architektur bekannte Effektivität bei der Ausführung, da sich unter der Stackarchitektur eine Risc ähnliche Architektur komplett mit einem 64 Einträge großen Register-File und '3 Operanden, register-basierten' Operationen befindet. Beispiel: Instruction folding Das, aus dem Abschnitt 'Stackorganisation' bereits bekannte Beispiel : ILOAD_1 ILOAD_2 IADD ISTORE_3 iload_1 und iload_2 bewegen lokale Daten auf den Stack. Darauf folgt direkt die Rechenoperation iadd, die nur die bewegten Daten benutzt. Dies ist Gruppierungsregel 1. Seite 20/ 31 Mit istore_3 wird das Resultat aus iadd lokal abgespeichert. Dies ist Gruppierungsregel 2. Damit können diese vier Befehle zu einem Befehl der Art ADD R3, R2, R1 zusammengefasst werden. R1 ist der, mit iload_1 auf den Stack bewegte, erste Summand, R2 ist der, mit iload_2 bewegte, zweite Summand und R3 ist das Register, zu der istore_3 die Daten bewegt. Der zusammengefasste Befehl benötigt statt vier nur noch einen Taktzyklus. Hardwareunterstützung der Laufzeitanforderungen Um die Ausführung von Java Programmen zu beschleunigen, wurde beim Entwurf von picoJava darauf geachtet, dass bestimmte Laufzeit-Anforderungen von Java Programmen, wie zum Beispiel die Thread-Verwaltung, das Object-Handling, und die Garbage Collection, durch die Hardware unterstützt werden. Dies soll für den Garbage Collection Mechanismus näher untersucht werden Seite 21/ 31 Garbage Collection Wie erwähnt, fordert das JSM, dass es nicht ersichtlich ist, wo Objekte im Speicher liegen. Ein Java Programm kann nur über Objektreferenzen 7 die keine Rückschlüsse auf die tatsächliche Position der Objekte zulassen, auf den Speicher zuzugreifen. Ein Java Programm kann also selbst keine Speicherverwaltung durchführen -diese Aufgabe übernimmt die JVM: Sie verwaltet den Speicher eines Programms automatisch, in dem sie dynamisch Speicherbereiche auf dem Heap alloziert, wenn er benötigt wird und ihn wieder frei gibt, wenn er nicht mehr benötigt wird. Der von der JVM verwendete Mechanismus zur Speicherverwaltung ist Garbage Collection. Die JVM hat einen HEAP Bereich, in dem Objektinstanzen, Klassen u. Interfaces liegen (s.o. [Speicher der Laufzeit Umgebung]) Heap ist der einzige Speicherbereich, der durch automatische Speicherbereinigung (GC) verwaltet wird. Neben der Bytecode-Ausführung ist der andere entscheidende Aspekt für die Performance einer VM die Garbage Collection. Weder in der Java Sprachspezifikation noch in der Java VM Spezifikation sind jedoch genaue Vorgaben für die Garbage Collection zu finden. Es steht den Herstellern somit größtenteils frei, wie sie die Speicherverwaltung implementieren. picoJava's Garbage Collection Mechanismus ist ein Zusammenspiel der folgenden Verfahren Mark & Sweep (markieren und rausschmeissen) Eine der einfachsten Formen der Garbage Collection ist der sogenannte Mark & SweepAlgorithmus. Bei diesem Verfahren wird ein Speicherbereich periodisch nach Objekten durchsucht, die von laufenden Programmen aus noch erreicht werden können. Die so gefundenen Objekte werden mit einem Bit markiert. Wenn alle noch erreichbaren Objekte markiert wurden, kann vom restlichen Speicher – in dem evtl. noch nicht erreichbare Objekte liegen – angenommen werden, dass dieser frei ist. Der Garbage Collector geht dann den Speicher durch, merkt sich allen freien Speicher und fügt ihn wieder zum „freier Speicher Pool“ hinzu. Um eine Fragmentierung des Speichers zu verhindern ist der letzte Schritt dieses Verfahrens, dass der freie Speicher und auch die noch referenzierten Objekte zu gemeinsamen Blöcken zusammengeführt werden. 7 Datentypen der Java VM: byte, short, int, long, float, double, char, object, returnAddress Seite 22/ 31 Während dieses Verfahrens (Aufräumen & Zusammenführen) befindet sich das System in einem instabilen Zustand, da, wenn der Prozess einmal begonnen hat er nicht unterbrochen werden darf. Dies führt zu merklichen Performanceeinbußen. Zum Markieren der Daten (referenziert/nicht referenziert) werde die, in den Datentypen Zeiger/Reference und Object für Software Zwecke reservierte Bits genutzt (Bits 30, 31) Diese gibt’s es sowohl im Typ Zeiger (Reference) als auch im Header eines Objekts für Softwarezwecke. Handles Das Verschieben von Objekten im Speicher wird durch sog. Handles unterstützt. Es wird zwischen direkten Referezen -Zeiger- und indirekten Referenzen –Handles– unterschieden. Dies trifft eine Aussage über die Art der Objektspeicherung. Der 32 bit große Typ Reference (Zeiger) nutzt Bit 0 um zwischen Handle und 'nicht Handle'zu unterscheiden. Ist der Wert von Bit 0=0 handelt es sich um eine direkte Referenz, ist er 1 um eine indirekte. Seite 23/ 31 Objektspeicherung An der Adresse auf die ein Zeiger verweist beginnt das Objekt im Speicher. An dieser Stelle liegt der Objekt Header, der Methoden Vektor o.ä. enthält. Ist ein Objekt nun in 'herkömmlicher' Weise im Speicher abgelegt liegen seine weiteren Daten direkt hinter dem Header. Zeiger auf solche Objekte nennen sich direkte Zeiger- das Handle-Bit ist nicht gesetzt. Ist das Handle-Bit einer Referenz allerdings gesetzt weist das die zweite Form der Objektspeicher hin. Hier liegt direkt hinter dem Objekt Header ein sog. Handle, ein weiterer Zeiger der auf die Adresse verweist an der der Rest des Objekts abgelegt ist. Seite 24/ 31 Diese Form hat den Vorteil, das wenn das Objekt im Speicher verschoben wird, nur die Handles und nicht alle vereisenden Zeiger geändert werden müssen. Sie bieten also einen einfachen Mechanismus für den GarbageCollector Objekte im Speicher zu verschieben, haben allerdings auch Nachteile. Die Nutzung von Handle benötigt zusätzlichen Speicher und Ausführungszeit. Jede Instruction die auf ein Objekt zugreift benötigt zusätzlichen Aufwand von mindestens 2 Taktzyklen um die Umleitung, die Indirektion zu handhaben. Ausserdem werden bei der Speicherung per Handle für jedes gespeicherte Objekt ein zusätzliches Word Speicher für den 'Object Storage Pointer' benötigt. Generational Garbage Collection Bei der Generational Garbage Collection (GGC) werden die Speicherscans des Mark & Sweep auf kleinere Speicherbereiche beschränkt. Dies erhöht die Performance der Garbage Collection, und damit auch deren Einsetzbarkeit in Echtzeit-Umgebungen. Generationen-Kollektoren gehen davon aus, dass einige Objekte länger leben als andere. Weiterhin wird angenommen, dass die meisten Objekte jung sterben. Dementsprechend wird der Heap in mehrere Überlebensräume – Generationen – unterteilt. Neue Objekte werden grundsätzlich in der jüngsten Generation – Eden- alloziert. Ist kein Platz mehr in Eden, greift ein kopierender Kollektor, scannt diesen Überlebensraum und verschiebt lebendige Objekte die eine bestimmte Anzahl von Aufräumphasen überlebt haben in den nächsten Überlebensraum (babei müssen alle Zeiger auf das Objekt entsprechend angepasst werden). Seite 25/ 31 Räume für jüngere Generationen sind kleiner. Wenn ein Bereich ein neues Objekt nicht mehr aufnehmen kann wird er gescannt und per mark & sweep aufgeräumt. Dies führt zu einer höheren Scanfrequenz. Dieser erste Überlebensraum wird mit hoher Frequenz nach nicht mehr referenzierten Objekten gescannt, wodurch bereits die meisten „toten“ Objekte entfernt werden können. Der nächste Überlebensraum wird seltener gescannt. Objekte, die auch hier die Inkubationszeit überleben, werden wieder in den Bereich für die nächste Generation verschoben und auch hier nimmt die Frequenz der Scanns wieder ab. Dieses Verfahren wird bis zur n-ten Inkubationsstufe fortgesetzt, wobei jeweils die Scanfrequenz abnimmt. Objekte die auch die n-te Inkubationszeit überleben, werden in den restlichen Speicher, die permanente Generation verschoben. Auch beim GGC-Verfahren muss ab und zu der gesamte Speicher nach nicht mehr benötigten Objekten durchsucht werden, da die Zahl der langlebigen Objekte mit der Zeit zunimmt und die Gefahr besteht, dass der Speicher aufgebraucht wird. Dennoch wird die Zahl dieser zeitintensiven komplett-Scans zur Ausnahme. Generationen optimieren also die Speicherbereinigung insofern, als dass nicht immer der ganze Speicher aufgeräumt wird, sondern nur der Teil, der gerade vollgelaufen ist. Die Pausen, die auftreten wenn das Programm z ur Speicherbereinigung gestoppt werden muss, sind somit geringer als bei nur einem großen zusammenhängenden Speicherbereich, der immer komplett gesäubert werden muss. Dabei macht man sich zunutze, dass ein kleines Speichersegment gewöhnlich sehr schnell mit Objekten belegt ist, die zu einem großen Teil direkt wieder aus dem Speicher entfernt werden können. Es gibt noch weitere hochentwickelte Garbage Collection Verfahren, von denen die meisten auch auf einer Segmentierung des Speichers beruhen. picoJava unterstützt daher die Segmentierung von Speicher hardwareseitig durch sog. Schreibbarrieren. Seite 26/ 31 Write Barriers Um einen bestimmtes Speichersegment zügig nach Objekten scannen zu können auf die noch Verweise existieren, muss der GC ein Liste mit mit allen Objekten in diesem Segment , die von ausserhalb des Segments erreichbar sind vorhalten. Ohne eine solche Liste müsste der GC den Rest des Speicher scannen um herauszufinden welches externe (extern des zu scanneden Segments) Objekt noch Referenzen auf interne Objekte hält (das somit ja nicht entfernt werden darf). Der primäre Mechanismus um eine solche Liste zu erstellen sind sog. Schreib Barrieren. Der Schreib-Barriere Mechnismus ermöglicht es Zeiger von Objekten die sich ausserhalb des zu scannenden Bereichs befinden in sog. Stores vorzuhalten/zu listen um sie effizient daraufhin zu untersuchen, ob sie auf Objekte inerhalb des zu scannenden Segments verweisen. In GenerationsSystemen werden diese Zeiger als 'inter generational pointers' bezeichnet. picoJava bietet eine fleixible Methode um Segment Grenzen zu definieren. Ist ein Segment einmal festgelegt, prüft als nächstes die Hardwe alle Zeiger in den 'Zeiger Stores' daraufhin, ob einer der Pointer auf ein Objekt in einem, von sich selbst verschiedenes Segment verweist. Ist das der Fall so wird eine Unterbrechung, ein 'Trap' ausgelöst 8 und der GC kann entsprechend reagieren, was hier bedeutet die Referenz in seine entsprechende Liste aufzunehmen. Der TrapHandler kann anhängig von genutzten GC Algorithmus verschieden reagieren. Die Segmentierung erfolt durch Maskierung des Registers GC_Config in welchem die Segment größen festgelegt werden, die Bestimmung zu welchem Segment ein Zeiger oder Objekt gehört durch abgleich des Zeigers/Objekts mit dem Register Benchmark Die Abbildung zeigt picoJava im Vergleich mit einem 80486- und einem Pentium-Prozessor. Es wurden javac, der Sun Java Compiler, und ein nicht näherbestimmter Raytracer jeweils 8 gc_notify(type = 0x27) Seite 27/ 31 auf dem Pentium und dem 486er mit einem Interpreter und einem JIT-Compiler getestet. Wie man sieht schneidet picoJava 15 bis 20 mal schneller als der 486 mit Interpreter bzw. 5 mal schneller als der Pentium mit einem JIT Compiler ab. Dies Testergebnis ist jedoch insofern fraglich, da javac und der nicht weiter spezifizierte Raytracer vielleicht extra gewählt wurden, weil sie besonders gut auf picoJava verarbeitet werden können. Zusammenfassung Java biete durch seine Objektorientierung und den Bytecode viele Vorteile. Beim Einsatz von Java in eingebetteten Echtzeitsystemen zeigt sich jedoch, dass interpretierter Bytecode in der Regel zu langsam ist. Diese Probleme sollen durch einen Java Prozessor gelöst werden Der vorgestellte, von Sun spezifizierte Prozessor-Kern picoJava wurde von mehreren großen Firmen lizenziert, ein picoJava-Boom blieb jedoch aus. Keiner der Lizenznehmer hat jemals picoJava-basierte Chips verkauft9. Bereits zum Erscheinen der Spezifikation wurde der Ansatz kritisch beäugt. Es waren bereits Spezial-Prozessoren für andere Sprachen wie LISP und Smalltalk entwickelt worden, und man hatte gesehen, dass Software-Implementierungen auf RISC-Chips bessere 9 deswegen gibt es keinen Abschnitt "Implementierungen" Seite 28/ 31 Performance boten. Man zweifelte daran, dass Suns picoJava besser performte. Und tatsächlich stellte sich später heraus, dass picoJava weder schnell noch billig noch sparsam genug war, um im Markt für Mobiltelefone und PDAs mithalten zu können. Stattdessen wurde ein etwas anderer Ansatz für Kleingeräte populär: Java-Beschleuniger. Dabei handelt es sich um Bausteine, die ähnlich wie Koprozessoren zusätzlich zum Hauptprozessor verwendet werden können. So lässt sich beispielsweise Nazomis bereits erwähnter Java-Koprozessor in bestehende Designs einbinden und erleichtert so Kleingeräte-Herstellern die Verwendung von Java unter Beibehaltung einer bereits vorhandenen Architektur. Einen anderen Weg ging die Firma ARM. ARM hat seinen Chips den Java-VM-Befehlssatz schlicht als dritten Befehlssatz hinzugefügt. Ein einfaches Umschalten macht so aus dem herkömmlichen ARM-Chip eine Java VM. Seite 29/ 31 Quellen SUN MICROELECTRONICS picoJava Microprocessor Cores http://www.sun.com/microelectronics/picoJava/ the Java TM Virtual Machine Specification, Second Edition http://java.sun.com/docs/books/vmspec/ Harlan McGhan: "picoJava: a direct Execution Engine for Java Bytecode" http://www.ece.purdue.edu/~arch/seminar/s chedules/spring00-pdf/mcghan98.pdf Inside the Java Virtual Machine http://www.artima.com/insidejvm/ed2/ Sun Gambles on Java Chips http://www.byte.com/art/9611/sec6/art2.htm#116st3d2 picoJava2 ProzessorCore Description http://epicentertech.net/java/Resources/Embedded_Java/Micro%20&%20Pico%20Java/picoJ ava-II.pdf Suns picoJava: http://www.sun.com/microelectronics/picoJava/ ARM: http://www.arm.com/ Nazomi: http://www.nazomi.com/ Seite 30/ 31