Einführung 1 - weblearn.hs

Werbung
Java Prozessoren
Referat
Labor Computertechnik
WS 2004
Bernd Reiners
Mat.Nr.: 49621622
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 VM's entfallen
Java Prozessoren könnten als in Silizium gegossene Java Virtual Machines bezeichnet 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 JVM's gemeinsame) StackArchitektur. Ein Beispiel hierfür ist Nazomis Java Koprozessor JA108 [9] (4k -2fach
assoziativer Bytecode-Cache, 2K Data Cache 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 Beispiel hierfür ist die
'ARM Jazelle Technology' [8a], die in den ARMxxJx Prozessoren zum Einsatz kommt.
'reine' Java Prozessoren
Von welchen dies Referat handelt und die im Folgenden näher untersucht werden
Einsatzgebiete
Der Einsatz von Java-Prozessoren ist in vielen Bereichen möglich. Insbesondere eignen sie sich in
ressourcenkritischen Umgebungen wie z.B. embedded Systems oder Mikrokontroller 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.
Einsatzgebiete von Java Prozessoren { SEQ Einsatzgebiete_von_Java_Prozessoren \* ARABIC }
Übersicht: Java Prozessoren Context
Der Java Prozessor Kontext von Sun [1] besteht neben der Spezifikation picoJava [6,7] 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. 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
'MicroJava' ist die 'customized Version' und der erste real existierende Mikroprozessor von
Sun. MicroJava erweitert picoJava um anwendungsspezifische E/A Einheiten, sowie Speicher-,
Kommunikations- und Steuerfunktionen. Der Preis von microJava lag 1997 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
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 lag bei c.a
100 Dollar und darüber.
Pico Java
Die Spezifikation eines Java Prozessors, picoJava soll nun näher untersucht werden.
Da wie bereits erwähnt der Java Prozessor eine Implementierung der JavaVirtualMachine plus
Erweiterungen zur Hardwarekontrolle ist, mag einiges aus der Darstellung der JVM bereits
bekannt sein.
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 (s.u.).
Besonderheiten der JVM 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 Java Virtual Machine (im folgenden VM genannt) ist eine 32-Bit Stack-Architektur. Das
bedeutet, Operanden einer Instruktion werden vor Ausführung der Instruction, z.B. durch
LOAD/STORE, auf den Stack geholt, durch die Instruktion bearbeitet und das Ergebnis 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, Operationen können nur
Objekte auf dem Stack referenzieren. [10 Kap. 3.1]
Ein weiteres Merkmal aus der herkömmlichen RISC-Architektur ist der Speicherzugriff, der
bei der VM nur durch Load/Store Befehle 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 enthalten (Flagregister, Stackpointer o.ä.)
Darüber hinaus besitzt die VM noch 32-Bit Register:
−
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).
vars Local Variable Pointer Register
enthält Basis Adresse (erste, unterste) des 'Lokale Variable' Bereichs der akt. Methode, sprich
Adresse der lokalen Variablen 0. Variable 1 wird dann an 'Vars-4' (4=1Word in Byte) abgelegt
Frame – Frame Pointer Register
Startadresse des Aufrufstacks der akt. Methode
optop – Top-of-Stack Pointer Register
Adresse des akt. obersten (noch leeren) Stackeintrags. Nach Push wird Registerwert um 4
inkrementiert. Aktuell oberster Eintrag mit gültigem Wert ist demnach 'iptop+4'
oplim – Minimum Value of Top-of-Stack Register
enthält minimalen wert den Top-of-Stack Register optop annehmen kann. Begrenzt das stack
Wachstum (von oben nach unten)
Form:
Enable Flag wird bei PowerOn Resettet ( Software setzt es, HW setzt es zurück)
sc_bottom – 'address of deepest Stack Cache Entry' register
zeigt den momentan 'tiefsten gültigen Eintrag des Operanden Stacks' im Stackcache
Speicherorganisation der Laufzeit Umgebung
Der Prozessor einer (virtuellen) Java Machine ist wie erwähnt eine Stackmaschine mit mehreren
Stacks 1 -jeweils ein JVM Stack pro Thread 2 - und einem von allen Threads gemeinsam genutzten
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 (AufrufStack, Parameter,
lokale Variablen) (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. Er ist ein
Index-Mapping Mechanismus für kompilierte Methoden in class-Dateien. Jede Klasse hat einen
zugehörigen Konstanten Pool
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
Mikroarchitektur
E/A Bus- und Speicherschnittstelle
stellt die Schnittstelle zwischen dem picoJava Kern, dem externen Speicher und anderen E/AEinheiten dar. Sie nimmt Daten an und liefert diese an den Instruktions- und den Datencache
weiter.
Datencache
besteht aus dem, in seiner Größe konfigurierbaren Datencache und dem Datencache-Kontroller.
Die Cachegröße ist zwischen 0 Kbyte, 1 Kbyte, 2 Kbyte, 4 Kbyte (default), 8 Kbyte und 16 Kbyte
konfigurierbar. Die Datencache-Einheit arbeitet die Anfragen des Dribble Managers (s. u.) und
der Pipeline ab (wobei der Pipeline Priorität eingeräumt wird)
Der DatenCache ist ein '2-fach assoziativer' Cache mit Schreibstrategien write-back und write
allocate [10 Kap. 6.1]
Instruktionscache
besteht aus dem, in seiner Größe konfigurierbaren Instruktionscache und dem
Instruktionspuffer. Die Cachegröße ist zwischen 0 Kbyte, 1 Kbyte, 2 Kbyte, 4 Kbyte (default), 8
Kbyte und 16 Kbyte konfigurierbar. Der Instructionscache cached eingegangene Instruktionen
und liefert sie an die Decodiereinheit
Um den Rest der Pipeline von der Fetch-Stufe zu trennen, wird ein 16 Byte Instruktionspuffer
eingesetzt, um die, aus dem Speicher geholten Instruktionen solange vorzuhalten, bis sie von
der Integer-Einheit (bzw. Decodier Einheit) verarbeitet werden können.
Der InstructionCache ist ein 'direct-mapped' Cache mit 16-byte großen 'Cache-Lines' (64
Einträge pro kb Cachegröße) [10 Kap. 6.1]
Stackcache
besteht aus dem Stackmanager, einen 3-Lese, 2 Schreib Port Stackcache (64 Einträge à 32 bit)
und dem Dribble Manager(s.u.)
Er ist für die Kommunikation zwischen Stack und Integer-Einheit (Operanden) verantwortlich
und kontrolliert Lesen bzw. Schreiben von Daten zwischen Datencache und Stackcache.
Der Stackcontroller sorgt für die nötigen Steuersignale an der Integer-Unit (2 Lese- 1
Schreibport zum Stackcache) um Operanden zu empfangen. Er kontrolliert ebenfalls das Lesen
von Daten aus dem Datencache in den Stackcache und umgekehrt
Der sog. Dribble Manager (to dribble = tröpfeln) - ein Hardware-Mechanismus der den aktuellen
Füllstand des Stacks überwacht - sorgt bei einem drohenden Stack-Überlauf dafür dass Daten
vom Stackcache in den Datencache ausgelagert und umgekehrt bei Stack-Unterlauf vom
Speicher in den Stackcache zurück geschrieben werden. [6a] Dafür verfügt er über einen
einzelnen Schreib- Lese Port des Stackcaches der exklusiv vom Dribbler genutzt wird (s.u.)
Da diese Transaktionen im Hintergrund stattfinden während der Core weiterhin seinen
Hauptaufgaben nachgeht ist der Begriff 'Dribbler' (to dribble = tröpfeln) hier so zu verstehen
dass durch die ständigen Transaktionen kleine Datenmengen vom Cache in den Speicher tröpfeln
und umgekehrt.
Höchst- bzw. Tiefststand des Stacks sind durch 'High- bzw Low Watermark' in den 3-Bit
Feldern PSR.DBH und PSR.DBL bestimmt. In jedem Taktzyklus wird nun die Anzahl der
vorhandenen Stack-Einträge durch Vergleich von SC_BOTTOM und OPTOP bestimmt. Abhängig
vom Ergebnis wird eine Über- oder Unterlauf Transaktion ausgelöst:
Ist die Anzahl der gültigen Einträge größer als die 'High Watermark' startet der Core eine
Überlauf Transaktion welche den obersten Wert, den an der Stelle 'SC_BOTTOM'
herausschreibt und 'SC_BOTTOM' mit der Adresse des nun gültigen obersten Stackeintrags
aktualisiert. Dieser Vorgang wiederholt sich bis die Anzahl der Stackeinträge unter die 'High
Watermark' sinkt.
Liegt die Anzahl der beschriebenen Stackeinträge unter der 'Low Watermark' wird eine
'Fülltransaktion aus dem Speicher' getriggert indem der Wert an der Stelle 'SC_BOTTOM+4'
angefordert wird und sobald dieser vorliegt ' SC_BOTTOM' mit der Adresse des neuen
nächsten gültigen Stackeintrags aktualisiert. Auch dieser Vorgang wiederholt sich bis die Anzahl
der Stackeinträge die 'low Watermark' erreicht.
Integer-Unit
ist der 'Pförtner' für alle Instruktionen die ausgeführt werden. Sie holt Instruktionen aus der
Instruktionscache-Einheit( InstructionFetch), holt bzw. speichert Daten aus/in der Datencache-
Einheit, leitet Floating-Point-Instruktionen an die Floating-Point-Einheit weiter und führt alle
Nicht-Floating-Point Instruktionen der JVM und die erweiterten Bytecodes (s.u.) aus.
Da picoJava eine stackbasierte Architektur ist sind für den Programmierer hier keine General
Purpose Register sichtbar.
Außerdem enthält die IU microcodeROM welcher verschiedene JVM-.Befehle enthält
Die Floating-Point-Einheit führt alle mathematischen Funktionen aus. Sie ist optional.
Signale
(vollständige Listung der Signale in [6a],[6b] )
pj_reset
the processor at address 0x00000000.
setup
R1
Resets and starts
pj_reset_out
extended bytecode was executed.
valid
R2
Indicates the reset
pj_clk clock.
In
R0
CLK picoJava core
pj_clk_out
external interfaces.
valid
R1
picoJava’s clock to
pj_irl [3:0] signals.
In
Interrupt exception
pj_nmi interrupt input to the picoJava core.
In
Nonmaskable
pj_boot8 instruction cache fetches.
static
R0
Controls the size of
pj_standby_out
that the processor is in standby mode.
valid
R1
Notifies the system
pj_no_fpu Floating Point Unit.
static
R0
Disables the internal
pj_scan_out
facility.
valid
R0
Provides basic scan
pj_scan_mode
in the core to serial shift.
setup
R1
Switches flip-flops
pj_scan_in
processor core’s scan chain.
setup
R1
Input to the
pj_data_in[31:0] setup
R1
Used during reads.
pj_data_out[31:0] valid
R1
Used during writes.
pj_address[31:0]
with nonmultiplexed 32-bit address bus.
valid
R1
Used to interface
pj_size[1:0]
requested data.
valid
R1
Indicates the size of
pj_type[3:0]
of transaction requested by the Integer Unit.
valid
R1
Indicates the type
pj_tv
transaction to the memory controller.
valid
R1
Starts a new
pj_ack[1:0]
will be driving in same cycle on pj_data_in.
setup
R0
Indicates that data
pj_ale[1:0]
latching.
valid
R0
Enables address
pj_halt
fetching.
setup
R0
Halts instruction
pj_resume
fetching.
setup
R0
Resumes instruction
pj_brk1_sync
detected by the core.
valid
R2
Breakpoint 1
pj_brk2_sync
detected by the core.
valid
R2
Breakpoint 2
pj_in_halt 1
mode (not fetching instructions).
valid
R0
Processor is in halt
pj_inst_complete valid
retrieved (when this signal is high).
R2
An instruction was just
der Befehlssatz
Jede Anweisung der Java VM besteht typischerweise aus einem ein Byte großem Op-Code (256
Instruktionen) plus null oder mehreren Operanden verschiedener Länge. Die Anzahl oder Größe
der Operanden wird dabei implizit durch den Op-Code 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.)
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. [10]
Befehlsfomat
Der Befehlssatz der JavaVM ist typisiert, 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.
opcode
byte
short
Tipush
Tconst
bipush
sipush
Tload
Tstore
Tinc
Taload
Tastore
Tadd
Tsub
Tmul
Tdiv
Trem
Tneg
Tshl
Tshr
Tushr
Tand
Tor
Txor
i2T
l2T
f2T
d2T
Tcmp
Tcmpl
Tcmpg
if_TcmpOP
Treturn
baload
bastore
saload
sastore
i2b
i2s
int
long
float
double
iconst
iconst
iload
Istore
Iinc
iaload
iastore
iadd
isub
imul
idiv
irem
ineg
ishl
ishr
iushr
iand
ior
ixor
lconst
fconst
dconst
aconst
lload
lstore
fload
fstore
Dload
dstore
aload
astore
laload
lastore
ladd
lsub
lmul
ldiv
lrem
lneg
lshl
lshr
lushr
land
lor
lxor
i2l
l2i
f2i
d2i
faload
fastore
fadd
fsub
fmul
fdiv
frem
fneg
fshl
daload
dastore
dadd
dsub
dmul
ddiv
drem
dneg
dshl
i2f
l2f
f2l
d2l
lcmp
fcmpl
fcmpg
i2d
l2d
f2d
d2f
freturn
dreturn
if_icmpOP
ireturn
lretrun
char
reference
caload
aload
castore aastore
asd
dcmpl
dcmpg
if_acmpOP
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).
Befehle > 3Byte:
Opcode
Mnemonic
Size
Description
Cycles
185 (0xb9)
invokeinterface
5
Call an interface method.
Trap
4/6
Extend local variable index by
additional bytes.
Trap
4
Allocate new multidimensional array.
196 (0xc4)
197 (0xc5)
wide
multianewarray
Trap
200 (0xc8)
goto_w
5
Branch always (wide index).
201 (0xc9)
jsr_w
5
Jump subroutine, 4-byte offset. 4
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 %)
4
Gesamtzahl
Anzahl von Befehlen mit der Länge:
2
Bytes
2
41
3 Bytes
Mehr als 3 Bytes
13
82
1
Byte
8
40
3
1
-
10
24
12
27
1
20
7
2
15
2
4
4
3
226
100
10
24
12
5
1
17
7
15
2
141
62
1
1
45
20
19
1
4
3
3
34
15
2
1
2
1
6
3
226 Instruktionen insgesamt teilen sich wie folgt:
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 alle Befehle, die mit den Zielen von
Java nicht vereinbar sind. Das JSM 3 fordert, dass es für den Programmierer nicht ersichtlich
sein darf, wo Objekte im Speicher liegen. Deshalb ist es mit Java-Instruktionen nicht möglich
direkt und willkürlich auf den Speicher zuzugreifen. JVM-Instruktionen arbeiten stattdessen
auf Objektreferenzen, welche keine Rückschlüsse auf die tatsächliche Position der Objekte im
Speicher zulassen. 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, sie hängen von der Beschaffenheit der Hardware ab. Außerdem sollte picoJava auch mit
Code umgehen können der nicht in Java geschrieben wurde wie C oder C++.
Im Befehlssatz der JVM fehlen somit Befehle, die für einen real existierenden Prozessor
unabdingbar sind. Da man den JVM 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 115 Befehle, um die der Befehlssatz der JVM erweitert worden ist, in
fünf funktionalen Gruppen zusammen:
Befehlstyp
Gesamtzahl
Diagnose
Lesen/Schreiben von Registern
Beliebiges Laden/Speichern
Unterstützung anderer
Programmiersprachen
System Software Unterstützung
Gesamtzahl
Anteil (in %)
8
49
35
6
Anzahl von Befehlen mit der
Länge:
1 Byte
2 Bytes
3 Bytes
8
49
26
9
5
1
17
115
100
2
2
2
10
98
85
5
15
13
Für die technische Realisierung dieser Erweiterung stellte sich als Problem:
3
JSM – Java Security Model
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.
Beim Design der JVM wurden zwei ByteCodes für implementations-spezielle Angelegenheiten
reserviert. Diese Bytecodes wurden als EscapeBytes definiert. Stößt die Dekodierlogik beim
Dekodieren eines Befehls auf einen dieser EscapeByte Codes, so weiß sie, dass es sich bei diesem
Befehl um einen erweiterten Befehl handelt da diese Bytekombination von der JVM nicht
benutzt wird. Die Dekodierlogik schaut dann auf das zweite Byte im Befehl, um herauszufinden
um welchen Befehl es sich handelt, bzw. was dieser Befehl macht.
Fast alle erweiterten Befehle beginnen mit einem EscapeByte
Diese 341 Instruktionen, die 226 Basis-, und 115 erweiterten Befehle ergeben den kompletten
Befehlssatz von picoJava.
Der Befehlssatz von picoJava kann nun mehr als der JVM Befehlssatz. 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 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.
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, indem sie die Instructions nach der 'Schwierigkeit
die Befehle zu implementieren' 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.
Gruppe 'einfach (RISC)'
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 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.
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 alloziert werden, was
eine Koordination mit dem 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.
Da die Instruktionen in dieser Kategorie also entweder sehr komplex sind und/oder eine flexible
Implementierung (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.
Kürzel
Operanden
Op-Code
Funktion
instanceof
Klasse 16b.
0xC1
Klassenzugehörigkeit
0xBF
Ausnahmebehandlung
athrow
new
Klasse 16b.
0xBB
ein neues Objekt
newarray
atype 8b.
0xBC
eine neue Tabelle
monitorenter
0xC2
Erwerben der Kontrolle
über ein Objekt
monitorexit
0xC3
Abgeben der Kontrolle
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. Allerdings 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.
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 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 Operationen4 (durch die zusätzlichen Stackzugriffe).
Beispiel / Vergleich (siehe auch [10]):
Registerbasierte Addition
Stackbasierte Addition
ADD R3, R2, R1
ILOAD_1 ILOAD_2 IADD ISTORE_3
Bei einer registerbasierten RISC-Maschine
kann eine Addition in einer einzigen
Instruktion ausgeführt werden. In den
Registern R1 und R2 sind die beiden
Summanden gespeichert, während im
Register R3 die Summe gespeichert werden
soll. Die Operation kann in einem Taktzyklus
ausgeführt werden.
Die stackbasierte Addition benötigt vier
Instruktionen, die jeweils einen Taktzyklus
benötigen. Die ersten beiden Befehle
bewegen die Summanden zum Stack. Diese
werden mit dem dritten Befehl addiert. Das
Resultat wird dann schließlich mit dem
vierten Befehl
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.
4
W. Wulf et al., 'The Design of an Optimizing Compiler', American Elsevier, New York, 1973.
picoJava's Stackorganisation { SEQ picoJava's_Stackorganisation \* ARABIC }
Die picoJava Registermaschine hat 64 Register, welche genutzt werden um die obersten 64
Einträge des Stacks vorzuhalten, zu cachen.
picoJava behandelt diese Register als „circular Buffer“, Ring Puffer, was bedeutet, dass das
unterste Element und das oberste Element im Stack benachbart sind.
Die jeweilige akt. Grenze ist aus den Stackverwaltungsregistern optop (Top-of-Stack Pointer
Register ) , oplim (Minimum Value of Top-of-Stack Register) und sc_bottom ('address of
deepest Stack Cache Entry' register) ermittelbar.
picoJava hält sich damit auch einen Zeiger auf das oberste Element im Stack. (Register optop)
Wenn die Ausführungseinheit Elemente vom Stack holt, schrumpft der Stack und das
StackPointer Register wird inkrementiert.
Wenn die Ausführungseinheit dagegen Elemente auf den Stack legt, wächst der Stack und das
StackPointer Register 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.
Der Stackcache 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 Dribble Manager, der durch ständige im Hintergrund laufende Spill- und
Fill-Operationen 5 den Stackcache mit dem Speicher konsistent hält.
High- und Low Watermarks
Einträge, die (geändert aber) 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-WaterMark), schreibt der Spill-Mechanismus (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 Dribbe-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.
.
Instruction Folding
Zusammenfassen von Befehlen
Eine wichtiges Merkmal des picoJava Caches ist, eine Lösung für das klassische Problem des
ineffektiven Stackzugriffs anzubieten (gegenüber Registermaschine)
Die Ursache für den Performanceverlust liegt in dem 'Operanden und Ergebnisse' vom Stack
holen bzw. auf den Stack schreiben. Hier setzt das InstructionFolding an:
5
Spill – schreibt Einträge vom Register File in den Datencache un schafft so Platz für neue
Werte
Fill – liest Einträge vom Datencache in das Register File und sorgt so für die nächsten
Berechnungen vor
Da der Stack in Wirklichkeit ein Register File mit Random Access ist, hat picoJava's Pipeline
nicht nur Zugriff auf die obersten zwei, sondern Zugriff auf die obersten 64 Stackelemente,
was das Instruction Folding – das Zusammenfassen von Instruktionen – ermöglicht.
Register: Quelle und Ziel in EINEM Befehl angegeben,
Stack: für das gleiche Ergebnis wird Sequenz aus mehreren Befehlen benötigt
In Wirklichkeit liegen die beiden benötigten Operanden bereits auf dem Stack (im Parameterund lokale Variablen-Bereich der akt. Methode). Das Problem ist, daß die ExecutionUnit jeweils
einen Takt benötigt um sie an die Spitze des Stacks zu transportieren, Ebenso schreibt der
'localStore' Befehl das Ergebnis nur an eine andere Stelle des Stacks.
Solange also alle Bewegungen innerhalb der obersten 64 Elemente des Stacks stattfinden
geschieht dies komplett innerhalb des Stacks. Daher ist es möglich diese verschiedenen seriellen
Befehle in einer einzelnen, RISC-artigen Operation zu kombinieren. Datenbewegungen innerhalb
dieses Bereichs bringen keinen Vorteil hinsichtlich Performance, da in Wirklichkeit ein
wahlfreier Zugriff auf die obersten 64 Stack Plätze bereits möglich ist.
Die IntegerUnit kann auf Operanden im lokalen Bereich der aktuellen Methode über die beiden
lese-Ports des StackCaches direkt zugreifen. Genauso kann sie Ergebnisse in den lokalen
Bereich über den Schreib Port des StackCaches direkt zurückschreiben
picoJava geht also folgendermaßen vor:
Basierend auf folgenden Gruppierungsregeln scannt picoJava's Instruction Decoder den
eingehenden Bytecode-Strom und untersucht ihn nach Befehls-Sequenzen welche zu einer
einzelnen Instruktion zusammengefasst werden können. Diese 'faltbaren' Sequenzen können
innerhalb von bis zu 4 Bytecode Instructions
−
lokale Daten direkt vor den Rechenoperationen welche 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, wird daraus eine einzelne, Register-basierte RISCartige Operation erzeugt, in welcher 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.
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
Garbage Collection
Wie erwähnt, fordert das JSM, dass nicht ersichtlich sei wo Objekte im Speicher liegen. Ein
Java Programm kann nur über Objektreferenzen 6 , 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, indem sie dynamisch Speicherbereiche auf dem Heap
6
Datentypen der Java VM: byte, short, int, long, float, double, char, object, returnAddress
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 Speicherbereinigung ist Garbage Collection.
Die JVM hat einen HEAP Bereich, in dem Objektinstanzen, Klassen u. Interfaces liegen (s.o. [{
REF _Ref68058838 \h \* MERGEFORMAT }]). 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
Eine der einfachsten Formen der Garbage Collection ist der so genannte 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.
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.
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, so weist das auf 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.
Diese Form hat den Vorteil, daß 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 (dabei
müssen alle Zeiger auf das Objekt entsprechend angepasst werden).
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 Scans 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 zur 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 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.
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 allen Objekten in diesem Segment , die von
außerhalb 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 scannenden
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. 'Write Barriers'. Der
Schreib-Barriere Mechnismus ermöglicht es, Zeiger von Objekten die sich ausserhalb des zu
scannenden Bereichs befinden, in sog. 'Stores' zu listen um sie effizient daraufhin zu
untersuchen, ob sie auf Objekte innerhalb des zu scannenden Segments verweisen.
In Generationen Systemen werden diese Zeiger als 'inter generational pointers' bezeichnet.
Die 'Write BArrier' ist ein Mechanismus, welcher den GC benachrichtigt sobald Objekt
Referenzen im Speicher abgelegt werden. Der GC kann diese Information dann nutzen um freien
Speicher zu identifizieren. Der GC wird vom 'Core' jeweils via gc_notify' trap informiert
woraufhin dann der Traphandler entsprechend dem genutzten GC Algorithmus in Aktion tritt.
Die 'Write-Barrier Trap' wird ausgelöst sobald eine Referenz irgendeines Objekts oder Arrays
beschrieben wird.
Die genauen Bedingungen wird durch die Flags im 'PSP_GCE' und GC_CONFIG' Register
konfiguriert. Hier können zwei Arten des 'Write Barier' Mechanismus konfiguriert werden [6a]
−
page based write Barrier
Hier wird die relative Speicherlokalisierung durch Object-Referenzen genutzt
−
reference based write Barrier
Hier werden die 2 Bit des reservierten GC_TAG Flags einer Referenz genutzt um zu
entscheiden, wann eine 'gc_notify' Trap ausgelöst wird.
Segmentierung
picoJava bietet eine flexible Methode um Segment Grenzen zu definieren.
Ist ein Segment einmal festgelegt, werden als nächstes alle Zeiger in den 'Zeiger -Stores'
daraufhin geprüft, ob einer der Pointer auf ein Objekt in einem, von sich selbst verschiedenen
Segment verweist. Ist das der Fall so wird eine Unterbrechung, ein 'Trap' ausgelöst 7 und der
GC kann entsprechend reagieren, was hier bedeutet die Referenz in seine entsprechende Liste
aufzunehmen. Der TrapHandler kann abhängig vom genutzten GC Algorithmus verschieden
reagieren.
Die Segmentierung erfolgt 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,
geschieht durch Abgleich des Zeigers/Objekts mit dem Register
Benchmark
Leider sind kaum Benchmarks zu picoJava zu finden. Die Abbildung zeigt picoJava im Vergleich
mit einem 80486- und einem Pentium-Prozessor. Es wurden javac, der Sun Java Compiler, und ein
nicht näher bestimmter Raytracer jeweils 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.
7
gc_notify(type = 0x27)
Dies Testergebnis ist jedoch sehr 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 das Prinzip der kompilierung in Java-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 verkauft 8 .
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 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 [9] bereits erwähnter JavaKoprozessor 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 [8]. 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.
8
deswegen gibt es keinen Abschnitt "Implementierungen"
Quellen 9
[1]
SUN MICROELECTRONICS picoJava Microprocessor Cores
http://www.sun.com/microelectronics/picoJava/
(als Kopie auf CD beiliegend)
[2]
the JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/vmspec/
(als Kopie auf CD beiliegend)
[3]
Harlan McGhan: "picoJava: a direct Execution Engine for Java Bytecode"
http://www.ece.purdue.edu/~arch/seminar/schedules/spring00-pdf/mcghan98.pdf
(als Kopie auf CD beiliegend)
[4]
Inside the Java Virtual Machine
http://www.artima.com/insidejvm/ed2/
(als Kopie auf CD beiliegend)
[5]
Sun Gambles on Java Chips
http://www.byte.com/art/9611/sec6/art2.htm#116st3d2
(als Kopie auf CD beiliegend)
[6]
picoJava2 ProzessorCore Description
https://spacejug.dev.java.net/source/browse/
spacejug/www/resources/Embedded_Java/picoJava/picoJava-II.pdf
(als Kopie auf CD beiliegend)
[6a]
'picoJava-II Programmers Reference Manual'
(als Kopie auf CD beiliegend)
[6b]
'picoJava-II' DataSheet
(als Kopie auf CD beiliegend)
[7]
Suns picoJava:
http://www.sun.com/microelectronics/picoJava/
(als Kopie auf CD beiliegend)
[8]
ARM:
http://www.arm.com/
[8a]
ARM Jazelle Technology
http://www.arm.com/products/esd/jazelle_home.html
[9]
Nazomi:
http://www.nazomi.com/
9
Da sich Internet Adressen schnell ändern liegen die Quellen soweit möglich, insbesondere aber die ohne
Angabe einer URL ([6a], [6b]) auf CD als Kopie bei.
[10]
Script zur Vorlesung 'Rechnerstrukturen' von Prof. Dr. Th. Risse
http://www.weblearn.hs-bremen.de/risse/RST/docs/RST.pdf
und für Interssierte: andere Referate zum Thema Java-Prozessoren:
Bartosz Tyrakowski: 'K-Virtual Machine'
http://www.weblearn.hs-bremen.de/risse/RST/WS02/KVM/KVM.htm
Mathias Pohl: Java Prozesoren
http://www.weblearn.hs-bremen.de/risse/RST/WS02/javaproc.pdf
Markus Müller: Sicherheitsfeatures von Chip-Karten, am Beispiel der JavaCard
http://www.weblearn.hs-bremen.de/risse/RST/ws99/JAVACARD/javacard.htm
Krzysztof Jedon: JAVA
http://www.weblearn.hs-bremen.de/risse/RST/SS98/JAVA/Jedon/Bericht.html
Herbert Stehr, Gordon Stöver : Die Java-Maschine als Soft- und HardwareImplementation
http://www.weblearn.hs-bremen.de/risse/RST/SS97/JavaProc/JAVAPROC.HTM
Roland Klein, Kürsad Afacan: Java Virtual Machine
http://www.weblearn.hs-bremen.de/risse/RST/SS96/Java_VM/Java_VM_.htm
Herunterladen