Karlsruhe, den Roman Kennke

Größe: px
Ab Seite anzeigen:

Download "Karlsruhe, den 30.03.2007. Roman Kennke"

Transkript

1 1 Hiermit erkläre ich, die vorliegende Diplomarbeit selbständig und ohne unzulässige fremde Hilfe angefertigt zu haben. Außer den angegebenen Quellen habe ich keine weiteren Hilfsmittel verwendet. Karlsruhe, den Roman Kennke

2 Inhaltsverzeichnis 1 Einleitung Motivation Aufgabenstellung Zielsetzung Technische Grundlagen Funktionsweise der Jamaica VM Bytecode Interpreter Runtime Memory Management Class Libraries Aufbau des Java-Stacks in der JamaicaVM Der Jamaica Builder Verwendete Software Verwendete Compiler Benchmarks Die Dacapo Benchmark Suite Versionierung Lösungsmöglichkeiten Code Review Gezielte Optimierung der Bytecode-Handler Threaded Dispatching Direct Dispatching Top-Of-Stack Caching Superinstructions Einsatz eines VM-Interpreter-Generators Just In Time Compilation

3 INHALTSVERZEICHNIS 3 4 Umsetzung Initiale Performanz-Analyse Ausgangssituation Analyse der Performanzprobleme Vorgehensweise Interpreter-Generator GForth s Vmgen Format der Eingabedatei Deklaration der Eingabe-Typen Implementierung der Bytecode-Handler Die Ausgabedateien Implementierung eines Interpreters mithilfe des Generators Stackzugriff Konsolidierung der Stack-Zugriffs-Makros Minimierung der Zugriffe auf den cont locals Stack Aufbau des Stackframes Threaded Dispatching Das Problem des while-switch-dispatchers Die Idee des Threaded-Dispatchers Implementierung des Threaded Dispatching Mögliche Ansätze für nicht GCC-Plattformen Superinstructions Funktionsweise von Superinstructions Auswahl von Superinstructions Implementierung Mögliche Probleme Ergebnisse Auswertung der verschiedenen Technologien Optimierung der Stack-Zugriffe Threaded Dispatching Superinstructions Zusammenfassung Ausblick Threading für nicht GCC Compiler Eliminierung des Java Stacks Instruction Prefetching Reduzierung von Sync-Points Direct Dispatching

4 INHALTSVERZEICHNIS Superinstructions

5 Abbildungsverzeichnis 2.1 Schematischer Aufbau einer Java VM Die Datenquellen der Interpreter Schleife Thread Problematik bei Realtime GC Obergrenze für Garbage Collection Arbeit Java Objekte als verlinkte Liste Java Arrays als Baumstruktur Realtime Threads Grundlegende Stack Struktur Aufbau eines Stack-Frames Schematische Funktionsweise des Jamaica Builders Schematische Funktionsweise des Jamaica Compiler Performanz-Vergleich Jamaica Interpreter Referenz-Sicherung auf dem Stack Vereinfachung der Referenz-Sicherung Problem mit areturn bei der Referenz-Sicherung Schematischer Ablauf beim While-Switch-Dispatching Schematischer Ablauf beim Threaded-Dispatching Vergleich der Optimierungstechniken Vergleich der Jamaica Versionen

6 Tabellenverzeichnis 2.1 Zielsysteme und deren C Compiler Populäre Java VM Benchmarks Die Dacapo Benchmarks Performanz von Jamaica 3.0 und Jamaica 3.1beta zum Projektbeginn Häufigkeit von Bytecodes Absolute Performanz von Bytecodehandlern Relative Performanz von Bytecodehandlern Superinstruction Statistik für 2-Bytecode-Sequenzen Superinstruction Statistik für 3-Bytecode-Sequenzen Superinstruction Statistik für 4-Bytecode-Sequenzen Performanz mit und ohne Stack Optimierung Performanz mit und ohne Threaded Dispatching Performanz mit und ohne Superinstructions Performance mit Syncpoints Performance ohne Syncpoints

7 Listings Ein typische Interpreterschleife Implementierung der Bytecodestatistik Aufbau eines Stackframes beim Methodenaufruf Ein while-switch-dispatcher Ein Threaded-Dispatcher Vmgen Quellcode des iadd Handlers Generierter Code des iadd Handlers Implementierung des NEXT P2 Dispatch-Makros für Threaded Dispatching Anlegen der Sprungtabelle für Threaded Dispatching Threaded Dispatching mit Tail Recursion Add Funktion, compiliert nach x86 von GCC Add Funktion, compiliert nach x86 von Microsoft CC Add Funktion, compiliert nach Arm von Microsoft CC Add Funktion, compiliert nach Arm von Ultra-C

8 Zusammenfassung Die JamaicaVM ist eine virtuelle Maschine für die Ausführung von Java Programmen auf eingebetteten und Echtzeit-Systemen. Java Programme liegen normaler als Java Bytecode vor, eine Art Maschinencode, der aber nicht durch den Mikroprozessor des Computers ausgeführt wird, sondern durch eine sogenannte virtualle Maschine. Dies ist ein Programm, das den Bytecode interpretiert und entsprechende Maschineninstruktionen durchführt. Ziel der Diplomarbeit soll es sein, die Ausführungsgeschwindigkeit der JamaicaVM zu verbessern. Die JamaicaVM unterstützt zwei Ausführungsmodi. Zum einen kann sie Java Bytecode precompilieren in Maschinencode der jeweiligen Zielplattform. Alternativ kann sie Java Bytecode interpretieren, d.h. zur Laufzeit die Bytecodes einlesen und entsprechend der Semantik der jeweiligen Bytecode-Instruktionen bestimmte Funktionen aufrufen. Im Rahmen dieser Diplomarbeit soll vor allem die Ausführungsgeschwindigkeit des Interpreters der JamaicaVM verbessert werden. In dieser Diplomarbeit soll zunächst eine systematische Analyse der Performanzprobleme des Interpreters erfolgen. Dazu gehören Benchmarks, statistische Auswertungen (z.b. welche Bytecodes werden wie häufig ausgeführt, wieviele CPU Zyklen werden je Bytecode benötigt etc), sowie Code-Analyse. Aufbauend auf den Ergebnissen dieser Analyse sollen Verbesserungen implementiert werden. Die Analyse und Implementierung sollen sich hauptsächlich auf den Interpreter beschränken. Der Garbage-Collector, Builder und andere Komponenten sollen im Rahmen dieser Diplomarbeit nicht betrachtet werden.

9 Kapitel 1 Einleitung Thema dieser Diplomarbeit ist Performanzoptimierung der JamaicaVM. In diesem Kapitel soll zunächst einmal erläutert werden, was die JamaicaVM ist und warum die Performanz verbessert werden soll. Weiterhin soll ein Rahmen gesteckt werden, was genau bei der Optimierung betrachtet werden soll und was das zu erreichende Ziel ist. 1.1 Motivation Die JamaicaVM ist eine echtzeitfähige Java VM. Sie ist für den Einsatz in eingebetteten Umgebungen konzipiert, wie z.b. Maschinensteuerungen. Eines der zentralen Konzepte der JamaicaVM ist, daß Java Bytecode vor dem Deployment in nativen, zielplattformspezifischen Maschinencode compiliert wird (AOT, Ahead-Of-Time Compilation, im Gegensatz zum üblichen JIT, Just-In-Time Compilation). Dies ermöglicht gute Performanz bei gleichzeitiger Erhaltung der Echtzeitfähigkeit (was bei einem JIT naturgemäß schwierig bis unmöglich ist). Im Gegensatz zur Compilierung des Bytecodes in Maschinencode (Ahead- Of-Time oder Just-In-Time) steht die reine Interpretation des Bytecode. Bei der Interpretation werden die Bytecode-Instruktionen von einem Programm, dem sogenannten Interpreter, ausgelesen und für jede Instruktion eine entsprechende Funktion aufgerufen, die die Semantik der jeweiligen Bytecode- Instruktion implementiert. Obwohl bei der JamaicaVM im Allgemeinen Java Bytecode nach Maschinencode compiliert wird, ist trotzdem ein Bytecode Interpreter notwendig. Dafür gibt es eine Reihe von Gründen: Typischerweise wird nicht eine komplette Anwendung in Nativecode compiliert. Dafür gibt es verschiedene Gründe. Zum einen ist Native- 1

10 KAPITEL 1. EINLEITUNG 2 code deutlich speicherintensiver als Java Bytecode. Schliesslich wurden die JVM Instruktionen als speicherplatzsparend designed, hauptsächlich mit Blick auf die Verwendung als Applet und im Internet im Allgemeinen. Zum anderen hat sich herausgestellt, daß Anwendungen bessere Performanz zeigen, wenn sie nur teilweise übersetzt werden. Die Ursache dafür ist hauptsächlich im verbesserten Inlining zu suchen, aber die Codegröße scheint auch direkt relevant zu sein im Hinblick auf das Prozessorcache und Pipeline Verhalten. Daher werden Anwendungen typischerweise nur teilweise übersetzt (circa 20%-30%), und der Rest wird als Java Bytecode in die ausführbare Binärdatei eingebettet, und auf dem Zielsystem von einem Bytecode Interpreter ausgeführt. Das Übersetzen von Java Bytecode in Maschinencode dauert im Allgemeinen sehr lange. Es ist deutlich bequemer in der Entwicklungsphase Java Bytecode direkt ausführen zu können. In Jamaica gibt es verschiedene Executables die zum Ausführen von Bytecode direkt verwendet werden können, z.b. jamaicavm_slim und jamaicavm. Zum Debuggen von Bytecode muss der Bytecode ebenfalls direkt ausgeführt werden. In Jamaica existieren 2 Methoden zum Debuggen. Zum einen gibt es eine Debug-VM (das Executable jamaicavmdb). Diese führt sehr viele zusätzliche Überprüfungen im Code aus und kann optional eine Menge Debug Ausgaben erzeugen. Dies gibt oftmals einen Hinweis auf mögliche Fehlerursachen. Andererseits gibt es noch die Möglichkeit, einen normalen Java-Debugger (z.b. jdb von Sun s JDK oder den eingebauten Debugger in Eclipse) zu verwenden. Beide Debugger führen Bytecode interpretiert aus und benötigen daher einen Bytecode Interpreter. Potentielle Kunden versuchen im Allgemeinen immer zuerst ihr Programm mal eben auszuprobieren. Dazu bietet sich selbstverständlich der Interpreter an, da dieser genauso gestartet wird, wie die VM von Sun. (Z.B. java -jar App.jar wird zu jamaicavm -jar App.jar). Es ist daher wichtig, daß die Anwendung dann halbwegs flüssig läuft, ansonsten bekommt der Kunde einen schlechten ersten Eindruck von Jamaica, der oftmals entscheidend für den Gesamteindruck ist, auch wenn es sicht im Produktionsbetrieb ganz anders darstellen würde, da die Anwendung dann kompiliert wäre. Selbst für teilweise compilierte Programme ist eine verbesserte Interpreterperformanz interessant, da eben viele Teile nicht compiliert werden.

11 KAPITEL 1. EINLEITUNG 3 Es kann auch vorkommen, daß selbst performanzkritische Programmteile nicht compiliert werden. Beispielsweise kann es passieren, daß beim Erstellen eines Profils bestimmte Routinen nicht verwendet wurden, die aber später im Produktionsbetrieb doch aufgerufen werden, oder daß Programmteile dynamisch nachgeladen werden (also zum Zeitpunkt der Profil-Erstellung noch unbekannt sind). Eine gute Performanz des Jamaica Interpreters ist also trotz Ahead- Of-Time Compilierung nicht unwichtig. Insbesondere die Möglichkeit, Programmmodule dynamisch hinzuladen zu können macht den Einsatz von Java attraktiv, aber gleichzeitig auch schwierig für Jamaica. Ohne eine gute Performanz des Interpreters ist dies nur sehr unpraktikabel. 1.2 Aufgabenstellung Im Rahmen dieser Diplomarbeit soll die Performanz des Interpreters der JamaicaVM signifikant verbessert werden. Das bedeutet, daß die Ausführungsgeschwindigkeit von Java Programmen (JVM Bytecode) messbar schneller wird. Dazu gehören als Teilaufgabe eine Analyse und Einordung der bestehenden Performanzprobleme, Erarbeitung von möglichen Vorgehensweisen zur Verbesserung der Performanz, die Auswahl und schliesslich die Umsetzung geeigneter Techniken. Es soll dabei im Wesentlichen nur der Interpreter betrachtet werden. Performanzprobleme in anderen Teilen der VM (z.b. im Garbage Collector oder im compilierten Code) sind zwar interessant zu betrachten, würden aber den Rahmen dieser Diplomarbeit sprengen. Besonderes Augenmerk sollte auch auf speziellere Zielsysteme mit besonderen Eigenheiten (z.b. RISC Prozessor, besonders kleiner Prozessor-Cache u.ä.) gelegt werden. 1.3 Zielsetzung Das Ziel dieser Diplomarbeit soll sein, die Interpreterperformanz um ca % zu steigern. Dabei soll es um möglichst realitätsnahe Anwendungen gehen, und nicht um spezielle Benchmarks, die besondere Effekte besonders hervorheben. Eine geeignete Methode zur Performanz-Bestimmung muss also im Vorfeld erarbeitet werden. Die Performanzmessungen sollen sich allerdings auf rein interpretierten Code beziehen, es wird daher im Rahmen dieser Diplomarbeit nicht die Ahead-Of-Time Compiler Funktionalität der JamaicaVM verwendet werden.

12 Kapitel 2 Technische Grundlagen Ein grundlegendes Verständnis des Aufbaus einer Java VM im Allgemeinen und der JamaicaVM im Speziellen ist Vorraussetzung für eine erfolgreiche Optimierung des Interpreters. Weiterhin müssen gewisse technische Randbedingungen und die verwendete Software betrachtet werden. Dieses Kapitel widmet sich daher ganz der Erläuterung aller technischen Grundlagen. 2.1 Funktionsweise der Jamaica VM Im Folgenden soll kurz erläutert werden, wie eine Java VM im Allgemeinen und die Jamaica VM im speziellen ungefähr funktioniert. Das Verständnis für die Funktionsweise ist wichtig um das Vorgehen bei der Performanzoptimierung zu verstehen. Ich werde nur den Interpreter genauer erläutern, da es im Rahmen dieser Diplomarbeit nur um den Interpreter geht. Ich werde die anderen Teile der VM nur kurz anschneiden. Eine Java VM besteht im Wesentlichen aus vier Komponenten, dem Bytecode Interpreter, der Memory Management Einheit (Garbage Collector), der Runtime und den Class Libraries. Der Interpreter bildet dabei den ausführenden Kern der VM, der Garbage Collector ist für die Speicherverwaltung zuständig, die Runtime bildet die Schnittstelle zur Native Plattform und die Class Libraries die Schnittstelle zur Applikation (API). Abbildung 2.1 veranschaulicht diesen Aufbau Bytecode Interpreter Der Bytecode Interpreter ist mehr oder weniger der Kern der VM. Er ist zuständig für die Interpretation des Programm Bytecode Streams, und die 4

13 KAPITEL 2. TECHNISCHE GRUNDLAGEN 5 Ausführung der dazugehörigen Operationen. Der Interpreter übernimmt sozusagen die Rolle des Prozessors für den Bytecode. Datenquellen Der Interpreter verarbeitet Daten aus verschiedenen Quellen. Da wäre zunächst der Bytecode-Stream. Von diesem werden die Bytecode Instruktionen ausgelesen, die letztendlich interpretiert werden. Weiterhin sind für viele Instruktionen feste Parameter enthalten, z.b. ein Wert, der auf den Stack geladen werden soll, oder eine Sprungadresse. Bytecode- Streams sind grundsätzlich nur lesbar, es werden nur Bytecodes und Parameter gelesen, aber nicht geschrieben. Es existiert ein Bytecode-Stream für jede Methode jeder geladenen Klasse. Alle Bytecode-Streams werden auf dem Java-Heap abgelegt, entweder in der Form eines sogenannten contiguous array (zusammenhängender Speicher), oder in Form eines Java array, eine Datenstruktur, die möglicherweise nicht zusammenhängend ist, sondern in einer Baumstruktur vorliegt. Das letztere ist - kurz gesagt - ein Feature der Speicherverwaltung um Echtzeitfähigkeit garantieren zu können. Dazu mehr im Abschnitt Speicherverwaltung. Die andere Datenquelle, die relevant für den Interpreter ist, ist der sogenannte Stack. Der Stack ist ein Datenspeicher, auf dem Operanden, lokale Variablen und Metadaten zu Methodenaufrufen (Frames) abgelegt werden. Jeder Java Thread besitzt genau einen Stack. Die Stacks selbst sind Datenstrukturen, die auf dem Java Heap liegen und von der Speicherverwaltung verwaltet werden. Der Interpreter greift sowohl lesend als auch schreibend auf den Stack zu. Class Libraries JNI Runtime Interpreter Memory Management Native Platform Abbildung 2.1: Schematischer Aufbau einer Java VM

14 KAPITEL 2. TECHNISCHE GRUNDLAGEN 6 Dispatching Der Dispatcher ist der Teil des Interpreters, der die Bytecode-Instruktionen aus dem Stream liest und die dazugehörigen Bytecode Handler aufruft. Ein Bytecode Handler ist eine Funktion, die die Funktionalität des dazugehörigen Bytecodes implementiert. Naive Implementierungen verwenden zum Beispiel eine Schleife mit einem Switch-Statement wie im folgenden Code-Fragment: Der Dispatcher ist kritisch für gute Performanz, da der Dispatch Code für jede einzelne Bytecode Instruktion ausgeführt wird. Es ist daher wichtig, den Dispatch Overhead so klein wie möglich zu halten. Der obige Ansatz z.b. bereitet einige Probleme wenn man gute Performanz benötigt. Teil der Diplomarbeit wird es sein, diese Probleme zu untersuchen und Lösungsansätze dafür zu entwerfen. Bytecode Handler Die Bytecode Handler schliesslich implementieren die Logik für jede Bytecode Instruktion. Ein Bytecode Handler ist im wesentlichen eine Funktion, die die Semantik des jeweiligen Opcodes implementiert. Eine mögliche Implementierung für den Java Opcode iadd könnte z.b. so aussehen: Interpreter Loop pop push read Bytecode Stream Stacks Abbildung 2.2: Die Datenquellen der Interpreter Schleife

15 KAPITEL 2. TECHNISCHE GRUNDLAGEN 7 void do bc iadd ( ) { i n t 3 2 v1, v2, r ; v2 = stack[ stackpointer ] ; v1 = stack[ stackpointer ] ; r = v1 + v2 ; stack [ stackpointer ] = r ; } Selbstverständlich ist es für die Performanz des Interpreters extrem wichtig, wie effizient die einzelnen Bytecode Handler implementiert sind. Dies zu untersuchen und gegebenenfalls zu verbessern wird ebenfalls Teil dieser Diplomarbeit sein Runtime Als Runtime werden unterstützende Funktionen bezeichnet, die von den Bytecodehandlern aufgerufen werden, um bestimmte komplexere Funktionalität zu implementieren. Dazu gehören z.b. das Erzeugen von Objekten, das Laden von Klassen, das Werfen von Exceptions und vieles mehr. Insbesondere bildet die Runtime eine Schnittstelle zur darunterliegenden Hardware- und Betriebssystem-Plattform. Ein weiterer wichtiger Teil der Runtime bilden die verschiedenen Schnittstellen nach außen. Da wären insbesondere JNI zu nennen - das Java Native Interface -, das es ermöglicht Java Methoden in nicht-java Programmiersprachen zu implementieren (hauptsächlich C und while ( pc >= 0) { switch ( bytecodes++) { case op1 : // Bytecode Handler f ü r op1. break ; case op2 : // Bytecode Handler f ü r op2. break ;... case opn : // Bytecode Handler f ü r opn. break ; } } Listing 2.1.1: Ein typische Interpreterschleife

16 KAPITEL 2. TECHNISCHE GRUNDLAGEN 8 C++). Weiterhin wären da die Debug und Toolschnittstellen JVM/TI (Java VM Tool Interface), JVM/DI (Java VM Debug Interface, ein Subset des JV- M/TI), sowie JDWP (Java Debug Wire Protocol). Die Runtime Funktionen stehen nicht im Mittelpunkt dieser Diplomarbeit, sind aber vermutlich an manchen Stellen peripher von verschiedenen Anpassungen betroffen Memory Management Der Speicherverwaltung kommt bei Java eine zentrale Rolle zu. Eine der grundlegenden Eigenschaften von Java ist es, daß Entwickler von Java Programmen sich nicht explizit um die Freigabe von Objekten auf dem Java Heap kümmern müssen. Stattdessen sorgt ein sogenannter Garbage Collector für das Freigeben von Objekten, die nicht mehr referenziert werden. Weiterhin existieren Funktionen zur Allokation von Objekten. Diese bilden allerdings keine separate Einheit sondern sind Teil des Garbage Collectors. Daher implementiert der Garbage Collector im weiteren Sinne die Speicherverwaltung einer Java VM. Die folgenden Informationen finden sich in detaillierter Form in [10]. Eine wesentliche Eigenschaft des JamaicaVM Garbage Collector (im folgenden GC genannt) ist seine Echtzeitfähigkeit. Das größte Problem bei herkömmlichen GCs bezüglich Echtzeitfähigkeit ist, daß sie in einem separaten Thread ausgeführt werden. Sie müssen daher in regelmäßigen Abständen aufgerufen werden um unbenutzte Objekte einzusammeln. Dies ist nötig, damit die Anwendung Speicher allozieren kann. Allerdings ist diese Garbage Collection Arbeit nicht vorhersehbar, sie kann mehr oder weniger unvorhersehbar jeden anderen Task unterbrechen. Abbildung 2.3 veranschaulicht dieses Problem. Abbildung 2.3: Thread Problematik bei Realtime GC Ein weiteres Problem ist die Handhabung von unterschiedlich großen Objekten. Es kann keine Garantie bezüglich der Ausführungszeit gegeben wer-

17 KAPITEL 2. TECHNISCHE GRUNDLAGEN 9 den, wenn die Einheiten, mit denen der Garbage Collector umgeht verschieden groß sind. Um diese Probleme zu lösen implementiert der JamaicaVM Garbage Collector verschiedene Techniken: Garbage Collection im Anwendungsthread Die Garbage Collection Arbeit wird beim JamaicaVM GC nicht wie bei den meisten VMs üblich in einem separaten Thread erledigt, sondern direkt im jeweiligen Anwendungsthread. Genauer gesagt, wird Garbage Collection angestoßen, wenn immer Objekte alloziert werden. Dabei wird jeweils eine genau vorgeschriebene Menge Garbage Collection Arbeit durchgeführt. Die genaue Menge an Garbage Collection Arbeit ist abhängig von der Auslastung des Heap Speichers. Je voller der Heap ist, desto mehr Garbage Collection Arbeit wird pro Allokation durchgeführt. Die Obergrenze der Garbage Collection Arbeit ist durch eine Funktion implementiert, die in Abbildung 2.4 dargestellt ist. Abbildung 2.4: Obergrenze für Garbage Collection Arbeit Auf diese Weise wird sichergestellt, daß: 1. Die Anwendungsthreads nicht durch GC Threads unterbrochen werden.

18 KAPITEL 2. TECHNISCHE GRUNDLAGEN Die Worst Case Execution Time für Allokationen vorhersehbar (und nicht zu hoch) ist. Aufbrechen von Objektstrukturen in Blöcke In den meisten Java VMs werden Java-Objekte durch zusammenhängenden Speicher repräsentiert, der groß genug ist, um die Felder des Objektes und einige Meta-Daten unterzubringen. Das Problem mit diesem Ansatz ist, daß die Ausführungszeit des Garbage Collectors nicht 100% deterministisch ist. Selbst wenn er inkrementell arbeitet (wie der Jamaica GC) kann es passieren, daß besonders große Objekte den GC unvorhersehbar aufhalten. Das Problem wird verstärkt wenn der GC sogenannte Compaction ausführen muss, um Heap-Fragmentierung vorzubeugen oder zu beseitigen. Dann müssen Objekte relokiert werden, was ein relativ aufwendiger Prozess ist, vor allem, weil alle Referenzen auf ein solches Objekt erneuert werden müssen. Aus diesem Grund werden Objekte in der JamaicaVM nicht als zusammenhängende Speicherbereiche repräsentiert, sondern als eine Datenstruktur aus sogenannten Blöcken. Diese Blöcke haben eine vorgegebene feste Größe (z.b. 32 Wörter). Damit kann der Heap als ein Speicherbereich betrachtet werden, der aus lauter gleich großen Objekten besteht. Fragmentierung und Compaction spielen damit keine Rolle mehr. Diese spezielle Repräsentation bringt natürlich einen gewissen Overhead mit sich, sowohl vom Speicherverbrauch her, als auch von der Verarbeitungszeit. Dafür bekommt man allerdings garantiertes Echtzeitverhalten für normale Java Anwendungen. Es gibt zwei verschiedene Repräsentationen von Objekten. Auf der einen Seite wären das Java-Arrays, und auf der anderen Seite normale Java Objekte. Java Objekte werden als verlinkte Liste von Blöcken repräsentiert (Abbildung 2.5, während Arrays als Baumstrukture dargestellt werden (Abbildung 2.6). Abbildung 2.5: Java Objekte als verlinkte Liste

19 KAPITEL 2. TECHNISCHE GRUNDLAGEN 11 Implementierung der RTSJ Spezifikation Um Echtzeitfähigkeit auch in herkömmlicheren VMs anbieten zu können, hat Sun eine Erweiterung entwickelt, die letztendlich in den ersten JSR und damit in die Realtime Specification for Java [9] gemündet ist. Im Wesentlichen ist dies eine Erweiterung der API Spezifikation, um Realtime Features bereitzustellen. Das Garbage Collection Problem wird hier völlig anders gelöst. Kurz gesagt ist es mit RTSJ möglich, spezielle Heaps bereitzustellen, die nicht unter der Kontrolle des Garbage Collectors stehen. Das bedeutet, daß man Objekte auf diesen Heaps manuell verwalten muss. Um die Unterbrechbarkeit von Anwendungsthreads durch den GC in den Griff zu bekommen, besteht die Möglichkeit, sogenannte Realtime Threads zu verwenden. Diese können grundsätzlich nicht durch den Garbage Collector unterbrochen werden (siehe Abbildung 2.7). Mithilfe dieser Techniken kann man auch mit herkömmlichen VMs Echtzeitfähigkeit implementieren. Allerdings wird hierbei erheblicher Aufwand auf der Seite des Entwicklers erwartet. Obwohl die JamaicaVM andere bessere Wege zur Echtzeitfähigkeit anbietet, ist es selbstverständlich auch möglich, RTSJ zu verwenden. RTSJ ist ein Teil der API der JamaicaVM. Abbildung 2.6: Java Arrays als Baumstruktur

20 KAPITEL 2. TECHNISCHE GRUNDLAGEN Class Libraries Als Klassenbibliothek verwendet die JamaicaVM zum größtenteil den Code von GNU Classpath [6]. Ausnahmen sind die Pakete java.lang.***, für welches in JamaicaVM eine komplett separate Implementierung existiert, die Realtime Erweiterung RTSJ (javax.realtime) sowie Plattform spezifische Implementierungsklassen, z.b. für die AWT Backends. Viele Klassen der Klassenbibliothek stellen Schnittstellen zu Funktionen des Betriebssystems dar, vor allem die Klassen in den Core Packages java.lang, java.io, java.net, java.nio, java.awt. Diese werden im Allgemeinen durch Native- Funktionen implementiert, die über JNI oder JBI (ein Jamaica-spezifisches Äquivalent zu JNI) aufgerufen werden. Die Class Libraries spielen im Zusammenhang mit dieser Diplomarbeit keine große Rolle und sollen daher nicht weitergehend betrachtet werden Aufbau des Java-Stacks in der JamaicaVM Eine Java Virtual Machine ist eine Stack-basierte Maschine (im Gegensatz zu Register-basierten Maschinen wie sie z.b. von vielen Prozessoren implementiert werden). Der Java Stack ist daher eine zentrale Datenstruktur in einer Java VM. Auf ihm werden lokale Variablen, Operanden für die Bytecode Instruktionen sowie zusätzliche Informationen zu Methodenaufrufen gespeichert. Es existiert pro Thread ein Java Stack. Wenn in der Folge von Stack die Rede ist, ist damit dieser Java Stack gemeint. Es gibt noch mehr Stacks in der virtuellen Maschine, insbesondere der native Stack, das ist der Aufrufstack des Betriebssystem für native Funktionen. Um die Optimierungsmöglichkeiten beim Stackzugriff zu verstehen muss zunächst einmal erläutert werden, wie der Stack aufgebaut ist und welche Besonderheiten es in der JamaicaVM gibt. Abbildung 2.7: Realtime Threads

21 KAPITEL 2. TECHNISCHE GRUNDLAGEN 13 Grundsätzlich ist der Stack ein Array von jamaica_int32 Wörtern. Diese sind unterteilt in eine Menge sogenannter Stack-Frames. Ein Stack-Frame ist die grundlegende Struktur auf dem Stack, alle Stack-Operationen sind letztendlich Operationen auf einem Stack-Frame. index Frame n Frame (n-1) Frame 1 ct->javastack Frame 0 Abbildung 2.8: Grundlegende Stack Struktur Pro Methodenaufruf wird ein Frame auf dem Stack erzeugt. Ein Frame bildet die grundlegende Struktur für die VM für die Abarbeitung einer Java Methode. In einem Frame werden Argumente und lokale Variablen der Methode gespeichert sowie Operanden für die Bytecode Instruktionen. Zusätzlich beherbergt ein Frame zusätzliche Informationen wie z.b. eine Referenz zu der zugehörigen Methode und zum darunterliegenden Frame. 2.2 Der Jamaica Builder Der Jamaica Builder ist neben der eigentlichen VM ein wesentlicher Bestandteil von Jamaica. Er dient dem Übersetzen von Java Bytecode nach Maschinen-Code. Im Gegensatz zu Just-In-Time Compilation wird allerdings dieser Compilierungsschritt nicht zur Laufzeit durchgeführt, sondern zur Deploy-Zeit, also wenn die Anwendung auf dem Zielsystem vorbereitet wird. Dies macht insofern Sinn, als das die Anwendung nicht wie bei normalen Desktop Anwendung auf beliebigen Computerplattformen, Betriebssystemen und virtuellen Maschinen laufen soll, sondern das genaue Zielsystem mit allen seinen Komponenten bekannt ist.

22 KAPITEL 2. TECHNISCHE GRUNDLAGEN 14 Der grundsätzliche Aufbau und die Funktionsweise des Jamaica Builders ist in Abbildung 2.10 illustriert. Die Java Quelldateien werden zunächst wie gewohnt mit dem JDK Java Compiler (javac) in Java Bytecode übersetzt. (Oftmals liegt ein Programm schon/nur in Bytecode vor, dann entfällt dieser Schritt). Der Java Bytecode wird dann vom eigentlichen Builder übersetzt in C Code, der in einer temporären.c Datei gespeichert wird. Dieser C Code wird dann mit einem plattformspezifischen C Compiler in eine Objektdatei compiliert, und schliesslich zusammen mit der VM Library und den Objektdateien der anderen Klassen und Packages des Programms gelinkt in ein ausführbares Executable. Die detaillierte Funktionsweise des Jamaica Compilers (der die.class Dateien in.c Dateien compiliert) ist in Abbildung 2.11 dargestellt. Der Class File Loader liesst zunächst die Klassendateien ein. Diese werden vom Static Analyser analysiert, in diesem Schritt werden viele Informationen gesammelt, die später der Static Compiler, der Bytecode Optimizer und der Smart Linker benötigen, z.b. Klassen-Hierarchie-Informationen, Field-Offsets und vieles mehr. Klassen und Methoden, die tatsächlich kompiliert werden sollen werden vom Static Compiler in C Code umgewandelt. Code, der nicht kompiliert, sondern als Java Bytecode eingebettet werden soll, wird in einem Zwischenschritt vom Bytecode Optimizer modifiziert, sodaß er später schneller ausgeführt werden kann. Der Smart Linker bringt die Code Fragmente zusammen, und vor allem entfernt er sehr viel Code, z.b. nicht benötigte Methoden und Klassen. Dies reduiziert die Code-Größe beträchtlich. Im Class Compaction Schritt wird die Code Größe weiter reduziert, indem die Abbildung 2.9: Aufbau eines Stack-Frames

23 KAPITEL 2. TECHNISCHE GRUNDLAGEN 15 Daten in ein weniger speicherintensives Format konvertiert werden. Z.B. werden redundante konstante Strings entfernt und vieles mehr. Der Class Writer schliesslich schreibt die resultierende C Datei, die dann vom C Compiler weiterverarbeitet wird. 2.3 Verwendete Software Eine wichtige Rolle bei der Entwicklung am Jamaica Interpreter spielt die Software, die im Rahmen dieses Projekts zur Verwendung kommen soll. Dies wird in den folgenden Abschnitten erläutert werden. Wichtig sind hier vor allem die verwendeten Compiler sowie die Benchmarks, die zur Bestimmung der Performanz verwendet werden Verwendete Compiler Die JamaicaVM unterstützt eine Reihe von Zielsystemen, z.b. Linux, Vx- Works, OS9, WindowsCE, nur um die wichtigsten zu nennen. Für die Übersetzung von JamaicaVM und von Anwendungen nach Maschinencode der jeweiligen Zielplattform kommen eine Reihe verschiedener C Compiler zum Einsatz. Die wichtigsten wären hier der GCC [4] in vielen verschiedenen Versionen (von 2.9 bis 4.1), der Microsoft C-Compiler für WindowsCE sowie der Ultra-C Compiler für OS/9. Java Quellcode.java javac Java Bytecode.class Builder Java class data.c C-Compiler Java class data.o Jamaica VM Library Linker Executable Abbildung 2.10: Schematische Funktionsweise des Jamaica Builders

24 KAPITEL 2. TECHNISCHE GRUNDLAGEN 16 Java Class Files.class Class file loader Static Analyser + Static GC Profiling Information Static Compiler Bytecode Optimizer Smart Linker Class Compaction Class Writer Java class Data.c Abbildung 2.11: Schematische Funktionsweise des Jamaica Compiler

25 KAPITEL 2. TECHNISCHE GRUNDLAGEN 17 Bei der Implementierung der Interpreter Optimierungen muss sehr darauf geachtet werden, welche C-Compiler verwendet werden und welche Features diese Unterstützen. Bisher wurde bei der Implementierung jeglicher C - Funktion streng darauf geachtet, nur ANSI-C zu verwenden. Es gibt allerdings interessante C Erweiterungen, die die Implementierung mancher Technologien vereinfachen, bzw. überhaupt erst ermöglichen. Hier muss jeweils möglichst eine ANSI-C Implementierung bevorzugt werden, oder mindestens eine alternative (möglicherweise weniger performante) ANSI-C Implementierung bereitgestellt werden, für Plattformen, die die jeweilige Erweiterung nicht unterstützt. Dies kann z.b. über bedingte Compilierung realisiert werden. Tabelle 2.1 listet die wichtigsten Zielsysteme und deren verwendete Compiler auf. Im Rahmen dieser Diplomarbeit wird wohl hauptsächlich der GCC verwendet werden, schlicht aus dem Grund, weil dieser bei Linux mitgeliefert wird und weil damit leicht gearbeitet werden kann. Die exotischeren Compiler werden nur für Zielsysteme verwendet, was bedeuten würde, daß bei jedem Test ein Programm für das Zielsystem compiliert werden muss. Zielsystem Compiler Besonderheiten Linux GCC VxWorks GCC v2.95 keine Label-Pointer WindowsCE Microsoft VC keine Label-Pointer OS9 Ultra C keine Label-Pointer Tabelle 2.1: Zielsysteme und deren C Compiler Benchmarks Im allen Phasen des Projektes sollen Performanz-Messungen vorgenommen werden. In der Analysephase dient es dazu festzustellen, wo die Probleme liegen, während der Implementierungsphase zur Dokumentation des Fortschritts und der Einschätzung des Effizienz der angewendeten Optimierungen. Abschliessend soll eine weitere Analyse zum Vergleich mit dem Beginn des Projektes und zur Beurteilung des Projekterfolgs dienen. Zur Einschätzung der Performanz des Jamaica-Interpreters sollen Testprogramme mit folgenden Eigenschaften dienen: Testläufe müssen vergleichbar sein. Das heisst, das zwei Testläufe mit derselben Interpretersoftware auch die gleichen Resultate liefert. Einflüsse, die die Testresultate unvorhersehbar beeinflussen müssen vermieden werden.

26 KAPITEL 2. TECHNISCHE GRUNDLAGEN 18 Testprogramme müssen realistisch sein. Benchmarks, die bestimmte Eigenheiten besonders verstärken und andere vernachlässigen verzerren das Gesamtbild. Idealerweise sollten real verwendete Programme zum Einsatz kommen. Dadurch soll sichergestellt sein, daß die Performanz- Verbesserungen auch im realen Einsatz voll zur Geltung kommen, und nicht nur im Laborbetrieb signifikant sind. Der Benchmark muss konstenlos verfügbar sein. Es wäre ausserdem hilfreich, wenn der Benchmark mit Quellcode (unter einer Open Source Lizenz) verfügbar wäre. In Tabelle 2.2 sind einige populäre Benchmarks aufgelistet. Ein offensichtliches Problem bei den bekannten Java VM Benchmarks ist ihre Verfügbarkeit. Offenbar sind die meisten ehemals populären Benchmarks entweder nicht mehr auffindbar (JMark und Caffeine) oder nicht ohne Kosten verfügbar (SpecJVM und Volcano). Der einzige Benchmark, der sowohl aktuell gepflegt, kostenlos verfügbar ist, und noch dazu reale Anwendungsfälle testet ist der DaCapo Benchmark. Benchmark Dacapo SpecJVM98 JMark Caffeine Mark Volcano Mark Bemerkungen Open Source Keine neuen Releases, kostenpflichtig Keine reale Anwendungen, Projekt nicht auffindbar Keine neuen Releases, Projekt nicht auffindbar Keine neuen Releases, kein Sourcecode Tabelle 2.2: Populäre Java VM Benchmarks Die Dacapo Benchmark Suite Eine Benchmark-Suite, die die obengenannten Kriterien erfüllt ist die Dacapo Benchmark Suite. Diese wird für die meisten Performanz-Messungen im Rahmen dieser Diplomarbeit zum Einsatz kommen. Die anderen Benchmarks sind aus verschiedenen Gründen nicht praktikabel. Manche sind schlichtweg nicht oder nicht kostenlos verfügbar, andere Benchmarks werden nicht mehr weiterentwickelt und liefern keinen Sourcecode mit aus (was sehr nachteilig beim Debuggen sein kann). In Dacapo sind eine Menge von Testläufen definiert. Jeder dieser Testläufe führt ein bestimmtes häufig verwendetes Java Programm und/oder Bibliothek aus, jedes mit leicht unterschiedlichem Fokus im Laufzeitverhalten. Die verschiedenen Einzelbenchmarks sind in Tabelle 2.3 aufgelistet.

27 KAPITEL 2. TECHNISCHE GRUNDLAGEN 19 Testname Beschreibung Besonderheiten antlr Parser-Generator Ok. bloat Bytecode level optimization Ok. and Analysis Tool chart JfreeChart chart plotting Benötigt Java2D, nicht toolkit möglich. eclipse Eclipse Platform Wirft InternalError, sehr wahrscheinlich ein Class- Loader Problem. fop Print Formatter Ok. hsqldb SQL Database engine Ok. jython Python interpreter Ok. luindex Lucene indexing engine Complains about not beeing able to create directory. lusearch Lucene search engine Complains about not beeing able to create directory. pmd Source code analyzer Ok. xalan XSLT processor ExcepionInInitializerError Tabelle 2.3: Die Dacapo Benchmarks

28 KAPITEL 2. TECHNISCHE GRUNDLAGEN 20 Einige Benchmarks bereiten offenbar Probleme mit der JamaicaVM. Da wäre zunächst der chart Benchmark, dieser zeichnet verschiedene Diagramme per Java2D in BufferedImages. JamaicaVM unterstützt noch kein Java2D, daher wird dieser Benchmark nicht verwendet werden. Von den anderen Benchmarks machen eclipse, luindex, lusearch sowie xalan Probleme. Es wird in einem vorbereitenden Schritt nötig sein, die Probleme soweit wie möglich zu beheben. Die Dacapo Benchmark Suite definiert ausserdem für jedes Benchmark 3 verschiedene Konfigurationen: Small Ein sehr kurzer Testlauf. Dauer im Minutenbereich. Dieser kann im Laufe der Entwicklung zum Schnellen Testen verwendet werden. Die Ergebnisse liefern schon deutliche Hinweise auf Performanz-Veränderungen, ist aber für brauchbare Statistiken zu ungenau. Die Ergebnisse dieser Tests sollen daher hier auch nicht extra erwähnt werden. Im Einzelfall kann bei Besonderheiten eine Ausnahme gemacht werden. Default Ein Testlauf mittleren Umfangs. Die Dauer bewegt sich hier im Bereich von (?) Minuten. Diese Konfiguration soll für Performanzmessungen dienen, die jeweils vor und nach Abschluss eines Teilprojekts vorgenommen werden. Die Messungen sind schon sehr genau und aussagekräftig. Large Ein sehr langer Testlauf. Die Dauer bewegt sich hier im Stundenbereich. Diese Konfiguration soll nur für die initiale und abschliessende Analyse zum Einsatz kommen, um die Gesamtergebnisse des ganzen Projektes so genau wie möglich zu erfassen. Wenn nötig, kann evtl. ein Zwischenbericht erzeugt werden. Zu Beginn der Diplomarbeit war gerade ein neuer Release von DaCapo in Arbeit. Ich habe meine ersten Versuche mit dem Release Candidate 4 Release gemacht. Offenbar gab es einen Fehler in DaCapo der mit JamaicaVM (und anderen GNU Classpath-basierten VMs) manche Tests fehlschlagen liess. Nach einer Nachricht von mir auf der DaCapo Mailingliste wurde das Problem prompt behoben und RC5 funktioniert weitestgehened. Verwendet wird letztendlich die finale Version MR1. Die verschiedenen Konfigurationen der Dacapo Benchmark sind sehr nützlich. Es werden im Laufe der Entwicklung die Small und Default-Sets zum Testen eingesetzt werden, und die Default- und Large-Sets für ausführliche Performanz-Analysen zu Beginn und am Ende des Projekts.

29 KAPITEL 2. TECHNISCHE GRUNDLAGEN Versionierung DaCapo Benchmark Für diese Diplomarbeit wird die Version MR1 der Dacapo Benchmark Suite verwendet. Die Dacapo Benchmark Suite wird im DaCapo Paper [1] und ausführlicher im DaCapo Technical Report [2] beschrieben. JamaicaVM Die Diplomarbeit basiert auf der Entwicklungsversion von Jamaica zum Zeitpunkt des Projektbeginns. Das ist im Wesentlichen Jamaica VM 3.1 alpha vom 12. Dezember Die Entwicklungen im Rahmen dieses Projekts werden in einem separaten Entwicklungs-Zweig vorgenommen, damit andere Änderungen an der JamaicaVM nicht das Ergebnis mitverfälschen.

30 Kapitel 3 Lösungsmöglichkeiten Zur Verbesserung der Interpreter Performanz kann man verschiedene Ansätze wählen. Auf der einen Seite gibt es eine Reihe von bewährten Technologien die z.b. in anderen Java VMs eingesetzt werden. Von diesen soll hier eine Auswahl vorgestellt sowie deren Umsetzbarkeit im Kontext der JamaicaVM diskutiert werden. Auf der anderen Seite gibt es einige Ansätze, die speziell im Kontext der JamaicaVM Sinn machen. Auch diese sollen hier vorgestellt werden 3.1 Code Review Eine naheliegende Methode zur Verbesserung von Performanz ist eine direkte manuelle Analyse des Programm-Codes. Es ist möglich, daß dadurch verschiedene Fehl-Konzeptionen oder schlechte Umsetzungen im Interpreter- Code gefunden werden können. Manueller Code-Review ist auch ansonsten sehr sinnvoll, wenn nicht sogar unvermeidlich, um überhaupt erst einmal mit dem Programm-Code des Interpreters vertraut zu werden. 3.2 Gezielte Optimierung der Bytecode-Handler Die Bytecode Handler spielen eine zentrale Rolle im Interpreter. Ineffiziente Implementierungen derselben müssen vermieden werden. Es muss hier allerdings gezielt vorgegangen werden. Es scheint hier sehr sinnvoll zu sein, zunächst einmal das Laufzeit-Verhalten der unterschiedlichen Bytecode-Handler zu analysieren. Anhand von Statistiken über die Laufzeit der unterschiedlichen Bytecode-Handler können kritische Handler identifiziert und gezielt optimiert werden. 22

31 KAPITEL 3. LÖSUNGSMÖGLICHKEITEN 23 Eine solche Optimierung ist sehr naheliegend. Eine Statistik über das Laufzeit-Verhalten der Verschiedenen Bytecode-Handler ist auch in anderen Zusammenhängen nützlich. Falls im Zusammenhang mit dem Code Review Probleme in performanz-kritischen Bytecode-Handlern identifiziert werden können, dann sollten diese auch behoben werden. 3.3 Threaded Dispatching Threaded Dispatching ist eine Optimierung des Dispatch Codes. Der normale (naive) Ansatz zum Bytecode-Dispatchen ist eine while-schleife mit einem eingebetteten Switch Statement. Dieses Switch Statement führt zu jeder Bytecode Instruktion den entsprechenden Bytecode Handler aus, der die Semantik dieses Bytecodes implementiert. Ein solches Switch Statement wird normalerweise vom C Compiler in einen indizierten Sprungbefehl übersetzt. Trotzdem ist der Overhead für das Dispatchen im Vergleich zu den meisten Operationen doch noch sehr groß. Im Allgemeinen werden folgende Operationen im Dispatcher ausgeführt: 1. Auswerten der Schleifenbedingung (ggf. Ausbruch aus Schleife). 2. Indizierter Sprung zum Bytecode Handler 3. (nach Ausführung des Handlers) Sprung zum Ende des Switch Statements 4. Sprung zum Beginn der Schleife Es gibt also insgesamt drei Sprünge, von denen zwei nicht wirklich nötig sind. Die Idee beim Threaded Dispatching ist, diese zwei Sprünge zu eliminieren. Dazu wird der relevante Dispatch Code (Punkt 1 und 2) an jeden Bytecode Handler angehängt und direkt vom Ende eines Handlers zum Anfang des nächsten Handlers gesprungen. Das Problem hierbei ist, daß man soetwas nicht mit reinem ANSI C realisieren kann. Es gibt Compiler die gewisse Erweiterungen haben, die man zur Implementierung eines solchen Dispatchers verwenden kann. Insbesondere der GCC besitzt die nötigen Syntax Erweiterungen. Dies ist der Compiler, der auch für die meisten Zielsysteme eingesetzt wird, die von JamaicaVM unterstützt werden. Eine wichtige Ausnahme davon ist WindowsCE, wo der Microsoft C Compiler verwendet wird. Wie das Problem dort gelöst werden kann, soll untersucht werden. Eine andere Plattform, die keinen GCC verwendet ist OS9. Auch für diese Plattform sollte nach einer alternativen Lösung gesucht werden. Im schlimmsten Fall muss für nicht-gcc Compiler weiterhin der While-Switch-Interpreter verwendet werden.

32 KAPITEL 3. LÖSUNGSMÖGLICHKEITEN Direct Dispatching Direct Dispatching ist eine Optimierung des Dispatching Codes, der auf dem Threaded Dispatching aufbaut. Beim Threaded Dispatching wird die Adresse des jeweils nächsten auszuführenden Handlers aus einer Sprungtabelle ausgelesen, mit dem jeweiligen Bytecode als Index. Die Idee beim Direct Dispatching ist nun, diesen Lookup einzusparen, und die Adresse des nächsten Bytecode-Handlers direkt im Bytecode-Stream zu speichern. Diese Technologie wird in vielen VMs implementiert (z.b. JamVM [8] und GCJ [5]) und hat dort jeweils signifikante Performanz Gewinne gebracht. Ein großer Nachteil allerdings ist der gesteigerte Speicherbedarf. Während normale Bytecode-Instruktionen nur ein Byte (== 8 Bit) Arbeitsspeicher verbrauchen, benötigen Adressen auf 32 Bit Systemen 4 Byte (== 32 Bit). Das bedeutet, daß der Speicherverbrauch von Java Programmen im Arbeitsspeicher im schlimmsten Fall vervierfacht wird, da für jeden eingelesenen Bytecode 4 statt 1 Byte im Speicher reserviert werden muss. Das mag für Desktop-Systeme nicht besonders signifikant sein, aber auf eingebetteten Systemen ist es nicht sinnvoll, Arbeitsspeicher für Performanz zu opfern. Es muss weiterhin bemerkt werden, daß Direct Dispatching eine direkte Erweiterung des Threaded Dispatching ist. Die Implementierung dieser Technologie hängt also unmittelbar von der Implementierung des Threaded Dispatching ab. Aus diesen Gründen wurde Direct Dispatching im Rahmen dieser Diplomarbeit nicht implementiert. Nichtdestotrotz ist es eine interessante Technologie, die möglicherweise in Zukunft im Rahmen eines separaten Projekts implementiert werden sollte. 3.5 Top-Of-Stack Caching Eine Java VM ist laut Spezifikation eine Stack-orientierte Maschine. Alle Operationen verwenden einen sogenannten Operanden-Stack zur Zwischenspeicherung von Operanden und Ergebnissen. Eine grundlegende Eigenschaft von Stacks ist es, daß immer nur das oberste Element zugreifbar ist. Im Gegensatz dazu stehen Register-basierte Maschinen, bei denen Operanden und Ergebnisse in speziellen Speicherstellen des Prozessors, den sogenannten Registern, abgelegt werden. Diese sind beliebig zugreifbar. Das Stack-basierte Design macht eine Implementierung von Interpretern und Compilern sehr einfach. Ein offensichtlicher Nachteil ist allerdings, daß die allermeisten (Hardware) Prozessoren register-basiert sind. Das bedeutet, daß man den Stack im Hauptspeicher verwalten muss (der deutlich langsamer im Zugriff ist als

33 KAPITEL 3. LÖSUNGSMÖGLICHKEITEN 25 Prozessorregister). Die Möglichkeiten des Prozessors lassen sich also nur begrenzt ausnutzen. Eine Register-basierte virtualle Maschine liese sich ggf. wesentlich besser auf einen Hardware-Prozessor abbilden. Eine Möglichkeit dieses Problem zu lösen, ist das sogenannte Top-Of- Stack-Caching. Dabei wird das jeweils oberste Element des Stacks (oder sogar mehrere Elemente) in einem Prozessor-Register gespeichert, statt im Hauptspeicher, und nur bei Bedarf (z.b. bei einem Methodenaufruf oder falls mehr Werte auf dem Stack landen) in den Hauptspeicher zurückgeschrieben. Dadurch lässt sich Interpreter-Performanz signifikant steigern. Allerdings erkauft man sich diesen Gewinn mit einer stark erhöhten Komplexität des Stack-Zugriffs-Codes. Top-Of-Stack Caching ist durch die Verwendung von Registern eine inherent plattformabhängige Technologie. Sowohl die Implementierung als auch der mögliche Nutzen hängen sehr stark von der Zielplattform ab. Die Implementierung von plattformunabhängigeren Optimierungen hat natürlicherweise Vorrang, da sie Performanzgewinn für alle oder viele unterstützte Plattformen versprechen. Sehr plattformabhängige Optimierungen wie diese werden daher hintenangestellt. Darum konnte diese Optimierung im Rahmen dieser Diplomarbeit nicht implementiert werden. 3.6 Superinstructions Es ist relativ wahrscheinlich, daß bestimmte Bytecode Gruppen existieren, die besonders häufig gemeinsam, in einer bestimmten Sequenz, vorkommen. Als Beispiel möchte ich hier die Addition zweier Werte aufführen. Zum Addieren zweier Integer Werte müssen diese auf den Stack gepusht, dann addiert und schliesslich das Ergebnis wieder vom Stack gepoppt werden. Beispielsweise durch folgende Bytecode-Sequenz: iload0 // Lädt lokale Variable 0 auf den Stack iload1 // Lädt lokale Variable 1 auf den Stack iadd // Addiert beide Werte und pushed das Ergebnis auf den Stack istore2 // Speichert Ergebnis in lokaler Variable 2 Es ist wahrscheinlich, daß solche oder ähnliche Bytecode-Sequenzen relativ häufig vorkommen. Eine mögliche Optimierung ist daher, häufig vorkommende Sequenzen zusammenzufassen und durch einen speziellen neuen Bytecode zu ersetzen. Solche Bytecode-Instruktionen werden Superinstructions genannet. Der Vorteil bei der Verwendung von Superinstructions besteht zum

34 KAPITEL 3. LÖSUNGSMÖGLICHKEITEN 26 einen darin, daß für diese Bytecode-Sequenzen der Dispatch-Overhead komplett eingespart werden kann. Ausserdem können in vielen Fällen Optimierungen implementiert werden, wie zum Beispiel das Weglassen von unnötigen Stack-Zugriffen. Aufgabe hier ist es, herauszufinden, welche Bytecode-Sequenzen besonders häufig auftreten, einen Mechnismus zu implementieren, um solche Sequenzen zu ersetzen (z.b. beim Klassenladen), und natürlich die neuen Superinstructions als Bytecode-Handler zu implementieren. 3.7 Einsatz eines VM-Interpreter-Generators In einem Java Interpreter gibt es sehr viel Code, der sich prinzipiell wiederholt. Zum Beispiel muss jeder Bytecode-Handler Werte vom Stack poppen, auf den Stack pushen oder aus dem Bytecode-Stream lesen. Dazu kommt, daß in der JamaicaVM zwei Interpreter-Schleifen existieren, und evtl noch mehr dazukommen. Ein weiterer Gesichtspunkt ist, daß Bytecode-Handler und der Dispatching Code idealerweise voneinander unabhängig sein sollten. Momentan sind der Dispatch Code und die Bytecode Handler sehr stark aneinander gekoppelt. Möchte man einen alternativen Dispatcher implementieren, muss man entweder sehr viel Code kopieren, oder manuell anpassen, beides ist sehr fehleranfällig. Es scheint daher sinnvoll zu sein, die Bytecode-Handler inklusive deren Boilerplate Code (Stack- und Bytecode-Streamzugriffe) zu generieren und sinnvoll aus dem Dispatch-Code herauszuabstrahieren. Die Idee dazu stammt von Vmgen [3], einem Interpreter Generator, der in GForth eingesetzt wird. Der Einsatz eines VM Generators kann als Querschnitts-Technologie betrachtet werden. Ein VM Generator ist für sich genommen keine Optimierung am Interpreter, aber es erleichtert die Implementierung verschiedener Technologien, oder macht es überhaupt erst machbar. Weiterhin muss festgestellt werden, daß durch die Generierung von häufig verwendeten, immer gleichen Codes (z.b. Stack-Zugriffs-Code) viele potentielle Fehlerquellen vermieden bzw. minimiert werden. Die Implementierung eines VM Generators im Stile des GForth Generators scheint sehr sinnvoll, und sollte auf jeden Fall zu Beginn des Projektes erfolgen, um größtmöglichen Nutzen zu bringen, und massives Refactoring in späteren Phasen zu vermeiden.

35 KAPITEL 3. LÖSUNGSMÖGLICHKEITEN Just In Time Compilation Bei der Just In Time Compilation wird der Java Bytecode nicht mehr im strengen Sinne interpretiert, sondern vor der Ausführung direkt in Maschinencode übersetzt. Damit wird zum einen der Dispatch-Overhead vollständig eliminiert. Weiterhin eröffnen sich bei der Just In Time Compilation eine Vielzahl weiterer Optimierungen, solche wie sie auch von gängigen Compilern implementiert werden, aber auch spezielle JIT spezifische Techniken. Man kann einen JIT Compiler allerdings nicht mehr wirklich als Interpreter bezeichnen. Es gibt verschiedene prinzipielle Ansätze zur JIT Compilation. Zum einen besteht die Möglichkeit, prinzipiell jede Methode vor Ihrer ersten Ausführung zu compilieren und nur Maschinencode auszuführen. Die andere Möglichkeit ist, nur Methoden zu compilieren, die auch häufig genug ausgeführt werden. Dieses Verfahren nennt man auch adaptiver JIT, da sich das Laufzeitverhalten den Bedürfnissen der Anwendung und der Benutzer anpasst. Das erste Verfahren hat den Vorteil, daß es relativ einfach ist, man benötigt keinen zusätzlichen Interpreter, und muss auch nicht den Übergang vom Interpreter in Maschinencode und zurück berücksichtigen, da schlichtweg nur Maschinencode letztendlich ausgeführt wird. Dafür ist die Startupzeit von solchen JITs im allgemeinen sehr lang. Das mag in manchen Umgebungen allerdings keine große Rolle spielen (z.b. Production Server und Embedded Anwendungen). Das zweite Verfahren ist in vielerlei Hinsicht deutlich effektiver. Zum einen ist die Startupzeit im Vergleich zum ersten Verfahren deutlich besser. Außerdem benötigt ein solcher JIT viel weniger zusätzlichen Arbeitsspeicher zur Laufzeit, da viel weniger Code letztendlich compiliert wird. Und nicht zuletzt können adaptive Compiler oftmals besser optimieren, da sie in der Regel auch verschiedene Informationen über das Laufzeitverhalten der Anwendung sammeln. JIT Compiler haben aber auch eine Reihe von Nachteilen. Insbesondere im Embedded und Realtime Bereich gibt es Probleme, die den sinnvollen Einsatz eines JITs fragwürdig erscheinen lassen: JIT Compiler benötigen Speicher. Zusätzlich zu dem Speicher, der durch den Java Bytecode belegt wird, benötigt ein JIT zur Laufzeit zusätzliche Speicher-Buffer um den Maschinencode abzulegen. Während dies auf modernen Desktop- und Server-Systemen kein großes Problem darstellt (man allokiert einfach zusätzliche Speicherbereiche), ist das bei einem Embedded System oftmals nicht möglich. Die Speicheranforderungen eines JITs sind im Allgemeinen nicht gering. Der compilierte Maschinencode zu einer Java Methode benötigt in der Regel ein

36 KAPITEL 3. LÖSUNGSMÖGLICHKEITEN 28 Vielfaches des Speichers, der durch den Java Bytecode belegt wird. Schlimmer noch, die Speicheranforderungen bei einem JIT sind nicht vorhersehbar. Bei den geringen verfügbaren Mengen an Arbeitsspeicher auf einem Embedded System muss immer damit gerechnet werden, daß einem JIT der Speicher ausgeht. JIT Compiler zerstören die Echtzeitfähigkeit einer Anwendung. Echtzeitfähigkeit bedeutet, daß zu einer gegebenen Methode eine sinnvolle Worst Case Execution Time bestimmt werden kann, die nicht (hartechtzeitfähig) oder nur selten und geringfügig (weich-echtzeitfähig) überschritten wird. Es ist zwar sicher zu sagen, daß die Worst Case Execution Time bei einem JIT durch die Worst Case Execution Time der rein interpretierten Methode beschränkt ist, diese Abschätzung ist aber ziemlich wertlos, da die Ausführungszeit einer compilierten Methode so viel kürzer ist. Abgesehen von diesen zwei großen Problemen muss festgestellt werden, daß JamaicaVM bereits einen Ahead Of Time Compiler (AOT) zur Verfügung stellt, mit dem Java Bytecode bereits vor seiner Ausführung, genauer gesagt zum Deploy-Zeitpunkt, in Maschinencode compiliert wird. Die Performanz von solchem Code ist vergleichbar mit modernen JITs wie Sun s Hotspot. Desweiteren zeigt er keine der beiden oben genannten Probleme. Es ist also prinizipiell nicht den Aufwand wert, einen JIT für JamaicaVM zu implementieren. Ganz abgesehen davon würde es bei weitem den Rahmen dieser Diplomarbeit sprengen.

37 Kapitel 4 Umsetzung Im vorangegangenen Kapitel wurden eine Reihe von möglichen Vorgehensweisen zur Optimierung der JamaicaVM vorgestellt und diskutiert. Es sollen nun einige dieser Technologien umgesetzt werden. Zunächst wird eine Auswahl der zu implementierenden Technologien getroffen und eine sinnvolle Reihenfolge gewählt. Im Folgenden wird die Umsetzung jeweils im Detail erläutert. 4.1 Initiale Performanz-Analyse In diesem Kapitel dient der Erfassung und Beschreibung der Ausgangssituation zu Beginn des Projektes sowie einer eingehenden Analyse der Probleme bezüglich der Performanz im Jamaica Interpreter Ausgangssituation Zunächst soll einmal festgestellt werden, wie sich die Performanz des JamaicaVM Interpreters zu Beginn des Projektes verhält. Die gemessene Performanz soll später als Vergleich dienen um den Erfolg des Projektes einschätzen zu können. Weiterhin soll der JamaicaVM Interpreter mit anderen verbreiteten Java Interpretern verglichen werden, um ein Gefühl dafür zu bekommen, wie die Performanz des Jamaica Interpreters einzuordnen ist. Die Performanzwerte im folgenden Abschnitt wurden mit der Dacapo Benchmark Suite gegen Jamaica 3.0 und der damaligen Entwicklungsversion von Jamaica vom ermittelt. Jamaica 3.0 ist der zum Zeitpunkt der Diplomarbeit aktuelle Release. Die Entwicklerversion enthält schon eine Reihe Optimierungen in verschiedenen Bereichen der VM. 29

38 KAPITEL 4. UMSETZUNG 30 Abbildung 4.1 stellt die Performanz der verschiedenen Jamaica Interpreter gegenüber. Abbildung 4.1: Performanz-Vergleich Jamaica Interpreter Analyse der Performanzprobleme Um gezielte Verbesserungen vornehmen zu können soll zunächst untersucht werden, wo genau nun eigentlich die Geschwindigkeitsprobleme liegen. Es soll vermieden werden, daß Arbeit in die Optimierung seltener Randfälle gesteckt wird, die in Wirklichkeit gar nicht auftreten. Insbesondere sind folgende Informationen relevant: Benchmark Jamaica 3.0 Jamaica HEAD antlr bloat fop hsqldb jython luindex lusearch pmd Gesamt Tabelle 4.1: Performanz von Jamaica 3.0 und Jamaica 3.1beta zum Projektbeginn

39 KAPITEL 4. UMSETZUNG 31 Welche Bytecodes kommen in typischen Anwendungen wie häufig vor? Wieviel Prozessorzeit benötigen die einzelnen Bytecodehandler im Schnitt? In den folgenden beiden Abschnitten sollen diese beiden Punkte genauer analysiert werden. Häufigkeit von Bytecodes Anhand der Häufigkeit der einzelnen Bytecodes lassen sich besonders interessante Ziele für Optimierungen ausmachen. Wenn man bei einem Bytecodehandler eines besonders häufig benutzten Bytecodes nur wenige Prozessorzyklen spart, kann das schon signifikante Auswirkungen auf die Gesamt- Performanz haben. Die Bestimmung der Häufigkeit von Bytecodes ist verhältnismässig einfach. Zu diesem Zweck muss nur in der Interpreterschleife direkt vor dem Switch-Statement ein Zähler hinzugefügt werden, der in einem Array die entsprechenden Indices hochzählt (nur auszugsweise angedeutet): unsigned long bytecodeexec [ ] ; LOCAL void i n t e r p r e t ( jamaica thread ct ) { // <... > while ( ct >c o d e c a r r a y == NULL && ct >pc >= 0) { // <... > bc = jamaica next ( ct ) ; i f ( bytecodeexec [ bc ] < ) bytecodeexec [ bc]++; switch ( bc ) { // <... > } } } Listing 4.1.1: Implementierung der Bytecodestatistik Als Referenz dient hier wieder die DaCapo Benchmark Suite, da sie eine Reihe von Real-World-Anwendungen ausführt. Die Ergebnisse sind in Tabelle 4.2 zusammengefasst.

40 KAPITEL 4. UMSETZUNG 32 Es lässt sich feststellen, daß die mit Abstand am häufigsten ausgeführte Bytecode aload_0 und getfield sind. Der Bytecode aload_0 implementiert (meistens) den Zugriff auf das this Objekt in Instanz-Methoden, this ist bei Instanz-Methoden immer die lokale Variable an der Position 0 im Stack- Frame. Für statische Methoden allerdings ist aload_0 der Zugriff auf die erste lokale Variable, oder der erste Methodenparameter. Der Opcode getfield implementiert das Auslesen eines Instanz-Feldes. Im Abschnitt wird gezeigt werden, daß diese beiden Opcodes sogar meistens gemeinsam ausgeführt werden als Sequenz aload_0 getfield, was zusammen ein Instanz-Feld des this Objekts ausliest. Die nächsthäufigsten Bytecodes (aber mit deutlichem Abstand) sind invokevirtual, also das Aufrufen einer normalen (nicht-konstruktor und nicht- Interface) Methode sowie diverse Stack-Zugriffs-Opcodes (iload*). Es wird sich wohl lohnen, wenn die aload_0 und getfield Instruktionen optimiert würden. Die weiteren Instruktionen sind deswegen nicht uninteressant, aber haben vorerst geringere Priorität. Durchschnittliche Ausführungszeit der Bytecode Handler Um nun die Auswirkungen der verschiedenen Bytecode Handler realistisch betrachten zu können benötigt man nicht nur die Häufigkeit der Aufrufe der einzelnen Handler, sondern auch deren durchschnittliche Ausführungszeit. Diese variiert je nach Komplexität des Bytecodes beträchtlich. Während manche Handler nur minimal CPU Zeit verbrauchen (z.b. das Laden eines Wertes auf den Stack), benötigen andere Operationen deutlich mehr Zeit (z.b. Aufruf einer Methode oder Allokation eines Objekts). Die Ausführungszeit der Bytecode Handler kann bestimmt werden, indem jeweils vor und nach Ausführung eines Handlers ein Zeitstempel abgefragt wird. Hier benötigt man allerdings eine extrem genaue Messung, da wie schon gesagt manche Handler nur sehr wenig CPU Zyklen verbrauchen und daher einzeln betrachtet nur sehr schwer messbar sind. Es existiert in der JamaicaVM bereits eine Methode zum genauen Bestimmen von Ausführugszeit, und zwar ein Makro daß den Zyklenzähler des Prozessors abfragt. Der Vorteil ist, daß man damit eine sehr genaue Messung bekommt, vor allem auch für die kleinen Bytecode Handler. Der Nachteil ist, daß die Messungen nicht in einer metrischen Einheit vorliegen, sondern eben in der Einheit Prozessorzyklen. Das ist aber kein Problem, da wir hier nur die relative Ausführungszeit der Bytecode Handler untereinander benötigen, und nicht die genaue Anzahl Millisekunden für jeden Handler. Die Implementierung der Zeitmessung macht sich schon den Interpreter Generator (siehe nächstes Kapitel) zunutze. Mithilfe dieses Generators

41 KAPITEL 4. UMSETZUNG 33 Bytecode Name Häufigkeit Relativ 42 aload , getfield , invokevirtual , aload , iload , iload , ireturn , iload , areturn , putfield , ifeq , iadd , checkcast , iinc , dup , return , iconst , iconst , invokespecial , bipush , iload , istore , if cmplt , iand , goto , if cmpge , sipush , ifne , isub , aaload , baload , aload , ishl , instanceof , iflt , dup x , istore , getstatic , invokestatic , arraylength , ifge , if cmpne , iconst , istore , Tabelle 4.2: Häufigkeit von Bytecodes

42 KAPITEL 4. UMSETZUNG 34 lässt sich sehr einfach zusätzlicher Code direkt in alle Bytecode Handler einfügen, ohne die jeden einzelnen Handler ändern zu müssen. In diesem Falle wurde jeweils am Anfang und am Ende jedes Bytecode-Handlers etwas Code eingefügt, um den Prozessor-Zyklen-Zähler von Intel i586 Prozessoren abzufragen. Zu diesem Zweck existieren bereits Makros in der JamaicaVM, diese müssen nur noch abgefragt werden. Es wird jeweils am Anfang eines Bytecode-Handlers der Zyklen-Zähler abgefragt und am Ende eines Bytecode-Handlers nocheinmal, und dann die Differenz gebildet. Der errechnete Wert wird dann in einer Tabelle zu den bisherigen Zyklen für den jeweiligen Bytecode hinzuaddiert. Hier muss bereits 64-Bit Arithmetik eingesetzt werden, da normale 32 Bit Integer überlaufen würden. Die auf diese Weise ermittelten Zyklen-Statistiken sind in den Tabellen 4.3 und 4.4 zusammengefasst, einmal sortiert nach den absoluten Zyklenwerten und einmal sortiert nach dem Quotienten cycles/f requency. Es zeigt sich hier, daß invokevirtual offenbar die meisten Zyklen verbraucht. Dieses Ergebnis muss allerdings mit Vorsicht genossen werden. Um die Problematik zu verstehen muss kurz erklärt werden, wie der Methodenaufruf funktioniert. Es gibt im Wesentlichen zwei Fälle: Eine Java Methode wird aufgerufen, indem der Program Counter (PC) auf 0 und das Code-Array auf die neue Methode gesetzt wird sowie der Stackframe für die neue Methode aufgebaut wird. Der Bytecode- Handler invokevirtual kehrt sofort zurück und der Interpreter setzt die Arbeit in der neuen Methode ganz normal fort. Eine Native-Methode (z.b. JNI, JBI oder eine compilierte Methode) wird aufgerufen, indem die entsprechende Stub-Funktion mit den zugehörigen Parametern aufgerufen wird. Im Gegensatz zu Java Methoden kehrt invokevirtual erst zurück, wenn die Native-Methode zurückkehrt. Die Zeitmessung für invokevirtual schliesst also die kompletten Ausführungszeiten für evtl. aufgerufene Native-Methoden mit ein. Aus Tabelle 4.4 wird offensichtlich, daß die Bytecode-Handler für alle new* Operationen sehr viel Rechenzeit verbrauchen. Die Ursache dafür ist hauptsächlich im Garbage Collector zu suchen. Der Jamaica Garbage Collector führt seine Arbeit jeweils bei der Allokation neuer Objekte aus (im Gegensatz zu herkömmlichen Garbage Collectoren, die ihre Arbeit in einem separaten Thread erledigen). Abgesehen von den invoke* und new* Bytecodes zeigt sich auch hier wieder, daß eine Optimierung von aload_0 sowie getfield lohnenswert sein sollte.

43 KAPITEL 4. UMSETZUNG 35 Bytecode Häufigkeit Zyklen Zyklen pro BC invokevirtual , getfield , invokespecial , invokestatic , ireturn , checkcast , aload , new , areturn , putfield , invokeinterface , return , newarray , dup , instanceof , getstatic , aload , iload , ldc , anewarray , iload , iinc , dup x , iload , iadd , goto , ifeq , aaload , istore , iload , aload , bipush , iconst , if cmplt , aastore , sipush , iand , iconst , if cmpge , baload , isub , ifne , ldc2 w , ishl , ldc w , Tabelle 4.3: Absolute Performanz von Bytecodehandlern

44 KAPITEL 4. UMSETZUNG 36 Bytecode Häufigkeit Zyklen Zyklen pro BC multianewarray , newarray , anewarray , putstatic , new , invokestatic , athrow , ldc w , invokeinterface , invokespecial , invokevirtual , fcmpg , wide , ddiv , dneg , ldc , i2d , d2l , monitorenter , lneg getstatic , dup2 x dsub , lstore , f2d , aastore , fastore freturn , dstore , jsr , instanceof , lshr , fadd , checkcast , f2i , lookupswitch , lreturn , d2i , dreturn , ireturn , areturn , lxor , ldc2 w , return , Tabelle 4.4: Relative Performanz von Bytecodehandlern

45 KAPITEL 4. UMSETZUNG Vorgehensweise Es soll hier eine Auswahl zu implementierender Technologien getroffen werden. Als Grundlage dieser Auswahl dienen die Technologien aus Abschnitt 3 sowie die Performanz-Analyse aus Abschnitt 4.1. Die folgenden Vorgehensweise scheint zur Performanz-Optimierung sinnvoll zu sein: 1. Zu Beginn steht ein eingehendes Code Review stehen, schon alleine um mit dem Interpreter Code vertraut zu werden. Desweiteren besteht natürlich die Hoffnung, daß dadurch einige offensichtliche Performance- Bottlenecks identifiziert werden können. 2. Im nächsten Schritt wird der VM Generator implementiert und intergriert. Durch die Verwendung dieses Generators wird die folgende Entwicklung wesentlich einfacher, insbesondere wird sich-wiederholender Code sowie Änderungen, die global alle Bytecode-Handler betreffen wesentlich einfacher und fehler-sicherer zu implementieren sein. 3. Grundlegende Probleme die im ersten Schritt (Code Review) gefunden wurden, sollten als erstes nach Umstellung auf den VM Generator umgesetzt werden. Dies schliesst eine gezielte Optimierung bestimmter performanz-kritischer Bytecode-Handler ein. 4. Threaded Dispatching ist eine sehr vielversprechende Technologie um die Interpreter Performanz zu steigern. Diese kann erst mit Hilfe des VM Generators sinnvoll implementiert werden. 5. Super-Instructions scheinen ebenfalls eine sinnvolle Technologie für den Einsatz in der JamaicaVM zu sein, zumal etwas ähnliches schon für compilierten Code mit Erfolg umgesetzt wurde. 6. Top Of Stack Caching könnte implementiert werden, scheint allerdings eine Menge Plattform-Spezifika zu benötigen. Einige andere Technologien machen prinzipiell keinen Sinn im Rahmen dieser Diplomarbeit: Direct Dispatching benötigt zusätzlichen Speicher, da 8-Bit Bytecodes durch 32-Bit Adressen ersetzt werden müssten. Das wäre zwar prinzipiell möglich, scheint aber sehr aufwendig zu sein, da auf eingebetteten Systemem nicht garantiert werden kann, daß zusätzlicher Speicher vorhanden ist. Man bräuchte also gegebenenfalls einen Mechanismus, der bei mangelndem Speicher zurückfällt auf normales Dispatching.

46 KAPITEL 4. UMSETZUNG 38 Just In Time Compilation macht noch weniger Sinn zu implementieren. Zum einen benötigt Just In Time Compilation viel zusätzlichen Speicher, mehr noch als Direct Dispatching. Abgesehen davon zerstört Just In Time Compilation die Echtzeitfähigkeit einer Anwendung, da nicht mehr sicher bestimmt werden kann, ob eine Methode kompiliert ausgeführt wird oder nicht. 4.3 Interpreter-Generator Bei der Implementierung eines Bytecode-Interpreters gibt es gewisse wiederkehrende Muster. Insbesondere die Bytecode-Handler Implementierungen folgen gewissen Regelmässigkeiten. Der Aufbau eines Bytecode-Handlers folgt in der Regel folgendem Muster: 1. Präambel (z.b. ein case-label) 2. Laden eventueller Parameter aus dem Bytecode-Stream (z.b. Konstanten) 3. Laden eventueller Parameter vom Stack (z.b. Summanden) 4. Nutzcode des Bytecode-Handlers (z.b. Addition) 5. Speichern des Ergebnisses auf dem Stack (z.b. Summe) 6. Postambel (z.b. break Statement) Viele dieser Schritte können je nach Implementierung sehr verschieden sein. Beispielsweise wird die Prä- und Postambel bei einem Switch-basierten Interpreter aussehen wie oben als Beispiel angegeben, es könnte aber im Prinzip genausogut eine Funktions-Definition sein, oder etwas noch anderes (wie im päteren Kapitel Threaded Dispatching 4.5 besprochen). Ebenso kann der Bytecode- und Stack-Zugriffscode verschieden sein, z.b: existiert in der Jamaica VM eine Interpreter-Schleife, die Bytecodes aus einem C-Array lädt und eine, die Bytecodes aus einem Java-Array lädt. Die einzige wirkliche Konstante ist der eigentliche Nutzcode des Bytecode-Handlers. Es scheint sinnvoll, den Nutzcode der Bytecode-Handler vom Boilerplate Code zu trennen. Dies hätte folgende Vorteile: 1. Bestimmte Aspekte des Interpreters können leich geändert werden. Beispielsweise könnte die Stack- und Bytecode-Zugriffe leicht angepasst und optimiert werden, ohne jeden einzelnen Handler ändern zu müssen.

47 KAPITEL 4. UMSETZUNG Es kann leicht zusätzlicher Code für bestimmte Zwecke eingefügt werden. Davon wird z.b: in Abschnitt Gebrauch gemacht, um Zeitmessungs- Code in jeden Bytecode-Handler einzufügen. 3. Die Implementierung des Dispatching kann leicht geändert werden. Anstatt für jeden Bytecode-Handler die case-labels usw. manuell durch etwas anderes ersetzen zu müssen, kann das an einer zentralen Stelle angepasst werden, und wird automatisch in allen Handlern implementiert. 4. Durch die automatisierte Erzeugung von häufigem sich immer wiederholendem Code wird die Fehlerwahrscheinlichkeit bei der Implementierung deutlich reduziert sowie Redundanz vermieden GForth s Vmgen In [3] wird (u.a.) eine Implementierung eines solchen Interpreter Generators beschrieben. Diese Implementierung verwendet im Prinzip eine Eingabedatei, die die Bytecode-Handler auf abstrakte Weise beschreibt und erzeugt daraus eine Reihe Ausgabedateien, die unter anderem den Nutzcode der Bytecode- Handler in verschiedene Makros einbetten. Um nun darauf aufbauend einen Interpreter zu implementieren, muss man nur die entsprechenden Makros in seinem Interpreter-Quellcode geeignet implementieren sowie die generierten Dateien einbinden. Diese Implementierung wurde im Rahmen dieser Diplomarbeit auf eine mögliche Verwendbarkeit evaluiert. Aufgrund der folgenden Nachteile habe ich mich allerdings dazu entschlossen, diese Implementierung nicht zu verwenden, und stattdessen einen eigenen Generator zu implementieren: 1. Schlechte Unterstützung für Bottom-Up Stacks. Der GForth Vmgen setzt ein Stack-Layout vorraus, bei dem das niedrigste Stack-Element an der höchsten Speicherstelle steht, und der Stack nach unten wächst. Das hat gewisse Vorteile und kann auf einigen Plattformen möglicherweise positiv für die Performanz sein. Jamaica verwendet allerdings ein Bottom-Up Stack Layout, und das zu ändern wäre ein unverhältnismässiger Aufwand. 2. In GForth s Vmgen ist es nur schwer möglich, mithilfe einer Eingabedatei mehrere verschiedene Interpreter zu erzeugen, die auf verschiedene Weise Bytecodes lesen. Dies wird benötigt, um die zwei Methoden zum Lesen von Bytecodes (aus einem C-Array vs. einem Java-Array) zu unterstützen.

48 KAPITEL 4. UMSETZUNG Vmgen selbst ist in GForth implementiert. Dies hat gleich zwei Nachteile. Forth ist eine sehr alte und im Vergleich zu Java eher schwierige Sprache. Sie lässt sich nur schwer pflegen wenn kaum noch jemand im Team diese Programmiersprache beherrscht. Zum anderen setzt es eine Installation von GForth auf allen Build-Maschinen vorraus, was insbesondere auf den Windows Build-Maschinen nicht trivial ist. Da die Implementierung eines solchen Generators im Prinzip nicht schwer ist, ist das Vorgehen hier, eine Adaption von GForth s Interpreter Generator in Java zu implementieren, und dort die nötigen Erweiterungen einzubauen. Diese Implementierung ist angelehnt an GForth s Vmgen, nicht zuletzt um z.b. auch mögliche Optimierungen die in [3] beschrieben werden umsetzen zu können Format der Eingabedatei Die Eingabedatei für den Interpreter-Generator gliedert sich in zwei Teile, eine Präambel und einen Implementierungsteil. In der Präambel werden die Datentypen für die Eingabe-Streams und Stacks deklariert. Der Implementierungsteil beschreibt die einzelnen Bytecode-Handler, genauer gesagt, deren Eingabedaten, die Funktionalität sowie die Ausgabedaten. Das Formate der Eingabedatei ist im Wesentlichen zeilenbasiert. Jeweils eine Zeile ist entweder ein Kommentar oder Leerzeile, eine Eingabe-Deklaration, eine Bytecode-Handler-Deklaration oder ein Teil einer Bytecode-Handler Implementierung. Im Folgenden sollen die Sektionen der Eingabedatei im Detail erläutert werden Deklaration der Eingabe-Typen Zu Beginn der Eingabedatei findet sich ein Abschnitt zur Deklaration der Eingabequellen und deren Datentypen. Ein Interpreter verarbeitet Daten aus zwei Quellen, dem Bytecode-Stream (z.b. Bytecode-Parameter und Konstanten) sowie dem Operanden-Stack (Ein- und Ausgabewerte). Die Eingabequellen werden zum Beispiel wie folgt deklariert:! stack jamaica_int32! stream jamaica_int8 Diese beiden Anweisungen legen die grundlegenden Datentypen für den Operanden-Stack und den Bytecode-Stream fest. In diesem Fall sind das die Typen jamaica_int32 für den Stack und jamaica_int8 für den Bytecode- Stream.

49 KAPITEL 4. UMSETZUNG 41 Weiterhin werden die unterschiedlichen Datentypen benötigt, mit denen der Interpreter letztendlich arbeitet. Beispielsweise wird der Interpreter im Allgemeinen nicht (nur) jamaica_int32 Werte verarbeiten, sondern z.b. auch floats, doubles, Referenzen, usw. Abbildungen auf solche Typen werden wie folgt deklariert:! type stream single b jamaica_value8! type stream double s jamaica_value16! type stream quad i jamaica_value32! type stack single r jamaica_ref! type stack single i jamaica_value32! type stack single f jamaica_value32! type stack double l jamaica_value64! type stack double d jamaica_value64! type stack single u jamaica_refval32 Das type Statement leitet hierbei eine Typ-Deklaration ein. Das folgende Argument (stream oder stack) gibt an, ob sich die Typdeklaration auf einen Operanden-Stack-Typ bezieht, oder auf einen Bytecode-Stream-Typ. Es folgt eine Größenangabe (single, double oder quad), die angibt, wieviel Einheiten des Basistyps der deklarierte Typ benötigt. Beispielsweise belegt ein Java double zwei Einheiten des Basistyps jamaica_int32 auf dem Operandenstack. Das folgende Zeichen bestimmt den Prefix, anhand dessen der Typ später in den Bytecode-Handler-Deklarationen identifiziert wird. Beispielsweise wird eine Variable mit dem Namen rresult als Variable vom Type jamaica_ref angelegt. Dieses Prefix-Mapping ist im Wesentlichen aus GForth s Vmgen übernommen. Das letzte Argument schliesslich gibt den Typen an, auf den Variablen mit dem jeweiligen Prefix abgebildet werden Implementierung der Bytecode-Handler Ein Bytecode-Handler wird definiert durch die Deklaration seiner Ein- und Ausgabeparameter sowie durch den eigentlichen implementierenden Code. Dies soll anhand des Bytecode-Handlers für die Instruktion iadd erläutert werden, die zwei Werte vom Operanden-Stack lädt, sie addiert, und das Ergebnis auf den Operanden-Stack zurückschreibt. Diese Funktionalität wird implementiert durch folgende Handler-Definition: iadd ( iv1 iv2 -- ir ) ir.i = iv1.i + iv2.i;

50 KAPITEL 4. UMSETZUNG 42 Die erste Zeile deklariert die Ein- und Ausgabeparameter. Die Variablen vor dem -- sind dabei die Eingabe-Parameter, und die Variablen nach dem -- bezeichnen Ausgabeparameter. Die Eingabeparameter werden hierbei rückwärts vom Stack gepoppt. Zuoberst auf dem Stack liegt also in diesem Beispiel der Wert für iv2, darunter der Wert für iv1. Dies wurde so implementiert, um die Notation weitestgehend an die Notation in der Java Virtual Machine Specification [7] anzugleichen. Um Parameter aus dem Bytecode-Stream zu lesen muss der Parameter mit einem $ Zeichen beginnen, wie im folgenden Beispiel: bipush ( $bbyte -- iv ) iv.i = (jamaica_int32) bbyte.b; Dieser Bytecode-Handler liesst z.b. ein einzelnes Byte aus dem Bytecode- Stream und pusht es auf den Operanden-Stack. Die restlichen Zeilen der Handler-Definition sind ganz gewöhnlicher C- Code. Auf die Variablen kann man direkt zugreifen. Der Generator deklariert diese mit dem Typ, den man in der Präambel für den jeweiligen Prefix angegeben hat. Unbekannte Prefixe erzeugen selbstverständlich einen Fehler. Die Handler-Definition wird durch eine Leerzeile beendet. Das bedeutet, daß man innerhalb der Implementierung eines Handlers keine Leerzeilen einfügen darf. Diese Beschränkung stammt aus GForth s Vmgen und sollte in einer zukünftigen Version behoben werden. Bis dahin kann man sich behelfen, indem man z.b. ein ; in eine Leerzeile schreibt Die Ausgabedateien Eine Eingabedatei in der Form wie oben beschrieben wird durch den Interpreter- Generator wie folgt verarbeitet: java com.aicas.jamaica.tools.vmgen.main jamaica.vmg Ein solcher Aufruf erzeugt drei Ausgabe-Dateien: jamaica-vm-c.i, jamaica-vm-j.i und jamaica-labels.i. Die erste Datei, jamaica-labels.i enthält nur eine Liste der Handler-Namen wie folgt: <..> INST_ADDR(swap), INST_ADDR(iadd), INST_ADDR(ladd), INST_ADDR(fadd), <..>

51 KAPITEL 4. UMSETZUNG 43 Die INST_ADDR() Instruktionen sind tatsächlich C Makros, die in der Implementierung des Interpreters definiert werden müssen. Diese Liste mit Makro- Aufrufen kann verwendet werden, um Sprungtabellen oder ähnliches zu generieren. Wie das implementiert werden kann wird im Abschnitt anhand eines Beispiels erläutert. Die beiden anderen Dateien, jamaica-vm-c.i und jamaica-vm-j.i sind beide im Wesentlichen äquivalent. Sie enthalten die eigentliche Implementierung der Bytecode-Handler, einmal mit Zugriff auf einen C-Array-basierten Bytecode-Stream und einmal mit Zugriff auf einen Java-Array-basierten Bytecode- Stream. Als Beispiel sei hier noch einmal der Bytecode-Handler für die JVM Instruktion iadd aufgeführt: iadd ( iv1 iv2 -- ir ) ir.i = iv1.i + iv2.i; Dieser Handler wird durch den Generator wie folgt erzeugt: /** iadd ( iv1 iv2 -- ir ) **/ LABEL(iadd) NAME("iadd") { jamaica_value32 iv1; jamaica_value32 iv2; jamaica_value32 ir; DEF_CA START NEXT_P0 STACK_POP_JAMAICA_VALUE32(ct, iv2, -1); STACK_POP_JAMAICA_VALUE32(ct, iv1, -2); { #line 447 "jamaica.vmg" ir.i = iv1.i + iv2.i; } NEXT_P1 STACK_PUSH_JAMAICA_VALUE32(ct, ir, -2); STACK_INCR(ct, -1) LABEL2(iadd) END NEXT_P2 }

52 KAPITEL 4. UMSETZUNG 44 Der Handler beginnt mit einem LABEL Makro. Dieses wird benutzt, um zum Beispiel in einem Switch-Statement die case Labels zu erzeugen. Das NAME Makro dient Debug-Zwecken, z.b. kann man sich hier den Namen des Bytecode-Handlers über ein printf ausgeben lassen. Es folgen die Deklarationen der Ein- und Ausgabevariablen. Diese werden vom Generator aus den Typen-Deklarationen und der Handler-Deklaration abgeleitet. Über das folgende DEF_CA Makro lassen sich leicht weitere Variablen-Deklarationen einbinden. Im Start-Makro kann zusätzlicher Code an den Beginn jedes Handlers eingefügt werden, wie zum Beispiel Code zur Zeitmessung wie in Abschnitt verwendet. Das NEXT_P0 Makro dient dem Instruction-Prefetching. Damit können alternative Strategien implementiert werden, um die nächste Bytecode-Instruktion zu laden. Dies ist der GForth-Implementierung entlehnt um in einem späteren Projekt solche Strategien wie in [3] beschrieben implementieren zu können. Es folgen zwei STACK_POP Makros, diese dienen dem Laden der Eingabevariablen mit den entsprechenden Stack-Werten. Der folgende Block ist die eigentliche Implementierung des Handlers, wie in der Eingabe-Datei angegeben. Es wird hier eine #line... Zeile eingefügt. Diese wird vom GCC-Compiler ausgewertet und als Debug-Information in die Objekt-Datei eingebettet. Damit kann man beim Debuggen die ursprüngliche Quellen des Codes besser zurückverfolgen. Der restliche Code ist mehr oder weniger symmetrisch zur Präambel des Handlers. NEXT_P1 und NEXT_P2 dienen dem Laden des nächsten Bytecodes. Die STACK_PUSH und STACK_INCR Makros dem Speichern der Ergebnisse. Dabei gibt STACK_INCR an, wieviel der Stack nach Ausführen des Handlers tatsächlich gewachsen ist. Mit LABEL2 kann man sich ein End-Label ausgeben lassen, wenn man eines benötigt. END schliesslich dient dem Einfügen von abschliessendem Code in jeden Handler, z.b. zur Zeitmessung Implementierung eines Interpreters mithilfe des Generators Die eigentliche Implementierung eines Interpreters muss jetzt nur noch die richtigen Dateien and der richtigen Stelle laden sowie die richtigen Makros definieren. Ein einfacher kompletter Interpreter könnte dann wie folgt aussehen: #define INST_ADDR(name) I_##name #define LABEL(name) case I_##name: #define NEXT_P2 break; #define FETCH_J_JAMAICA_VALUE8(bc) bc.b = next(); /*.. more FETCH definitions here... */

53 KAPITEL 4. UMSETZUNG 45 #define STACK_INCR(ct,i) #define STACK_POP_JAMAICA_REF(target, idx) (target).r = pop_ref(); /*.. more STACK definitions here... */ /** The entry point for the interpreter **/ void interpret() { /* This defines an enum which is then used for indexing the switch cases. */ enum { #include "jamaica-labels.i" }; /* The interpreter loop. */ while (pc >= 0) { FETCH_J_JAMAICA_VALUE8(bc); switch (bc.u) { #include "jamaica-vm-j.i" default: handle_unknown_bc(); } } } Dies ist ausreichend um einen sehr einfachen Interpreter zu implementieren. Zunächst werden alle benötigten Makros sinnvoll definiert. Alle weiteren Makros müssen natürlich auch definiert werden, aber zu inem leeren Statement expandieren. Diese sind hier nicht extra aufgeführt. In diesem Beispiel wird ein Switch-basierter Interpreter implementiert. Zunächst wird hier ein enum definiert, das später zum Indizieren des Switch-Cases verwendet wird. Dieses enum expandiert nach dem Präprozessor zu (Ausschnitt): enum { /*... */ I_swap, I_iadd, I_ladd, I_fadd, /*... */ };

54 KAPITEL 4. UMSETZUNG 46 Die eigentliche Interpreter-Schleife wird vom Präprozessor folgendermaßen expandiert (Ausschnitt): while (pc >= 0) { bc.b = next(); switch (bc.u) { /*... */ case I_iadd: jamaica_value32 iv1; jamaica_value32 iv2; jamaica_value32 ir; iv2.i = pop_value32(); iv2.i = pop_value32(); { ir.i = iv1.i + iv2.i; } push_value32(ir.i); break; /*... */ default: handle_unknown_bc(); } } Mit Hilfe dieses generierten Handler-Codes ist es nun sehr einfach, einen völlig anderen Interpreter zu implementieren (z.b. mit Threaded Dispatching wie in Abschnitt 4.5 beschrieben), oder bestimmte Aspekte einfach zu ändern, wie zum Beispiel den Stack-Zugriff. 4.4 Stackzugriff Im Abschnitt 3 wurden eine Reihe von Vorgehensweisen erläutert. Als einer der wichtigen Ansätze wurde Code Review genannt. Eines der Resultate des Code Review war die Feststellung, daß der Zugriff auf den Operanden Stack in einigen Punkten suboptimal ist. In diesem Abschnitt wird erläutert, wie der Stack-Zugriff verbessert wurde. Um die Umsetzung zu verstehen, ist es erforderlich den Aufbau der Stacks in der Jamaica VM zu verstehen. Dies ist im Abschnitt erläutert.

55 KAPITEL 4. UMSETZUNG Konsolidierung der Stack-Zugriffs-Makros Der Zugriff auf den Java Stack ist in der Jamaica VM über eine Reihe von Makros implementiert. Die Idee dahinter ist, daß Programmteile, die auf den Java Stack nicht die beteiligten Datenstrukturen selbst manipulieren sollen, sondern diese Zugriffe soweit wie nötig abstrahiert sind. Es werden keine Inline Methoden verwendet, da die Interpreterschleife komplett in einer Funktion liegt und diese sehr groß ist. Ein C Compiler würde Funktionen hier nicht inlinen. In der JamaicaVM gibt es eine ganze Reihe von Stackzugriffsmakros. Leider sind diese wenig systematisch und enthalten viele Doppelungen. Dies ist historisch gewachsen und wurde einfach nur nie bereinigt. Um nun aber unnötige Arbeit bei der eigentlichen Änderung des Stack Zugriffs zu vermeiden, sollen diese Zugriffsmakros zunächst einmal vereinheitlicht werden. Folgende Makros finden sich zum Stackzugriff: JAMAICA_PUSH JAMAICA_PUSH2 JAMAICA_PUSHR JAMAICA_PUSH_IR JAMAICA_POP JAMAICA_POP2 JAMAICA_POPR JAMAICA_POP_IR JAMAICA_AT JAMAICA_AT_2 JAMAICA_AT_R JAMAICA_AT_IR JAMAICA_STORE_AT JAMAICA_STORE_AT_2 JAMAICA_STORE_AT_R

56 KAPITEL 4. UMSETZUNG 48 JAMAICA_GET JAMAICA_GET2 JAMAICA_GETR JAMAICA_GETIR JAMAICA_STACK_PUT JAMAICA_STACK_PUT2 JAMAICA_STACK_PUTR JAMAICA_POP_INT32 JAMAICA_POP_INT64 JAMAICA_POP_FLOAT JAMAICA_POP_DOUBLE JAMAICA_POP_REFERENCE JAMAICA_PUSH_INT32 JAMAICA_PUSH_INT64 JAMAICA_PUSH_FLOAT JAMAICA_PUSH_DOUBLE JAMAICA_PUSH_REFERENCE JAMAICA_GET_INT32 JAMAICA_GET_INT64 JAMAICA_GET_FLOAT JAMAICA_GET_DOUBLE JAMAICA_GET_REFERENCE JAMAICA_PUT_INT32 JAMAICA_PUT_INT64 JAMAICA_PUT_FLOAT

57 KAPITEL 4. UMSETZUNG 49 JAMAICA_PUT_DOUBLE JAMAICA_PUT_REFERENCE JAMAICA_POP_REMOVE JAMAICA_POPN JAMAICA_STACK_DISCARD INTERPRET_PUSH_CHECK INTERPRET_PUSHR_CHECK INTERPRET_POP_CHECK INTERPRET_POPR_CHECK Weiterhin existieren noch ein paar Inline-Funktionen: jamaicainterpreter_push() jamaicainterpreter_push2() jamaicainterpreter_pushr() jamaicainterpreter_pop() jamaicainterpreter_pop2() jamaicainterpreter_popr() Zusaätzlich existieren noch einige Zugriffsmakros für lokale Variablen, die letztendlich auch auf den Stack zugreifen: JAMAICA_LOCAL JAMAICA_LOCAL2 JAMAICA_LOCAL_R JAMAICA_SET_LOCAL JAMAICA_SET_LOCAL2 JAMAICA_SET_LOCAL_R INTERPRET_LOCAL

58 KAPITEL 4. UMSETZUNG 50 INTERPRET_LOCAL_R Ich erspare mir die Erläuterung der Funktionalität dieser Makros. Diese ergibt sich größtenteils aus dem Namen. Aber es ist offensichtlich, daß hier unnötig viel Duplikate vorhanden sind. Dies macht den Code nicht nur unübersichtlich, sondern auch anfällig für Fehler. Änderungen müssen immer an mehreren Stellen eingepflegt werden, wenn man Inkonsistenzen vermeiden möchte. Im Zusammenhang mit dem Interpreter-Generator wurde der Stack-Zugriff signifikant verbessert. Im Wesentlichen bestehen jetzt zwei verschiedene Wege um auf den Stack zuzugreifen: Von innerhalb des Interpreters über die neuen Stack-Zugriffs-Makros (siehe unten). Dies Verwendung dieser Makros wird ausschliesslich vom Interpreter-Generator erzeugt. Von ausserhalb des Interpreters über ein Set von Funktionen. Die Zugriffsmakros bzw. Funktionen sind jetzt einheitlich und Typ-spezifisch. Weiterhin wurde Wert darauf gelegt, daß nur POP und PUSH Zugriffe für den Operanden-Stack erlaubt werden. Das erzwingt eine sauberere Programmierung. Die einzigen Ausnahmen bleiben der Zugriff auf Stack-Frame-Daten und auf lokale Variablen. Folgende Zugriffsmakros werden nun innerhalb des Interpreters verwendet: STACK_POP_JAMAICA_VALUE32 STACK_POP_JAMAICA_VALUE64 STACK_POP_JAMAICA_REF STACK_POP_JAMAICA_REFVAL32i STACK_PUSH_JAMAICA_VALUE32 STACK_PUSH_JAMAICA_VALUE64 STACK_PUSH_JAMAICA_REF STACK_PUSH_JAMAICA_REFVAL32i Für den Stack-Zugriff von ausserhalb des Interpreters (also zum Beispiel aus dem Garbage Collector oder aus der Runtime Implementierung) bleiben die folgenden Funktionen bestehen:

59 KAPITEL 4. UMSETZUNG 51 jamaicainterpreter_pop() jamaicainterpreter_pop2() jamaicainterpreter_popr() jamaicainterpreter_push() jamaicainterpreter_push2() jamaicainterpreter_pushr() Diese Konsolidierung hat nicht nur den Interpreter Code übersichtlicher gemacht, sondern führt zu folgenden Verbesserungen: Die Pflege des Codes wird deutlich einfacher. Eine Änderung am Stack- Zugriff muss nun an wesentlich weniger Stellen angepasst werden als vorher. Durch weniger Code und einfachere Pflege desselben werden selbstverständlich auch viele potentielle Fehlerquellen vermieden. Der beliebige Zugriff auf den Operanden Stack wurde komplett entfernt. Der Operanden-Stack kann jetzt nur noch über pop und push- Operationen zugegriffen werden. Das erzwingt saubere Programmierung und vermeidet auf diese Weise Fehler Minimierung der Zugriffe auf den cont locals Stack Wie in Abschnitt erläutert, müssen Referenzen, die auf dem Operanden Stack gespeichert werden, separat in einem zweiten Stack gespeichert, damit der Garbage Collector diese nicht als löschbar identifizieren kann. Daher wurde bisher jedes pushen einer Referenz auf den cont_locals Stack gespiegelt, ebenso das poppen von Referenzen. Damit wird sichergestellt, daß keine Referenz die auf den Operanden-Stack liegt vom Garbage Collector gelöscht werden kann. Dies wird schematisch in Abbildung 4.2 dargestellt. Allerdings ist das tatsächlich nur nötig, wenn neue Referenzen auf den Stack gelangen. Wenn eine Referenz bereits auf dem cont_locals Stack gesichert ist, dann muss sie nicht ein zweites Mal gesichert werden. Die interessante Frage ist, wie das festgestellt werden kann. Es ist einleuchtend, daß es nicht sinnvoll ist, bei jedem Stackzugriff zu prüfen, ob dieselbe Referenz bereits gesichert ist. Ein recht einfacher Weg bietet sich hier, indem man die Semantik mancher Bytecodes genauer analysiert. Ich möchte hier im Speziellen auf den Bytecode

60 KAPITEL 4. UMSETZUNG 52 sichert kopie sichert kopie aload_0 eingehen, daß sich dieser in Abschnitt 4.1 als besonders performanzkritisch herausgestellt hat. Dieselbe Überlegung lässt sich in ähnlicher Weise auf andere Bytecodes anwenden. Der Bytecode aload_0 lädt eine lokale Variable auf den Operanden-Stack, und zwar die lokale Variable an Position 0 auf dem Stack-Frame. Das bedeutet gleichzeitig, daß sie aber schon gesichert ist. Das lässt sich relativ leicht auf induktive Weise zeigen, wenn man sich überlegt, wie der Operanden-Stack und die Stack-Frames aufgebaut werden. Abbildung 4.3 zeigt, wie eine solche Optimierung des Stack-Zugriffs aussehen müsste. Wenn sichergestellt wird, daß das erste Speichern einer Referenz auf dem Stack auf gesicherte Weise geschieht, also auf dem cont_locals Stack gespiegelt wird. Der Bytecode aload_0 kann allerdings niemals die erste Speicher-Operation einer Referenz auf den Stack sein, da diese Referenz vorher schon einmal im Stack-Frame gesichert werden musste. Es gibt allerdings einen Spezialfall, der Probleme bereiten kann. Dieser ist in Abbildung 4.4 dargestellt. Es kann passieren, daß mit new eine neue Referenz auf den Stack gelangt, die an dieser Stelle auch gesichert wird. Wird diese Referenz dann mit einem aload geladen, und danach mit areturn der Frame abgebaut, so geht die ursprüngliche Referenz samt Kopie verloren, da die lokalen Variablen abgebaut werden, und nur der Rückgabewert der aktuellen Methode (die gerade vorher mit aload geladen wurde) auf die oberste Position des darunterliegenden Frames kopiert wird. Glücklicherweise lässt sich dieses Problem relativ leicht und effizient vermeiden, indem man den Handler für areturn so ändert, daß der Rückgabesichert java stack cont_locals stack Abbildung 4.2: Referenz-Sicherung auf dem Stack

61 KAPITEL 4. UMSETZUNG 53 kopie kopie sichert java stack cont_locals stack Abbildung 4.3: Vereinfachung der Referenz-Sicherung kopie areturn new lokale vars sichert! java stack cont_locals stack java stack cont_locals stack Abbildung 4.4: Problem mit areturn bei der Referenz-Sicherung

62 KAPITEL 4. UMSETZUNG 54 wert explizit noch einmal auf dem cont_locals Stack gesichert wird. Ein ähnliches Problem ergibt sich, wenn nach einem aload_0 ein astore_0 ausgeführt wird. In diesem Fall wird unter Umständen die bereits gesicherte Referenz in der lokalen Variable 0 ersetzt durch eine andere Referenz, und kann damit die ursprüngliche Referenz auf dem Stack dem Garbage Collector preisgeben. Die Lösung dieses Problems besteht darin, beim Laden einer Methode alle Vorkommen des Bytecodes aload_0 durch eine optimierte Version aload_0_safe zu ersetzen, aber nur dann, wenn in dieser Methode kein astore_0 vorkommt. Dies ist zum Glück fast nie der Fall, da die lokale Variable bei Instanz-Methoden das this Objekt speichert, das nicht geändert werden kann. Der Bytecode astore_0 kann also nur bei statischen Methoden vorkommen Aufbau des Stackframes Beim Aufbau des neuen Stackframes wurde der Platz für lokale Variablen in einer Schleife reserviert: for ( i=c a l l e d n a r g s ; i<c a l l e d n l o c a l s ; i++) { jamaicascheduler syncpoint ( ct ) ; JAMAICA PUSH( ct, 0 ) ; } Listing 4.4.1: Aufbau eines Stackframes beim Methodenaufruf Das Löschen der Speicherstellen für die lokalen Variablen ist unnötig, da vom Java-Compiler erzwungen wird, daß lokale Variablen initialisiert werden. Es reicht daher, einfach den Stack-Zeiger um die nötige Anzahl Elemente hochzusetzen: ct->m.cont_locals_index += called_nlocals-called_nargs; Diese Optimierung ist besonders für Methoden mit vielen lokalen Variablen oder Parametern interessant. 4.5 Threaded Dispatching Beim Interpretieren von Java Bytecode (und in diesem Zusammenhang in jedem Interpreter) spielt der sogenannte Dispatcher (deutsch: Zusteller oder

63 KAPITEL 4. UMSETZUNG 55 Bote ). Damit ist der Teil des Interpreters gemeint, der aus dem Bytecode- Stream die Bytecode-Instruktionen ausliest und die dazugehörigen Funktionen, die sogenannten Bytecode-Handler, aufruft. Das ist sozusagen der eigentliche Kern des Interpreters. Dieser ist deshalb so wichtig, weil er für jeden einzelnen Bytecode immer wieder ausgeführt wird. Es von absoluter Wichtigkeit, daß der Overhead des Dispatching so minimal wie möglich gehalten wird Das Problem des while-switch-dispatchers Es soll zunächst einmal betrachtet werden, wie der Dispatcher bisher in der JamiacaVM implementiert war, sowie dessen Probleme näher betrachtet werden. Der Dispatcher in der JamaicaVM ist ein ganz einfacher sogenannter while-switch-dispatcher. Das ist der offensichtliche Ansatz zur Implementierung eines Bytecode-Interpreters wie er in vielen VMs umgesetzt ist. Listing veranschaulicht die prinzipielle Implementierung eines solchen Dispatchers. while ( pc >= 0) { switch ( bytecodes++) { case op1 : // Bytecode Handler f ü r op1. break ; case op2 : // Bytecode Handler f ü r op2. break ;... case opn : // Bytecode Handler f ü r opn. break ; } } Listing 4.5.1: Ein while-switch-dispatcher Im Wesentlichen besteht dieser aus einer großen while-schleife. Die Abbruchbedingung ist hier so gewählt, daß die Schleife sich beendet, wenn der progam counter kleiner als 0 ist. Der Program Counter wird dann auf -1 gesetzt, wenn entweder eine Exception geworfen wurde, die nirgends gefangen wird, oder wenn das Programm am ordnungsgemäßen Ende angekommen ist.

64 KAPITEL 4. UMSETZUNG 56 Innerhalb der while-schleife befindet sich ein großes switch-statement, der eigentliche Kern des Dispatchers. Dieses Switch-Statement verzweigt abhängig vom eingelesenen Bytecode zum jeweils dazugehörigen Bytecode Handler. Ein solches Switch-Statement wird von einem C Compiler in der Regel zu einem indizierten Sprung über eine Sprungtabelle kompiliert. Der schematische Ablauf beim Interpretieren von Bytecodes mit einem whileswitch-dispatcher ist in Abbildung 4.5 dargestellt. Diese Abbildung veranschaulicht auch gleich das Problem eines while-switch-dispatchers: Es müssen hier pro Dispatch-Vorgang drei Sprünge ausgeführt werden, ein Sprung vom Schleifenanfang zum Bytecode-Handler, ein Sprung vom Bytecodehandler zum Schleifenende und noch ein Sprung vom Schleifenende zum Schleifenanfang. Davon ist nur ein Sprung wirklich vorhersehbar (weil unbedingt), und das ist der Sprung vom Handler-Ende zum Schleifenende. Die beiden anderen Sprünge sind beide abhängig von den Eingangsdaten, und damit für einen Prozessor schwerer bis gar nicht vorhersehbar. Das hat schwerwiegende Auswirkungen für die Ausnutzung der Prozessorpipeline, im schlimmsten Fall, und dieser dürfte hier sehr häufig sein, muss die Pipeline für jeden Bytecode ein bis zweimal geleert werden. Dispatcher 1 BC Handler # BC Handler #2 5 BC Hander #3 Dispatcher Abbildung 4.5: Schematischer Ablauf beim While-Switch-Dispatching

65 KAPITEL 4. UMSETZUNG Die Idee des Threaded-Dispatchers Um die Performanz des Dispatchers zu verbessern, ist die Idee, die Anzahl der nötigen Sprünge auf ein Minimum zu reduzieren. Ideal wäre, wenn man pro Dispatch Vorgang nur einen Sprung hätte (es geht noch besser, bei einem JIT hat man gar keinen Sprung mehr, weil es da kein Dispatching mehr gibt). Erreicht werden kann das, indem man den Dispatch-Code an jeden einzelnen Bytecode-Handler anhängt, und von dort direkt zum folgenden Bytecode- Handler springt. Schematisch sähe das in etwa aus wie in Listing void handlers [NUM BC] = {&&do bc1, &&do bc2,... &&do bcn } ; // S t a r t I n t e r p r e t e r by jumping // to f i r s t handler. goto handlers [ bytecodes +]; // No ANSI C!! //... do bc1 : // Bytecode Handler f ü r op1. goto handlers [ bytecodes +]; // No ANSI C!! do bc2 : // Bytecode Handler f ü r op2. goto handlers [ bytecodes +]; // No ANSI C!! //... do bcn : // Bytecode Handler f ü r opn. goto handlers [ bytecodes +]; // No ANSI C!! Listing 4.5.2: Ein Threaded-Dispatcher Wie letztendlich das Dispatching funktioniert, kann man sehr gut in Abbildung 4.6 erkennen. Sehr deutlich wird auch, wieviel weniger Sprünge hier benötigt werden. Hier wird auch klar, woher der Name threaded dispatcher kommt. Die einzelnen Bytecode-Handler werden sozusagen wie an einem Faden aufgefädelt hintereinander angesprungen, gesteuert durch die Bytecode- Sequenz des Eingabestroms. Es gibt zwei Gesichtspunkte, die beim Threaded-Dispatching beachtet werden müssen: 1. Das Anhängen des Dispatch Codes an jeden einzelnen Bytecode-Handler kann die Gesamtgröße des Interpreter Executables negativ beeinflussen.

66 KAPITEL 4. UMSETZUNG 58 Während in einem While-Switch-Dispatcher der Dispatch Code nur einmal vorhanden ist, muss dieser Code beim Threaded-Dispatching für jeden einzelnen Bytecode-Handler dupliziert werden. Bei mehr als 200 Bytecode-Handlern kann das eine signifikante Menge werden. (Wie wir später sehen werden, kann der Compiler das allerdings optimieren und in manchen Fällen sogar ein kleineres Binary erzeugen als beim While- Switch-Dispatcher). 2. Ein Threaded-Dispatcher lässt sich nicht (oder nur sehr schwer und ineffizient) mit reinen ANSI-C Mitteln implementieren. Es existieren zumindest im GCC Compiler [4] Spracherweiterungen, mit denen man relativ einfach einen Threaded Dispatcher implementieren kann. Für andere Compiler, z.b. den Microsoft C Compiler, der bei der JamaicaVM für Windows CE verwendet wird, existieren solche Erweiterungen nicht. Es muss ggf. auf solchen Plattformen untersucht werden, wie man trotzdem einen Threaded Dispatcher implementieren kann, oder falls das nicht möglich ist, weiterhin der While-Switch-Dispatcher verwendet werden. interpret() 1 BC Handler #1 2 BC Handler #2 3 usw. BC Handler #3 Abbildung 4.6: Schematischer Ablauf beim Threaded-Dispatching

67 KAPITEL 4. UMSETZUNG Implementierung des Threaded Dispatching Die Implementierung des Threaded Dispatching macht starken Gebrauch von der Abstraktion die durch die Generierung des Interpreter-Codes durch Vmgen erreicht wurde. Betrachten wir noch einmal am Beispiel des Bytecode- Handlers iadd, wie der generierte Code aufgebaut ist. Listing zeigt den Quellcode für iadd (aus jamaica.vmg). Listing zeigt den Code, der durch Vmgen für diesen Handler generiert wurde (aus jamaica-vm.i). Weiterhin wird in jamaica-labels.i ein Eintrag der Form INST_ADDR(iadd), erzeugt. Interessant für den Dispatcher sind hier die Makros INST_ADDR, LABEL sowie NEXT_P2 (NEXT_P0 und NEXT_P1 sind vorgesehen für verbesserte Versionen des Threaded Dispatching, werden aber hier nicht verwendet. # iadd (0x60) iadd ( iv1 iv2 -- ir ) ir.i = iv1.i + iv2.i; Listing 4.5.3: Vmgen Quellcode des iadd Handlers Die eigentliche Implementierung ist relativ einfach zu bewerkstelligen. Es müssen nur die relevanten Makros sinnvoll implementiert werden, und der generierte Code zusammengesetzt werden. Zunächst einmal benötigen wir die Addressen für die Bytecode Handler. Diese bekommen wir, indem wir uns eine GCC spezifische Syntax-Erweiterung für C zunutze machen. Es handelt sich dabei um sogenannte Label-Pointer, vom Konzept her ähnlich wie Funktionspointer, aber eben angewendet auf Labels innerhalb einer Funktion. Das heisst, daß die Bytecode-Handler als einfache Code-Blöcke mit Labels versehen implementiert werden. Zu diesem Zweck lassen wir das Makro LABEL(name) wie folgt zu einem Label expandieren: #define LABEL(name) IJ_##name: Dies erzeugt aus einem Bytecode-Handler mit dem Namen XYZ ein Label der Form IJ_XYZ:, also z.b. für iadd ein Label IJ_iadd:. Es sollte bemerkt werden, daß das J eine besondere Bedeutung hat, es werden nämlich für alle Bytecode-Handler zwei Implementierungen erzeugt, eine, die auf einem Bytecode-Stream in einem C-Array operiert (also einfacher Speicher) und eine, die auf einem Bytecode-Stream in einem Java-Array operiert (der in einer Baumstruktur vorliegen kann). Die erstere Version wird durch Labels der Form IC_name dargestellt, und die zweite Version durch IJ_name.

68 KAPITEL 4. UMSETZUNG 60 /** iadd ( iv1 iv2 -- ir ) **/ LABEL(iadd) NAME("iadd") { jamaica_value32 iv1; jamaica_value32 iv2; jamaica_value32 ir; DEF_CA START NEXT_P0 STACK_POP_JAMAICA_VALUE32(ct, iv2, -1); STACK_POP_JAMAICA_VALUE32(ct, iv1, -2); { #line 447 "jamaica.vmg" ir.i = iv1.i + iv2.i; } NEXT_P1 STACK_PUSH_JAMAICA_VALUE32(ct, ir, -2); STACK_INCR(ct, -1) LABEL2(iadd) END NEXT_P2 } Listing 4.5.4: Generierter Code des iadd Handlers

69 KAPITEL 4. UMSETZUNG 61 Der wichtigste Teil der Implementierung für Threaded Dispatching ist der Dispatcher selbst. Dieser ist realisiert im Makro NEXT_P2. Dieses Makro wird von Vmgen nach dem eigentlichen Bytecode-Handler und dem Stackzugriff eingefügt. Die Implementierung dieses Makros enthält die Endbedingung für den Interpreter (die beim While-Switch-Interpreter in der While-Schleife codiert ist) sowie den Lookup und Sprung zum nächsten Bytecode-Handler. #define NEXT P2 \ i f ( ct >c o d e c a r r a y!= NULL ct >pc < 0) \ goto e x i t J ; \ else { \ FETCH JAMAICA VALUE8( bc ) ; \ jamaicascheduler syncpoint ( ct ) ; \ goto b c j l a b e l s [ bc. u ] ; \ } Listing 4.5.5: Implementierung des NEXT P2 Dispatch-Makros für Threaded Dispatching Im Interpreter selbst muss die Sprungtabelle aufgebaut werden. Hier ergibt sich ein Problem. Definiert man die Sprungtabelle als lokale Variable, dann erzeugt man bei jedem Aufruf der interpret() Funktion diese Sprungtabelle auf dem Aufrufstack. Das führt sehr schnell zu einem Stack Overflow. Legt man die Sprungtabelle stattdessen als globale Variable an, kann man in der Initialisierung nicht auf die Labels innerhalb der interpret() Funktion zugreifen. Die Initialisierung müsste also trotzdem in der interpret() Funktion implementiert sein. Das gestaltet sich aber nicht einfach. Man müsste bei jedem Aufruf von interpret() prüfen, ob die Sprungtabelle schon angelegt wurde, und falls nicht, eine entsprechende Initialisierungsroutine aufrufen, die die Adressen der Sprunglabels in die Tabelle einträgt. Es geht allerdings auch eleganter. In C gibt es das Konzept von statischen lokalen Variablen. Im Wesentlichen sind das globale Variablen, die nur innerhalb einer Funktion (oder innerhalb eines Blockes) sichtbar sind. Die Sprungtabelle kann auf diese Weise wie in Listing angelegt werden. Was hier passiert ist schnell erklärt: Für jedes Label, das in jamaica-labels.i über ein Makro INST_ADDR(name) referenziert ist wird die Adresse des Labels in den Array-Initializer eingefügt. Die Adresse eines Labels bekommt man über die GCC-spezifische Sytax-Erweiterung &&label. Letztendlich muss der Threaded Interpreter noch gestartet werden, indem zum Bytecode-Handler für den ersten Bytecode gesprungen wird. Dies ist ganz einfach durch einfügen eines NEXT_P2 Makros direkt nach dem Erzeugen

70 KAPITEL 4. UMSETZUNG 62 der Sprungtabelle realisiert Mögliche Ansätze für nicht GCC-Plattformen Der Implementierungsansatz der in den vorigen Abschnitten beschrieben wurde, verwendet Syntax-Erweiterungen von C, die nur im GCC implementiert sind. Jamaica unterstützt allerdings eine Reihe von Plattformen, auf denen nicht der GCC zum Compilieren verwendet wird. Das betrifft insbesondere WindowsCE, weil dies die meistverwendete Zielplattform ohne GCC ist. (Es gibt noch mehr Plattformen, aber diese sind eher exotisch und werden - noch - nicht in performanzkritischen Anwendungen eingesetzt). Im folgenden sollen einige Ansätze diskutiert werden um auch auf diesen Plattformen einen Threaded Interpreter implementieren zu können. Tail Recursion Statt Compiler Erweiterungen zu verwenden, um die Bytecode-Handler aufzurufen, kann man versuchen, die Bytecodehandler als Funktionen zu erzeugen, und am Ende jedes Handlers die jeweils nächstfolgende Funktion aufzurufen. Keine dieser Handler kehrt je wirklich zurück. Dies entspricht im Wesentlichen einer Tail-Recursion. Das Problem hierbei ist, daß das normalerweise relativ schnell einen Stack-Overflow erzeugt, da bei jedem Aufruf einer Funktion entsprechende Adressen auf dem Stack zwischengespeichert werden. Viele Compiler können allerdings solche speziellen Tail Recursions erkennen und so optimieren, daß der Stack für die Aufrufe überhaupt nicht verwendet wird. Eine prototypische Beispielimplementierung zeigt Listing Ein Test mit verschiedenen Compilern soll zeigen, ob und wie diese Comvoid i n t e r p r e t ( ) { //... #define INST ADDR( name) &&I J ##name static void b c j l a b e l s [MAX BC HANDLERS] = { #include jamaica l a b e l s. i &&e x i t J } ; //... } Listing 4.5.6: Anlegen der Sprungtabelle für Threaded Dispatching

71 KAPITEL 4. UMSETZUNG 63 typedef void Word ; typedef void ( I n s t r ) (Word pc ) ; static int acc ; static void Add(Word pc ) { acc += ( int ) pc++; ( ( I n s t r ) ( pc ) ) ( pc +1); } static void Print (Word pc ) { p r i n t f ( Accumulator = %d\n, acc ) ; ( ( I n s t r ) ( pc ) ) ( pc +1); } static void Halt (Word pc ) { e x i t ( 0 ) ; } int main ( int argc, char argv ) { Word pc ; Word program [ ] = { (Word) &Add, (Word)3, (Word) &Print, (Word) &Add, (Word)5, (Word) &Print, (Word) &Halt, } ; acc = 0 ; // the acc r e g i s t e r i s a g l o b a l v a r i a b l e pc = &program [ 0 ] ; // the pc r e g i s t e r i s threaded through c a l l s ( ( I n s t r ) ( pc ) ) ( pc +1); // S t a r t! } p r i n t f ( Error! \ n ) ; return 1 ; Listing 4.5.7: Threaded Dispatching mit Tail Recursion

72 KAPITEL 4. UMSETZUNG 64 piler diesen Prototyp in Maschinencode abbilden. Zu diesem Zwecke möchte ich hier relevante Ausschnitte aus dem erzeugten Assembler Code für GCC, den Microsoft Compiler sowie den OS9 Compiler aufführen. Listing zeigt den X86 Assembler Code, den der GCC für den obigen Prototyp generiert, genauer gesagt den Handler für die Add() Funktion. Interessant sind hier folgende Beobachtungen: Der Funktionsaufruf zum nächsten Handler am Ende der Funktion wird nicht als call generiert, sondern als jmp, d.h. es wird keine Rücksprungadresse auf den Stack gespeichert. Auch besitzt die Funktion keine Rücksprung-Instruktion (ret). Die Tail-Recursion wird also offenbar korrekt erkannt und wegoptimiert. Der Funktions-Boilerplate wird hier nicht vollständig eliminiert. Die pushl und popl Instruktionen sind offenbar Überbleibsel davon, die aber offenbar keinen Effekt haben. Das ist nicht optimal, aber vermutlich immer noch besser als ein Switch-Dispatcher (aber nicht so gut wie ein Threaded-Dispatcher basierend auf GCC s Erweiterungen). Add: pushl movl movl movl addl leal movl movl popl jmp %ebp %esp, %ebp 8(%ebp), %edx (%edx), %eax %eax, acc 8(%edx), %eax %eax, 8(%ebp) 4(%edx), %ecx %ebp *%ecx Listing 4.5.8: Add Funktion, compiliert nach x86 von GCC Listing zeigt dasselbe Code-Fragment, compiliert mit dem Microsoft C-Compiler. Der erzeugte Code ist im Wesentlichen recht ähnlich. Die Tail- Recursion wird auch hier richtig erkannt, und der Funktionsaufruf durch ein jmp generiert, statt durch ein call. Im Unterschied zum GCC allerdings erzeugt der Microsoft Compiler keine pushl und popl Instruktionen. Damit ist dieser Ansatz für den Microsoft Compiler im Grunde genommen äquivalent zu dem Threaded Dispatcher, der die GCC Erweiterungen nutzt.

73 KAPITEL 4. UMSETZUNG 65 Listing zeigt dieselbe Funktion, compiliert vom Microsoft C Compiler nach Arm Assembly. Auffällig ist hier, daß dieser Code den Funktions- Boilerplate enthält ( stmdb und ldmia). Besonders interessant ist auch, daß die ldmia nie erreicht wird, da der Prozessor vorher verzweigt (mov pc,r2 ist ein Sprung in Arm Schreibweise). Das bedeutet, daß der Stack-Abbau in ldmia nie durchgeführt wird, und der Stack in kurzer Zeit überlaufen würde. Also ist die Verwendung des Microsoft Compilers auf Arm nicht machbar für Tail Recursion. Listing schliesslich zeigt dieselbe Funktion in Arm Assembler, diesmal compiliert von OS9 s C Compiler. Hier fällt auf, daß der Code nicht nur deutlich komplizierter ist, als Microsoft s Code. Insbesondere scheint dieser Compiler die Tail-Recursion nicht zu erkennen. Als letzte Instruktion findest sich ein ldmfd, das offenbar die Register, inkl. den Program Counter (PC) vom Stack restauriert (symmetrisch zur stmfd Instruktion). Alles in allem scheint eine solche Implementierung nur attraktiv zu sein für den Microsoft Compiler für x86. Alle anderen erkennen offenbar die Tail- Recursion nicht korrekt, oder lösen sie zumindest nicht elegant auf. Verwenden von Inline-Assembly Statt der &&label und goto *addr Syntax lässt sich Inline-Assembler-Code verwenden, um die gewünschte Funktionalität eingehen. An dieser Stelle soll darauf verzichtet werden, dies im Detail zu erläutern. Es soll nur festgestellt werden, daß diese Option nur im äußersten Notfall zur Anwendung kommen sollte, da es sich hierbei um hochgradig Prozessor-spezifischen Code handelt. Dies ist aufwendig zu entwickeln, und zwar für jede mögliche _Add PROC ; Line 14 mov eax, DWORD PTR _pc$[esp-4] mov ecx, DWORD PTR [eax] add DWORD PTR _acc, ecx add eax, 4 ; Line 15 lea edx, DWORD PTR [eax+4] mov eax, DWORD PTR [eax] mov DWORD PTR _pc$[esp-4], edx jmp eax Listing 4.5.9: Add Funktion, compiliert nach x86 von Microsoft CC

74 KAPITEL 4. UMSETZUNG 66 Add PROC ; Line 13 $LN5@Add stmdb sp!, {r4, lr} $M3286 ; Line 14 ldr r4, [pc, #0x20] ldr r1, [r0], #4 ; Line 15 ldr r3, [r4] ldr r2, [r0] add r3, r3, r1 str r3, [r4] add r0, r0, #4 mov lr, pc mov pc, r2 ; Line 16 ldmia sp!, {r4, pc} $LN6@Add DCD acc $M3287 ENDP ; Add Listing : Add Funktion, compiliert nach Arm von Microsoft CC

75 KAPITEL 4. UMSETZUNG 67 =Add mov R11,R13 stmfd R13!,0x4001 sub R13,R13,8 str R11,[R13] mov R8,R7 ldr R7,[R8],4 mov R9,(=_$s0)&0xfff00000 add R9,R9,(=_$s0)&0xff000 add R9,R9,R6 ldr R10,[R9,(=_$s0)&0xfff] add R10,R10,R7 str R10,[R9,(=_$s0)&0xfff] add R7,R8,4 ldr R8,[R8,0] mov R14,R15 mov R15,R8 add R13,R13,8 ldmfd R13!,0x8001 Listing : Add Funktion, compiliert nach Arm von Ultra-C

76 KAPITEL 4. UMSETZUNG 68 Prozessor-Plattform separat, und schwer zu handhaben. Grundsätzlich sollte auf Inline-Assembly verzichtet werden. Äquivalente Compiler Erweiterungen Es besteht eine gewisse Wahrscheinlichkeit, daß andere Compiler ähnliche Syntax-Erweiterungen kennen, mit deren Hilfe die Implementierung eines Threaded Dispatchers möglich ist. Dies müsste gegebenenfalls in einem späteren Projekt untersucht werden. Dies konnte aus Zeitgründen nicht im Rahmen dieser Arbeit durchgeführt werden. Compiler Optimierungen Manche Compiler unterstützen Optimierungsoptionen, die den Compiler veranlassen, einen While-Switch-Dispatcher in einen Threaded-Dispatcher zu compilieren. Beispielsweise unterstützt der GCC ein Flag -funswitch-loops, das zu diesem Zweck implementiert wurde (Es muss hier gesagt werden, daß dieses Flag laut einem Test offenbar nicht besonders gut funnktioniert, zumindest deutlich weniger Effekt hat, als die hier vorgestellte manuelle Implementierung). C Compiler können theoretisch bei einem dichten Switch-Statement die Schleifenbedingung in jeden Case Zweig eincompilieren und auf diese Weise Threaded-Dispatching unterstützen. Inwiefern das durch z.b: Microsoft C Compiler möglich ist, müsste untersucht werden. Das würde aber ebenfalls den Rahmen dieser Diplomarbeit sprengen. Verwenden von GCC GCC Portierungen gibt es für so gut wie alle Plattformen. JamaicaVM verwendet andere Compiler in der Regel nur dann, wenn diese performanteren Code liefern. Das ist zum Beispiel der Fall auf Windows CE mit dem Microsoft Compiler. Möglicherweise könnte die Verwendung von GCC für den Interpreter aber trotzdem sinnvoll sein. Es müsste untersucht werden, ob man verschiedene Compiler für verschiedene Programmteile verwenden kann, und trotzdem ein einheitliches Executable erzeugen kann. Auch dies soll nicht teil dieser Diplomarbeit sein. Fallback While-Switch-Loop Falls keine der oben genannten Optionen möglich ist, kann immer noch auf die bisherige While-Switch-Implementierung zurückgegriffen werden. Dies ist

77 KAPITEL 4. UMSETZUNG 69 relativ einfach durch bedingte Compilierung zu realisieren. Zum Beispiel definiert der GCC implizit das Makro GNUC, das man abfragen kann um GCC spezifischen Code zu compilieren. 4.6 Superinstructions Bei einem einfachen Interpreter (wie z.b. bei der JamaicaVM bisher zum Einsatz kam) wird jede Bytecode Instruktion einzeln ausgeführt. Zu diesem Zweck existiert für jeden Opcode ein Programmfragment zu dem der Interpreter verzweigt, wenn die entsprechende Instruktion im Bytecodestream erkannt wird. Die Idee von Superinstructions ist nun, häufig auftretende Gruppen von Bytecode Instruktionen sinnvoll zusammenzufassen und gemeinsam auszuführen. Beispielsweise könnte die Folge iload1 iload2 iadd istore1 (addiere die lokalen Variablen 1 und 2 und speichere das Ergebnis in der lokalen Variable 1) durch eine Instruktion iadd_1_2_1 ersetzt werden, die eben genau das macht, aber mit weniger Overhead. Setzen wir das eben genannte Beispiel fort und schauen uns an, was ein einfacher Interpreter alles machen muss um die Bytecodesequenz auszuführen: 1. Dispatch. 2. Lade lokale Variable Lege sie auf den Stack ab. 4. Dispatch. 5. Lade lokale Variable Lege sie auf den Stack ab. 7. Dispatch. 8. Hole einen Integer vom Stack. 9. Hole noch einen Integer vom Stack. 10. Addiere sie. 11. Speichere Ergebnis auf Stack. 12. Dispatch.

78 KAPITEL 4. UMSETZUNG Hole einen Integer vom Stack. 14. Speichere Wert in lokale Variable 1. Pro Schritt muss mindestens ein Speicherzugriff ausgeführt werden. Beim Dispatchen kommen noch 3 Sprünge dazu, einer zum Bytecodehandler, einer ans Ende der Interpreter Schleife und einer zum Anfang der Interpreter Schleife (zur Dispatch-Optimierung in einem anderen Kapitel mehr). Das dispatchen ist besonders schlecht da es oft eine Entleerung der Prozessor- Pipeline bedeutet. Könnten wir die 4 Bytecode Instruktionen in einem einzigen Bytecode Handler ausführen hätten wir folgende Operationen: 1. Dispatch. 2. Lade lokale Variable Lade lokale Variable Addiere beide Werte. 5. Speichere Ergebnis in lokaler Variable 1. Wir haben also den Zugriff auf den Operandenstack für diese Sequenz vollkommen eliminiert sowie das Dispatching minimiert. Im folgenden soll erörtert werden, wie geeignete Sequenzen für Superinstructions gefunden werden können und wie Superinstructions im Interpreter umgesetzt werden können Funktionsweise von Superinstructions Die Implementierung von Superinstructions erfordert Eingriffe an zwei Stellen: 1. Im ClassLoader müssen bekannte Bytecodesequenzen erkannt und durch speziell kodierte Superinstructions ersetzt werden. Es wird also der Bytecode Stream einer Java Methode direkt im Arbeitsspeicher modifiziert indem neue bisher illegale Opcodes eingefügt werden. Es ist wichtig zu beachten das der resultierende Bytecode Stream kein legaler Java Bytecode mehr ist. Insbesondere muss jeglicher Programmcode der für eine Java Methode legalen Bytecode ausgeben soll angepasst werden, so daß Superinstructions wieder zurücktransformier werden. Das ist insbesondere für die Instrumentation API sowie für JVMTI (Java VM Tool Interface) relevant. Diese werden allerdings bisher nicht von der Jamaica VM unterstützt.

79 KAPITEL 4. UMSETZUNG Im Interpreter müssen zusätzliche Bytecode Handler für die zusätzlichen Opcodes für Superinstructions hinzugefügt werden. Die Standard Bytecodes für Java VM Instruktionen belegen den Bereich 0x00 bis 0xc9. Der Bytecode 0xca ist für Breakpoints im Java Debugger reserviert. Die Opcodes 0xfe und 0xff sind durch die Java VM Spezifikation reserviert, werden aber nicht benutzt. Es sind also praktisch noch 53 Slots frei für 1-Byte lange Opcodes. Zusätzlich gäbe es die Möglichkeit, 2 Byte lange Opcodes einzuführen, sollte sich herausstellen, daß 1 Byte lange Opcodes nicht ausreichen. Dazu müsste ein Opcode reserviert werden (z.b: 0xfd), bei dem ein weiteres Byte dazu dient, zwischen weiteren maximal 255 Instruktionen zu verzweigen. Es ist allerdings unwahrscheinlich, daß dies nötig wird, da der Binärcode für den Interpreter schliesslich auch mit jedem neuen Bytecode Handler wächst, was zum einen den Speicherverbrauch negativ beeinflusst und ausserdem für Systeme mit kleinem Cache ungünstig ist Auswahl von Superinstructions Leider sieht die Java VM Spezifikation eine Vielzahl von Instruktionen zum Laden von und Speichern auf den Stack vor (z.b. iload_0, iload_1, iload_2, iload_3 sowie iload mit Parameter). Diese können nun zusammen mit den eigentlichen Operationen (z.b. iadd) eine überwältigende Vielzahl von Kombinationen bilden. Bezieht man nun noch mit ein, daß nicht alle Operationen nach dem Muster load load do store gestrickt sind, sondern daß oftmals schon ein Wert aus einer vorigen Operation auf dem Stack vorhanden ist (load do store) oder das Ergebnis nicht in einer lokalen Variable gespeichert sondern an die nächste Operation weitergereicht wird (load load do), wird deutlich, daß es keinen Sinn macht, alle möglichen Superinstruktionen zu implementieren. In der Realität werden natürlich bei weitem nicht alle Kombinationen verwendet. Vielmehr gibt es Sequenzen die häufiger bzw. weniger häufig vorkommen. Um eine Vorstellung zu bekommen, welche Bytecode Sequenzen besonders häufig vorkommen, und die es daher wert wären als Superinstruktion zu implementieren, scheint es sinnvoll eine Reihe von einfachen Statistiken zu erstellen. Zunächst einmal soll ermittelt werden, welche Muster einer vorgegebenen Größe wie häufig ausgeführt werden. Zu diesem Zweck habe ich einige Funktionen in den Interpreter eingefügt: superstat(char) wird für jede Bytecode Instruktion aufgerufen, um die Statistik zu führen, und printsuperstats() wird alle Bytecodes aufgerufen um die Statistik auf der Konsole auszugeben.

80 KAPITEL 4. UMSETZUNG 72 Die Funktion superstat(char) verwaltet ein kleines Array in dem die letzten N Bytecodes im Stream gespeichert werden. Bei jedem neuen Aufruf wird das Array verschoben und vorn der neue Bytecode eingefügt. Dieses Array repräsentiert das aktuelle Muster. Dieses wird dann in einer Datenstruktur, die die Häufigkeiten für jedes Muster speichert, gesucht, und der zugehörige Zähler um 1 inkrementiert. Weiterhin wird diese Liste durch eine Bubblesort-artige Methode sortiert, aber nicht die ganze Liste auf einmal, sondern es wird nur der betreffende Eintrag N mit dem vorigen N-1 vertauscht, falls die Zähler indizieren, daß N-1 weniger häufig auftrat als N. Auf diese Weise erhält man keine perfekt sortierte Liste, aber eine einigermaßen sortierte Liste mit dem Effekt, daß die Suche angenehm schnell geht. Zumindest schnell genug für diese Zwecke. Weitere Optimierungen sind hier nicht notwendig, da es sich ja um eine einmalige Statistik handelt. Die Ergebnisse sind in den Tabellen 4.5 bis 4.7 zusammengefasst. Aus Platzgründen führe ich hier jeweils die häufigsten Bytecode Sequenzen auf. Aus allen drei Statistiken zusammen kann man folgende Schlüsse ableiten: Wie schon in den Bytecode-Statistiken vermutet, spielen Sequenzen mit aload_0 eine besonders kritische Rolle. Besonders auffällig sind dabei Sequenzen mit aload_0 getfield. Es gibt eine unwahrscheinliche Vielzahl möglicher Kombinationen von Bytecodes. Aufgrund der Vielzahl der möglichen Kombinationen sowie der zeitlichen Beschränkung des Projekts wurde im Rahmen der Diplomarbeit nur ein spezieller Fall exemplarisch betrachtet. Es wurde die performanz-intensivste Sequenz aload_0 getfield ausgewählt und implementiert. Dies dient unter anderem auch der Evaluierung des Performanzgewinns im Verhältnis zum Implementierungsaufwand Implementierung Die Implementierung von Superinstructions soll hier am Beispiel der Sequenz aload_0 getfield erläutert werden. Im ersten Schritt wurde der Klassen-Lade-Code erweitert um eine zusätzliche Routine, die den geladenen Bytecode nach Superinstruction-Sequenzen scannt und gefundene Sequenzen durch einen neuen Bytecode ersetzt wie im folgenden Code-Fragment angedeutet. while (i < code_length) { int8 bc = jamaicaclasses_getbytecode(method, i);

81 KAPITEL 4. UMSETZUNG 73 Bytecodesequenz Häufigkeit aload 0 getfield dup getfield aload 0 dup getfield aload sipush iand getfield iload iadd istore putfield aload istore iload iload iload iadd putfield aload 1 iload iload aload iconst 1 iadd iinc iload istore iinc iinc baload iload 2 iinc baload sipush getfield dup x dup x1 iconst iload iadd putfield return getfield invokevirtual iload bipush getfield iload iload ifge iand iadd bipush iand dup istore getfield iconst iload 3 ifge iconst 1 isub aload 0 aload getstatic iload iinc iload bipush iushr isub putfield iload 1 isub ixor istore Tabelle 4.5: Superinstruction Statistik für 2-Bytecode-Sequenzen

82 KAPITEL 4. UMSETZUNG 74 Bytecodesequenz Häufigkeit aload 0 dup getfield aload 0 getfield aload getfield aload 0 getfield iload aload 1 iload aload 1 iload 2 iinc iload 2 iinc baload iconst 1 iadd putfield baload sipush iand getfield dup x1 iconst dup getfield dup x dup x1 iconst 1 iadd aload 0 getfield iload dup getfield iload iadd istore iload getfield aload 0 dup istore iload iload aload 0 getfield invokevirtual aload 0 getfield iload iload iadd istore iload iload iadd iinc iload iflt sipush iand iadd iinc baload sipush iadd istore goto iand iadd istore putfield aload 0 getfield putfield aload 0 dup aload 0 getfield iconst iinc iload 3 iflt iload bipush iushr baload ixor sipush ixor sipush iand sipush iand iaload bipush iushr ixor iaload iload bipush iand iaload iload getstatic iload aload iinc baload ixor iushr ixor istore ixor istore goto iadd putfield baload getfield iload 1 if icmpge iconst 1 isub iand ishl iconst 1 isub getfield iconst 1 iload Tabelle 4.6: Superinstruction Statistik für 3-Bytecode-Sequenzen

83 KAPITEL 4. UMSETZUNG 75 Bytecodesequenz Häufigkeit getfield aload 0 dup getfield aload 0 getfield aload 0 getfield aload 0 dup getfield iconst dup getfield iconst 1 isub aload iload aload 0 iload iload aload 0 iload invokevirtual aload 0 getfield getfield aload getfield iconst 1 isub dup x iconst 1 isub dup x1 putfield isub dup x1 putfield aaload dup x1 putfield aaload astore getfield getfield aload 0 dup putfield aaload astore 1 goto dup getfield dup x1 iconst aload 0 dup getfield dup x dup x1 iconst 1 iadd putfield getfield dup x1 iconst 1 iadd aload 0 getfield aload 0 dup iload 3 iload iadd aload aload iload 3 iload iadd iload iadd aload 1 iload iadd aload 1 iload invokevirtual iconst 1 iadd putfield iload aload 0 aload 1 putfield aload aload 0 getfield astore 1 aload getfield astore 1 aload 1 ifnonnull getfield iload 1 aload 0 getfield getfield iconst 1 isub putfield putfield aload 0 getfield iconst aload 1 putfield aload 0 getfield aload 0 aload 1 getfield putfield getfield aload 0 getfield getfield aload 1 getfield putfield aload aload 0 getfield iinc iload getfield iinc iload iaload iinc iload iaload tableswitch aload 0 aload 0 getfield invokeinterface aload 0 getfield iload 1 aload isub putfield aload 0 getfield getfield putfield aload 0 aload Tabelle 4.7: Superinstruction Statistik für 4-Bytecode-Sequenzen

84 KAPITEL 4. UMSETZUNG 76 } int32 next_i = i + sizeofbc(method, i); /* Replace a sequence of aload_0 and getfield by optimized * superinstruction getfield_this. */ if (prev == O_aload_0 && bc == O_getield ) { setbytecode(method, i - 1, O_getfield_this); } prev = bc; i = next_i; Eine Bytecode-Sequenz aload_0 getfield $X $Y (wobei XandY zusammen den Field-Index des zugegriffenen Feldes ergeben) wird also ersetzt durch eine Sequenz getfield_this getfield $X $Y. Weiterhin wird ein neuer Bytecode-Handler für getfield_this benötigt, der, vereinfacht gesagt, wie folgt aussieht: getfield_this ( $bnbc $soff -- ) (0xd7) jamaica_ref o; /* This corresponds to the aload_0 part. */ JAMAICA_LOCAL_R(ct, 0, o); do_bc_getfield(ct, soff, o); Der Bytecode-Handler überspringt dabei den alten getfield Opcode durch den Parameter $bnbc. Danach wird wie beim normalen getfield der Field-Offset eingelesen. In der Implementierung des Handlers wird nun die lokale Variable 0 gelesen und direkt an die Funktion do_bc_getfield weitergereicht, statt sie auf den (Java-) Stack zwischenzuspeichern und wieder zu lesen, wie es bei einer normalen aload_0 getfield Sequenz geschehen würde Mögliche Probleme Ein sehr wichtiges Problem bei der Implementierung von Superinstructions sind Verzweigungen aller Art. Es kann passieren, daß ein Sprung, z.b. ein if Verzweigung, in die Mitte einer Bytecode-Sequenz springt. Dies würde die Superinstruction gewissermaßen aufbrechen und kann zu Fehlern führen, wenn die ursprüngliche Instruktionen ungünstig ersetzt wurden. Es bieten sich prinzipiell zwei Lösungsmöglichkeiten zur Lösung dieses Problems an: Der Klassen-Lader kann eine strukturelle Analyse des gesamten Codes einer Methode durchführen, und die Ersetzung nur vornehmen, wenn

85 KAPITEL 4. UMSETZUNG 77 festgestellt wird, daß keine Verzweigung in eine Superinstruction hineinspringt. Die Ersetzung kann auf eine Weise vorgenommen werden, die kompatibel mit Sprüngen ist. Das heisst, daß wenn eine Verzweigung in die Mitte einer Superinstruktion springt, die ursprüngliche Funktionalität ausgeführt wird. Im Fall der getfield_this Implementierung wurde die zweite Möglichkeit gewählt, da sie wesentlich einfacher zu implementieren ist. Wenn der Interpreter auf den getfield_this stößt, dann führt er den optimierten Handler aus. Dieser überspringt schlichtweg die folgende ursprüngliche getfield Instruktion, die danach folgt. Springt allerdings der Interpreter durch eine Verzweigung direkt zu der getfield Instruktion, so wird diese ganz normal ausgeführt. Es wurde also keine Änderung an der Struktur der Bytecodes sowie deren Semantik durchgeführt.

86 Kapitel 5 Ergebnisse 5.1 Auswertung der verschiedenen Technologien Im vorigen Abschnitt wurden die Performanz-Verbesserungen auf verschiedenen Zielsystemen gegenübergestellt und diskutiert. In diesem Abschnitt sollen die verschiedenen angewendeten Techniken betrachtet werden. Das Ziel soll sein, festzustellen, welche Technik die meiste Performanz eingebracht hat, und warum. Als Grundlage für die Performanzbestimmung dient selbstverständlich wieder das Dacapo Benchmark. Im Gegensatz zum vorigen Abschnitt werden hier der Einfachheit und Vergleichbarkeit halber alle Benchmarks auf ein und demselben System ausgeführt. Es handelt sich in diesem Fall um ein Linux System mit einem AMD Athlon Prozessor, der mit 1500MHz getaktet ist, und 768 MB Arbeitsspeicher besitzt. Die bestimmten Werte sind jeweils relativ zu verstehen, immer einmal mit und einmal ohne die betreffenden Optimierung. Die jeweilige Performanz- Differenz kann dann später verwendet werden, um die verschiedenen Techniken einmal gegenüberzustellen. Weiterhin wurde versucht, die Performanz- Messungen in der Reihenfolge durchzuführen, in der sie implementiert wurden. Das heisst, die Ergebnisse sind auch untereinander relativ zu verstehen. Z.B. sind bei den Messungen für die Superinstructions alle vorigen Optimierungen auch eingeschaltet, die Messungen für die Stackzugriffe allerdings haben ansonsten keinerlei Optimierung. Dies ermöglicht auch einen chronologischen Vergleich und tut den Einzelergebnissen keinen Nachteil. 78

87 KAPITEL 5. ERGEBNISSE Optimierung der Stack-Zugriffe Der erste Schritt bei der Performanz-Optimierung bestand aus Code-Cleanup, der Einführung des Interpreter-Generators sowie damit einhergehend eine Konsolidierung und Optimierung des Stack-Zugriffe innerhalb der Bytecode- Handler. Die Tabelle 5.1 stellt das Performanz-Verhalten zu Beginn des Projekts dar, sowie das Verhalten nach Implementierung dieser ersten Optimierungen. Es ist leider nicht möglich, die einzelnen Aspekte (VM-Generator, Stack- Zugriffe und Cleanup) einzeln zu betrachten, weil diese Hand-in-Hand gingen. Beispielsweise wurde die Optimierung des Stack-Zugriffs z.t. erst durch Verwendung des VM-Generators sinnvoll machbar. Es lässt sich feststellen, daß es einen durchschnittlichen Performanz-Gewinn von ca. 10% gibt. Es sollte darauf hingewiesen werden, daß ein Benchmark (jython) sogar eine Verschlechterung der Performant zeigt. Dies lässt sich auch bei wiederholten Benchmarks zeigen, es handelt sich hier also nicht um eine zufällige einmalige Abweichung in der Messung. Leider blieb mir im Zeitraum der Diplomarbeit nicht die Zeit, die Ursachen dafür zu analysieren. Benchmark Ohne Optimierung Mit Stack-Optimierung antlr bloat fop hsqldb jython luindex lusearch pmd Gesamt Differenz (11,6 %) Tabelle 5.1: Performanz mit und ohne Stack Optimierung Threaded Dispatching Die zweite große Optimierung war die Implementierung des Threaded Dispatching. Die Tabelle 5.2 fasst die Performanz-Verbesserung dieser Optimierung zusammen. Es lässt sich zum einen feststellen, daß das Threaded Dispatching die bisher beste Performanz-Verbesserung gebracht hat. Unglücklicherweise lässt sich Threaded Dispatching nicht auf allen Plattformen ohne weiteres

88 KAPITEL 5. ERGEBNISSE 80 implementieren, wie im entsprechenden Abschnitt erläutert. Diese Performanzverbesserung kommt also vorerst nur Plattformen zugute, auf denen Jamaica mit dem GCC übersetzt wird. Benchmark Ohne Threading Mit Threading antlr bloat fop hsqldb jython luindex lusearch pmd Gesamt Differenz (29,1 %) Tabelle 5.2: Performanz mit und ohne Threaded Dispatching Superinstructions In Tabelle 5.3 sind die Performanzwerte der Dacapo Benchmarks jeweils mit und ohne Superinstructions Unterstützung zusammengefasst. Zur Messung wurde einmal die Endversion des Jamaica Interpreters verwendet, und einmal der Code zur Generierung der Superinstructions auskommentiert. Die Optimierung für die Stack-Zugriffe und Threaded Dispatching sind bei beiden Messungen eingeschaltet. Es zeigt sich, daß die Implementierung von Superinstructions ungefähr 10% Performanzgewinn bringt. Es muss hier betont werden, daß nur sehr wenige (wichtige) Superinstructions implementiert wurden. Das Ergebnis kann sicher durch Implementierung weiterer Superinstruction verbessert werden (siehe auch Kapitel Zusammenfassung Die unterschiedlichen Optimierungstechniken haben in unterschiedlichem Grade zur Verbesserung der Gesamtperformanz der Jamaica VM beigetragen. Folgende Charakteristiken liessen sich feststellen: Die Optimierung des Stack-Zugriffs und die Verwendung eines generierten Interpreters führten zwar insgesamt zu einer Verbesserung von ca. 10%, aber interessanterweise sind einzelne Benchmarks trotzdem langsamer geworden.

89 KAPITEL 5. ERGEBNISSE 81 Den größten Performanzgewinn hat bisher die Implementierung des Threaded Dispatching gebracht. Das Threaded Dispatching bewirkt eine deutliche Verringerung des Dispatch-Overheads. Die Verringerung der Anzahl der Sprünge, die der Prozessor pro Dispatch-Vorgang ausführen muss führt zu einem deutlich besseren Vorhersageverhalten in der Prozessor- Pipeline. Insgesamt kann ein Performanzgewinn von ca 20% festgestellt werden. Nachteilig ist allerdings die schlechte Portabilität. Die im Rahmen dieser Diplomarbeit umgesetzte Implementierung verwendet GCC-spezifische Erweiterungen der C-Syntax, um Threaded Dispatching implementieren zu können. Alternativen für Plattformen mit einem anderen Compiler als dem GCC wurden erörtert und sollten in einem Folgeprojekt implementiert werden. Die Implementierung einiger wichtiger Superinstructions führte zu einer Performanzsteigerung von ca 10%. Die nötige Infrastruktur für die Implementierung von Superinstructions wurde implementiert. Es spricht also nicht viel dagegen, weitere Superinstructionen zu implementieren. Es muss allerdings abgewogen werden, wie weit man hier gehen möchte, da jede zusätzliche Superinstruktion selbstverständlich das Interpreter-Executable aufbläht. Dies kann auf manchen Systemen sogar kontraproduktiv sein, nämlich wenn dies zu einem schlechteren Prozessor-Cache-Verhalten führt (vor allem bei sehr kleinen Prozessor- Caches). In 5.2 wird die Performanz des verbesserten Interpreters insgesamt mit der Performanz von der letzten Jamaica Release Version 3.0 sowie der Jamaica Entwicklerversion 3.1 alpha zu Beginn des Projektes gegenübergestellt. Es Benchmark Ohne Superinstructions Mit Superinstructions antlr bloat fop hsqldb jython luindex lusearch pmd Gesamt Differenz (10,6 %) Tabelle 5.3: Performanz mit und ohne Superinstructions

90 KAPITEL 5. ERGEBNISSE 82 lässt sich zusammenfassend sagen, daß die Performanz gegenüber Projektbeginn um ca. 40% gesteiger werden konnte und gegenüber der JamaicaVM Version 3.0 sogar um ca. 45%. Abbildung 5.1: Vergleich der Optimierungstechniken Abbildung 5.2: Vergleich der Jamaica Versionen

Diplomarbeit Antrittsvortrag

Diplomarbeit Antrittsvortrag Diplomarbeit Antrittsvortrag Christian Müller Run-time byte code compilation, interpretation and optimization for Alice Betreuer: Guido Tack Verantwortlicher Prof.: Gert Smolka Die nächsten 15 Minuten...

Mehr

Kompilieren und Linken

Kompilieren und Linken Kapitel 2 Kompilieren und Linken Bevor wir uns auf C++ selbst stürzen, brauchen wir einiges Vorgeplänkel, wie man komfortabel ein größeres C++- kompilieren kann. Mit Java stellt sich der Kompiliervorgang

Mehr

J.5 Die Java Virtual Machine

J.5 Die Java Virtual Machine Java Virtual Machine Die Java Virtual Machine 22 Prof. Dr. Rainer Manthey Informatik II Java-Compiler und Java Virtual Machine Quellcode-Datei class C... javac D.java Java-Compiler - Dateien class class

Mehr

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren Lineargleichungssysteme: Additions-/ Subtraktionsverfahren W. Kippels 22. Februar 2014 Inhaltsverzeichnis 1 Einleitung 2 2 Lineargleichungssysteme zweiten Grades 2 3 Lineargleichungssysteme höheren als

Mehr

4D Server v12 64-bit Version BETA VERSION

4D Server v12 64-bit Version BETA VERSION 4D Server v12 64-bit Version BETA VERSION 4D Server v12 unterstützt jetzt das Windows 64-bit Betriebssystem. Hauptvorteil der 64-bit Technologie ist die rundum verbesserte Performance der Anwendungen und

Mehr

In 15 einfachen Schritten zum mobilen PC mit Paragon Drive Copy 10 und Microsoft Windows Virtual PC

In 15 einfachen Schritten zum mobilen PC mit Paragon Drive Copy 10 und Microsoft Windows Virtual PC PARAGON Technologie GmbH, Systemprogrammierung Heinrich-von-Stephan-Str. 5c 79100 Freiburg, Germany Tel. +49 (0) 761 59018201 Fax +49 (0) 761 59018130 Internet www.paragon-software.com Email sales@paragon-software.com

Mehr

In 12 Schritten zum mobilen PC mit Paragon Drive Copy 11 und Microsoft Windows Virtual PC

In 12 Schritten zum mobilen PC mit Paragon Drive Copy 11 und Microsoft Windows Virtual PC PARAGON Technologie GmbH, Systemprogrammierung Heinrich-von-Stephan-Str. 5c 79100 Freiburg, Germany Tel. +49 (0) 761 59018201 Fax +49 (0) 761 59018130 Internet www.paragon-software.com Email sales@paragon-software.com

Mehr

Es sollte die MS-DOS Eingabeaufforderung starten. Geben Sie nun den Befehl javac ein.

Es sollte die MS-DOS Eingabeaufforderung starten. Geben Sie nun den Befehl javac ein. Schritt 1: Installation des Javacompilers JDK. Der erste Start mit Eclipse Bevor Sie den Java-Compiler installieren sollten Sie sich vergewissern, ob er eventuell schon installiert ist. Gehen sie wie folgt

Mehr

Grundlagen von Python

Grundlagen von Python Einführung in Python Grundlagen von Python Felix Döring, Felix Wittwer November 17, 2015 Scriptcharakter Programmierparadigmen Imperatives Programmieren Das Scoping Problem Objektorientiertes Programmieren

Mehr

Übung: Verwendung von Java-Threads

Übung: Verwendung von Java-Threads Übung: Verwendung von Java-Threads Ziel der Übung: Diese Übung dient dazu, den Umgang mit Threads in der Programmiersprache Java kennenzulernen. Ein einfaches Java-Programm, das Threads nutzt, soll zum

Mehr

Schritt-Schritt-Anleitung zum mobilen PC mit Paragon Drive Copy 10 und VMware Player

Schritt-Schritt-Anleitung zum mobilen PC mit Paragon Drive Copy 10 und VMware Player PARAGON Technologie GmbH, Systemprogrammierung Heinrich-von-Stephan-Str. 5c 79100 Freiburg, Germany Tel. +49 (0) 761 59018201 Fax +49 (0) 761 59018130 Internet www.paragon-software.com Email sales@paragon-software.com

Mehr

Anleitung über den Umgang mit Schildern

Anleitung über den Umgang mit Schildern Anleitung über den Umgang mit Schildern -Vorwort -Wo bekommt man Schilder? -Wo und wie speichert man die Schilder? -Wie füge ich die Schilder in meinen Track ein? -Welche Bauteile kann man noch für Schilder

Mehr

Grundlagen verteilter Systeme

Grundlagen verteilter Systeme Universität Augsburg Insitut für Informatik Prof. Dr. Bernhard Bauer Wolf Fischer Christian Saad Wintersemester 08/09 Übungsblatt 3 12.11.08 Grundlagen verteilter Systeme Lösungsvorschlag Aufgabe 1: a)

Mehr

In 15 Schritten zum mobilen PC mit Paragon Drive Copy 14 und VMware Player

In 15 Schritten zum mobilen PC mit Paragon Drive Copy 14 und VMware Player PARAGON Technologie GmbH, Systemprogrammierung Heinrich-von-Stephan-Str. 5c 79100 Freiburg, Germany Tel. +49 (0)761 59018-201 Fax +49 (0)761 59018-130 Internet www.paragon-software.com E-Mail sales@paragon-software.com

Mehr

Die Programmiersprache Java. Dr. Wolfgang Süß Thorsten Schlachter

Die Programmiersprache Java. Dr. Wolfgang Süß Thorsten Schlachter Die Programmiersprache Java Dr. Wolfgang Süß Thorsten Schlachter Eigenschaften von Java Java ist eine von der Firma Sun Microsystems entwickelte objektorientierte Programmiersprache. Java ist......a simple,

Mehr

In 15 Schritten zum mobilen PC mit Paragon Drive Copy 11 und VMware Player

In 15 Schritten zum mobilen PC mit Paragon Drive Copy 11 und VMware Player PARAGON Technologie GmbH, Systemprogrammierung Heinrich-von-Schritthan-Str. 5c 79100 Freiburg, Germany Tel. +49 (0) 761 59018201 Fax +49 (0) 761 59018130 Internet www.paragon-software.com Email sales@paragon-software.com

Mehr

Systeme 1. Kapitel 6. Nebenläufigkeit und wechselseitiger Ausschluss

Systeme 1. Kapitel 6. Nebenläufigkeit und wechselseitiger Ausschluss Systeme 1 Kapitel 6 Nebenläufigkeit und wechselseitiger Ausschluss Threads Die Adressräume verschiedener Prozesse sind getrennt und geschützt gegen den Zugriff anderer Prozesse. Threads sind leichtgewichtige

Mehr

Arbeiten mit UMLed und Delphi

Arbeiten mit UMLed und Delphi Arbeiten mit UMLed und Delphi Diese Anleitung soll zeigen, wie man Klassen mit dem UML ( Unified Modeling Language ) Editor UMLed erstellt, in Delphi exportiert und dort so einbindet, dass diese (bis auf

Mehr

Java Virtual Machine (JVM) Bytecode

Java Virtual Machine (JVM) Bytecode Java Virtual Machine (JVM) durch Java-Interpreter (java) realisiert abstrakte Maschine = Softwareschicht zwischen Anwendung und Betriebssystem verantwortlich für Laden von Klassen, Ausführen des Bytecodes,

Mehr

mywms Vorlage Seite 1/5 mywms Datenhaltung von Haug Bürger

mywms Vorlage Seite 1/5 mywms Datenhaltung von Haug Bürger mywms Vorlage Seite 1/5 mywms Datenhaltung von Haug Bürger Grundlegendes Oracle9i PostgreSQL Prevayler Memory mywms bietet umfangreiche Konfigurationsmöglichkeiten um die Daten dauerhaft zu speichern.

Mehr

! " # $ " % & Nicki Wruck worldwidewruck 08.02.2006

!  # $  % & Nicki Wruck worldwidewruck 08.02.2006 !"# $ " %& Nicki Wruck worldwidewruck 08.02.2006 Wer kennt die Problematik nicht? Die.pst Datei von Outlook wird unübersichtlich groß, das Starten und Beenden dauert immer länger. Hat man dann noch die.pst

Mehr

10 Erweiterung und Portierung

10 Erweiterung und Portierung 10.1 Überblick In vielen Fällen werden Compiler nicht vollständig neu geschrieben, sondern von einem Rechnersystem auf ein anderes portiert. Das spart viel Arbeit, ist aber immer noch eine sehr anspruchsvolle

Mehr

Online Newsletter III

Online Newsletter III Online Newsletter III Hallo zusammen! Aus aktuellem Anlass wurde ein neuer Newsletter fällig. Die wichtigste Neuerung betrifft unseren Webshop mit dem Namen ehbshop! Am Montag 17.10.11 wurde die Testphase

Mehr

Installation der SAS Foundation Software auf Windows

Installation der SAS Foundation Software auf Windows Installation der SAS Foundation Software auf Windows Der installierende Benutzer unter Windows muss Mitglied der lokalen Gruppe Administratoren / Administrators sein und damit das Recht besitzen, Software

Mehr

Informationen zur Verwendung von Visual Studio und cmake

Informationen zur Verwendung von Visual Studio und cmake Inhaltsverzeichnis Informationen zur Verwendung von Visual Studio und cmake... 2 Erste Schritte mit Visual Studio... 2 Einstellungen für Visual Studio 2013... 2 Nutzung von cmake... 6 Installation von

Mehr

Einrichtung des Cisco VPN Clients (IPSEC) in Windows7

Einrichtung des Cisco VPN Clients (IPSEC) in Windows7 Einrichtung des Cisco VPN Clients (IPSEC) in Windows7 Diese Verbindung muss einmalig eingerichtet werden und wird benötigt, um den Zugriff vom privaten Rechner oder der Workstation im Home Office über

Mehr

L10N-Manager 3. Netzwerktreffen der Hochschulübersetzer/i nnen Mannheim 10. Mai 2016

L10N-Manager 3. Netzwerktreffen der Hochschulübersetzer/i nnen Mannheim 10. Mai 2016 L10N-Manager 3. Netzwerktreffen der Hochschulübersetzer/i nnen Mannheim 10. Mai 2016 Referentin: Dr. Kelly Neudorfer Universität Hohenheim Was wir jetzt besprechen werden ist eine Frage, mit denen viele

Mehr

Programmierung für Mathematik (HS13)

Programmierung für Mathematik (HS13) software evolution & architecture lab Programmierung für Mathematik (HS13) Übung 5 1 Aufgabe: Eclipse IDE 1.1 Lernziele 1. Die Entwicklungsumgebung Eclipse einrichten. 2. Eclipse kennen lernen und mit

Mehr

DOKUMENTATION VOGELZUCHT 2015 PLUS

DOKUMENTATION VOGELZUCHT 2015 PLUS DOKUMENTATION VOGELZUCHT 2015 PLUS Vogelzucht2015 App für Geräte mit Android Betriebssystemen Läuft nur in Zusammenhang mit einer Vollversion vogelzucht2015 auf einem PC. Zusammenfassung: a. Mit der APP

Mehr

schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG mitp/bhv

schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG mitp/bhv Roboter programmieren mit NXC für Lego Mindstorms NXT 1. Auflage Roboter programmieren mit NXC für Lego Mindstorms NXT schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG mitp/bhv Verlag

Mehr

Java Entwicklung für Embedded Devices Best & Worst Practices!

Java Entwicklung für Embedded Devices Best & Worst Practices! Java Entwicklung für Embedded Devices! George Mesesan Microdoc GmbH Natürlich können wir dieses neue log4j Bundle auch auf dem Device verwenden. Ist doch alles Java. Java Micro Edition (ME) Java Standard

Mehr

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang sysplus.ch outlook - mail-grundlagen Seite 1/8 Outlook Mail-Grundlagen Posteingang Es gibt verschiedene Möglichkeiten, um zum Posteingang zu gelangen. Man kann links im Outlook-Fenster auf die Schaltfläche

Mehr

PHPNuke Quick & Dirty

PHPNuke Quick & Dirty PHPNuke Quick & Dirty Dieses Tutorial richtet sich an all die, die zum erstenmal an PHPNuke System aufsetzen und wirklich keine Ahnung haben wie es geht. Hier wird sehr flott, ohne grosse Umschweife dargestellt

Mehr

Inhalt. 1 Einleitung AUTOMATISCHE DATENSICHERUNG AUF EINEN CLOUDSPEICHER

Inhalt. 1 Einleitung AUTOMATISCHE DATENSICHERUNG AUF EINEN CLOUDSPEICHER AUTOMATISCHE DATENSICHERUNG AUF EINEN CLOUDSPEICHER Inhalt 1 Einleitung... 1 2 Einrichtung der Aufgabe für die automatische Sicherung... 2 2.1 Die Aufgabenplanung... 2 2.2 Der erste Testlauf... 9 3 Problembehebung...

Mehr

Software Engineering Klassendiagramme Assoziationen

Software Engineering Klassendiagramme Assoziationen Software Engineering Klassendiagramme Assoziationen Prof. Adrian A. Müller, PMP, PSM 1, CSM Fachbereich Informatik und Mikrosystemtechnik 1 Lesen von Multiplizitäten (1) Multiplizitäten werden folgendermaßen

Mehr

Diplomarbeit. Konzeption und Implementierung einer automatisierten Testumgebung. Thomas Wehrspann. 10. Dezember 2008

Diplomarbeit. Konzeption und Implementierung einer automatisierten Testumgebung. Thomas Wehrspann. 10. Dezember 2008 Konzeption und Implementierung einer automatisierten Testumgebung, 10. Dezember 2008 1 Gliederung Einleitung Softwaretests Beispiel Konzeption Zusammenfassung 2 Einleitung Komplexität von Softwaresystemen

Mehr

Einführung in PHP. (mit Aufgaben)

Einführung in PHP. (mit Aufgaben) Einführung in PHP (mit Aufgaben) Dynamische Inhalte mit PHP? 2 Aus der Wikipedia (verkürzt): PHP wird auf etwa 244 Millionen Websites eingesetzt (Stand: Januar 2013) und wird auf etwa 80 % aller Websites

Mehr

Objektorientierte Programmierung für Anfänger am Beispiel PHP

Objektorientierte Programmierung für Anfänger am Beispiel PHP Objektorientierte Programmierung für Anfänger am Beispiel PHP Johannes Mittendorfer http://jmittendorfer.hostingsociety.com 19. August 2012 Abstract Dieses Dokument soll die Vorteile der objektorientierten

Mehr

Leitfaden zur ersten Nutzung der R FOM Portable-Version für Windows (Version 1.0)

Leitfaden zur ersten Nutzung der R FOM Portable-Version für Windows (Version 1.0) Leitfaden zur ersten Nutzung der R FOM Portable-Version für Windows (Version 1.0) Peter Koos 03. Dezember 2015 0 Inhaltsverzeichnis 1 Voraussetzung... 3 2 Hintergrundinformationen... 3 2.1 Installationsarten...

Mehr

ObjectBridge Java Edition

ObjectBridge Java Edition ObjectBridge Java Edition Als Bestandteil von SCORE Integration Suite stellt ObjectBridge Java Edition eine Verbindung von einem objektorientierten Java-Client zu einer fast beliebigen Server-Komponente

Mehr

TTS - TinyTimeSystem. Unterrichtsprojekt BIBI

TTS - TinyTimeSystem. Unterrichtsprojekt BIBI TTS - TinyTimeSystem Unterrichtsprojekt BIBI Mathias Metzler, Philipp Winder, Viktor Sohm 28.01.2008 TinyTimeSystem Inhaltsverzeichnis Problemstellung... 2 Lösungsvorschlag... 2 Punkte die unser Tool erfüllen

Mehr

icloud nicht neu, aber doch irgendwie anders

icloud nicht neu, aber doch irgendwie anders Kapitel 6 In diesem Kapitel zeigen wir Ihnen, welche Dienste die icloud beim Abgleich von Dateien und Informationen anbietet. Sie lernen icloud Drive kennen, den Fotostream, den icloud-schlüsselbund und

Mehr

Prozessbewertung und -verbesserung nach ITIL im Kontext des betrieblichen Informationsmanagements. von Stephanie Wilke am 14.08.08

Prozessbewertung und -verbesserung nach ITIL im Kontext des betrieblichen Informationsmanagements. von Stephanie Wilke am 14.08.08 Prozessbewertung und -verbesserung nach ITIL im Kontext des betrieblichen Informationsmanagements von Stephanie Wilke am 14.08.08 Überblick Einleitung Was ist ITIL? Gegenüberstellung der Prozesse Neuer

Mehr

Projektmanagement in der Spieleentwicklung

Projektmanagement in der Spieleentwicklung Projektmanagement in der Spieleentwicklung Inhalt 1. Warum brauche ich ein Projekt-Management? 2. Die Charaktere des Projektmanagement - Mastermind - Producer - Projektleiter 3. Schnittstellen definieren

Mehr

CADEMIA: Einrichtung Ihres Computers unter Linux mit Oracle-Java

CADEMIA: Einrichtung Ihres Computers unter Linux mit Oracle-Java CADEMIA: Einrichtung Ihres Computers unter Linux mit Oracle-Java Stand: 21.02.2015 Java-Plattform: Auf Ihrem Computer muss die Java-Plattform, Standard-Edition der Version 7 (Java SE 7) oder höher installiert

Mehr

C++ mit Eclipse & GCC unter Windows

C++ mit Eclipse & GCC unter Windows C++ mit Eclipse & GCC Seite 1 / 14 C++ mit Eclipse & GCC unter Windows Hinweise Stand 18. Okt. 2014 => GCC 4.9.1 Boost 1.56.0 Eclipse Luna V. 4.4.1 Java Version 8, Update 25 (entspricht 1.8.0_25) Achtung

Mehr

Speicher in der Cloud

Speicher in der Cloud Speicher in der Cloud Kostenbremse, Sicherheitsrisiko oder Basis für die unternehmensweite Kollaboration? von Cornelius Höchel-Winter 2013 ComConsult Research GmbH, Aachen 3 SYNCHRONISATION TEUFELSZEUG

Mehr

Albert HAYR Linux, IT and Open Source Expert and Solution Architect. Open Source professionell einsetzen

Albert HAYR Linux, IT and Open Source Expert and Solution Architect. Open Source professionell einsetzen Open Source professionell einsetzen 1 Mein Background Ich bin überzeugt von Open Source. Ich verwende fast nur Open Source privat und beruflich. Ich arbeite seit mehr als 10 Jahren mit Linux und Open Source.

Mehr

Programmieren I. Kapitel 15. Ein und Ausgabe

Programmieren I. Kapitel 15. Ein und Ausgabe Programmieren I Kapitel 15. Ein und Ausgabe Kapitel 15: Ein und Ausgabe Ziel: einen kleinen Einblick in die vielfältigen IO Klassen geben Grober Überblick Klasse File zur Verwaltung von Dateien Random

Mehr

Qt-Projekte mit Visual Studio 2005

Qt-Projekte mit Visual Studio 2005 Qt-Projekte mit Visual Studio 2005 Benötigte Programme: Visual Studio 2005 Vollversion, Microsoft Qt 4 Open Source s. Qt 4-Installationsanleitung Tabelle 1: Benötigte Programme für die Qt-Programmierung

Mehr

Lizenzierung von System Center 2012

Lizenzierung von System Center 2012 Lizenzierung von System Center 2012 Mit den Microsoft System Center-Produkten lassen sich Endgeräte wie Server, Clients und mobile Geräte mit unterschiedlichen Betriebssystemen verwalten. Verwalten im

Mehr

1 topologisches Sortieren

1 topologisches Sortieren Wolfgang Hönig / Andreas Ecke WS 09/0 topologisches Sortieren. Überblick. Solange noch Knoten vorhanden: a) Suche Knoten v, zu dem keine Kante führt (Falls nicht vorhanden keine topologische Sortierung

Mehr

Handbuch ECDL 2003 Basic Modul 5: Datenbank Grundlagen von relationalen Datenbanken

Handbuch ECDL 2003 Basic Modul 5: Datenbank Grundlagen von relationalen Datenbanken Handbuch ECDL 2003 Basic Modul 5: Datenbank Grundlagen von relationalen Datenbanken Dateiname: ecdl5_01_00_documentation_standard.doc Speicherdatum: 14.02.2005 ECDL 2003 Basic Modul 5 Datenbank - Grundlagen

Mehr

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen Binäre Bäume 1. Allgemeines Binäre Bäume werden grundsätzlich verwendet, um Zahlen der Größe nach, oder Wörter dem Alphabet nach zu sortieren. Dem einfacheren Verständnis zu Liebe werde ich mich hier besonders

Mehr

Gruppenrichtlinien und Softwareverteilung

Gruppenrichtlinien und Softwareverteilung Gruppenrichtlinien und Softwareverteilung Ergänzungen zur Musterlösung Bitte lesen Sie zuerst die gesamte Anleitung durch! Vorbemerkung: Die Begriffe OU (Organizational Unit) und Raum werden in der folgenden

Mehr

CADEMIA: Einrichtung Ihres Computers unter Windows

CADEMIA: Einrichtung Ihres Computers unter Windows CADEMIA: Einrichtung Ihres Computers unter Windows Stand: 21.02.2015 Java-Plattform: Auf Ihrem Computer muss die Java-Plattform, Standard-Edition der Version 7 (Java SE 7) oder höher installiert sein.

Mehr

Javadoc. Programmiermethodik. Eva Zangerle Universität Innsbruck

Javadoc. Programmiermethodik. Eva Zangerle Universität Innsbruck Javadoc Programmiermethodik Eva Zangerle Universität Innsbruck Überblick Einführung Java Ein erster Überblick Objektorientierung Vererbung und Polymorphismus Ausnahmebehandlung Pakete und Javadoc Spezielle

Mehr

Reporting Services und SharePoint 2010 Teil 1

Reporting Services und SharePoint 2010 Teil 1 Reporting Services und SharePoint 2010 Teil 1 Abstract Bei der Verwendung der Reporting Services in Zusammenhang mit SharePoint 2010 stellt sich immer wieder die Frage bei der Installation: Wo und Wie?

Mehr

BüroWARE Exchange Synchronisation Grundlagen und Voraussetzungen

BüroWARE Exchange Synchronisation Grundlagen und Voraussetzungen BüroWARE Exchange Synchronisation Grundlagen und Voraussetzungen Stand: 13.12.2010 Die BüroWARE SoftENGINE ist ab Version 5.42.000-060 in der Lage mit einem Microsoft Exchange Server ab Version 2007 SP1

Mehr

Upgrade auf die Standalone Editionen von Acronis Backup & Recovery 10. Technische Informationen (White Paper)

Upgrade auf die Standalone Editionen von Acronis Backup & Recovery 10. Technische Informationen (White Paper) Upgrade auf die Standalone Editionen von Acronis Backup & Recovery 10 Technische Informationen (White Paper) Inhaltsverzeichnis 1. Über dieses Dokument... 3 2. Überblick... 3 3. Upgrade Verfahren... 4

Mehr

M. Graefenhan 2000-12-07. Übungen zu C. Blatt 3. Musterlösung

M. Graefenhan 2000-12-07. Übungen zu C. Blatt 3. Musterlösung M. Graefenhan 2000-12-07 Aufgabe Lösungsweg Übungen zu C Blatt 3 Musterlösung Schreiben Sie ein Programm, das die Häufigkeit von Zeichen in einem eingelesenen String feststellt. Benutzen Sie dazu ein zweidimensionales

Mehr

Workshop: Eigenes Image ohne VMware-Programme erstellen

Workshop: Eigenes Image ohne VMware-Programme erstellen Workshop: Eigenes Image ohne VMware-Programme erstellen Normalerweise sind zum Erstellen neuer, kompatibler Images VMware-Programme wie die Workstation, der ESX-Server oder VMware ACE notwendig. Die Community

Mehr

White Paper. Konfiguration und Verwendung des Auditlogs. 2012 Winter Release

White Paper. Konfiguration und Verwendung des Auditlogs. 2012 Winter Release White Paper Konfiguration und Verwendung des Auditlogs 2012 Winter Release Copyright Fabasoft R&D GmbH, A-4020 Linz, 2011. Alle Rechte vorbehalten. Alle verwendeten Hard- und Softwarenamen sind Handelsnamen

Mehr

Primzahlen und RSA-Verschlüsselung

Primzahlen und RSA-Verschlüsselung Primzahlen und RSA-Verschlüsselung Michael Fütterer und Jonathan Zachhuber 1 Einiges zu Primzahlen Ein paar Definitionen: Wir bezeichnen mit Z die Menge der positiven und negativen ganzen Zahlen, also

Mehr

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3 Handbuch Fischertechnik-Einzelteiltabelle V3.7.3 von Markus Mack Stand: Samstag, 17. April 2004 Inhaltsverzeichnis 1. Systemvorraussetzungen...3 2. Installation und Start...3 3. Anpassen der Tabelle...3

Mehr

Datensicherung. Beschreibung der Datensicherung

Datensicherung. Beschreibung der Datensicherung Datensicherung Mit dem Datensicherungsprogramm können Sie Ihre persönlichen Daten problemlos Sichern. Es ist möglich eine komplette Datensicherung durchzuführen, aber auch nur die neuen und geänderten

Mehr

CADEMIA: Einrichtung Ihres Computers unter Mac OS X

CADEMIA: Einrichtung Ihres Computers unter Mac OS X CADEMIA: Einrichtung Ihres Computers unter Mac OS X Stand: 28.01.2016 Java-Plattform: Auf Ihrem Computer muss die Java-Plattform, Standard-Edition der Version 7 (Java SE 7) oder höher installiert sein.

Mehr

Er musste so eingerichtet werden, dass das D-Laufwerk auf das E-Laufwerk gespiegelt

Er musste so eingerichtet werden, dass das D-Laufwerk auf das E-Laufwerk gespiegelt Inhaltsverzeichnis Aufgabe... 1 Allgemein... 1 Active Directory... 1 Konfiguration... 2 Benutzer erstellen... 3 Eigenes Verzeichnis erstellen... 3 Benutzerkonto erstellen... 3 Profil einrichten... 5 Berechtigungen

Mehr

Software-Engineering und Optimierungsanwendungen in der Thermodynamik

Software-Engineering und Optimierungsanwendungen in der Thermodynamik Software-Engineering und Optimierungsanwendungen in der Thermodynamik Software-Engineering 5 Programmentwicklung und Debuggen mit IDE und CASE-Tools Übungen Prof. Dr. Rolf Dornberger OPTSWE_SWE: 5 Programmentwicklung

Mehr

IBM Software Demos Tivoli Provisioning Manager for OS Deployment

IBM Software Demos Tivoli Provisioning Manager for OS Deployment Für viele Unternehmen steht ein Wechsel zu Microsoft Windows Vista an. Doch auch für gut vorbereitete Unternehmen ist der Übergang zu einem neuen Betriebssystem stets ein Wagnis. ist eine benutzerfreundliche,

Mehr

.NET Code schützen. Projekt.NET. Version 1.0

.NET Code schützen. Projekt.NET. Version 1.0 .NET Code schützen Projekt.NET Informationsmaterial zum Schützen des.net Codes Version 1.0 Autor: Status: Ablage: Empfänger: Seiten: D. Hoyer 1 / 6 Verteiler : Dokument1 Seite 1 von 1 Änderungsprotokoll

Mehr

Installation SQL- Server 2012 Single Node

Installation SQL- Server 2012 Single Node Installation SQL- Server 2012 Single Node Dies ist eine Installationsanleitung für den neuen SQL Server 2012. Es beschreibt eine Single Node Installation auf einem virtuellen Windows Server 2008 R2 mit

Mehr

Einführung zu den Übungen aus Softwareentwicklung 1

Einführung zu den Übungen aus Softwareentwicklung 1 Einführung zu den Übungen aus Softwareentwicklung 1 Dipl.-Ing. Andreas Riener Universität Linz, Institut für Pervasive Computing Altenberger Straße 69, A-4040 Linz riener@pervasive.jku.at SWE 1 // Organisatorisches

Mehr

Vorlesung Objektorientierte Softwareentwicklung. Kapitel 0. Java-Überblick

Vorlesung Objektorientierte Softwareentwicklung. Kapitel 0. Java-Überblick Vorlesung Objektorientierte Softwareentwicklung Sommersemester este 2008 Kapitel 0. Java-Überblick Was sind die Ziele? Warum Java? Komplexe Anwendungen e-business verteilt zuverlässig sicher mobil persistent

Mehr

Applikations-Performance in Citrix Umgebungen

Applikations-Performance in Citrix Umgebungen Applikations-Performance in Citrix Umgebungen Monitoring und Troubleshooting mit OPNET Lösungen Page 1 of 6 CITRIX ist langsam! Mit dieser Frage sehen sich immer wieder IT Administratoren konfrontiert.

Mehr

Erstellung von Reports mit Anwender-Dokumentation und System-Dokumentation in der ArtemiS SUITE (ab Version 5.0)

Erstellung von Reports mit Anwender-Dokumentation und System-Dokumentation in der ArtemiS SUITE (ab Version 5.0) Erstellung von und System-Dokumentation in der ArtemiS SUITE (ab Version 5.0) In der ArtemiS SUITE steht eine neue, sehr flexible Reporting-Funktion zur Verfügung, die mit der Version 5.0 noch einmal verbessert

Mehr

Lokale Installation von DotNetNuke 4 ohne IIS

Lokale Installation von DotNetNuke 4 ohne IIS Lokale Installation von DotNetNuke 4 ohne IIS ITM GmbH Wankelstr. 14 70563 Stuttgart http://www.itm-consulting.de Benjamin Hermann hermann@itm-consulting.de 12.12.2006 Agenda Benötigte Komponenten Installation

Mehr

infach Geld FBV Ihr Weg zum finanzellen Erfolg Florian Mock

infach Geld FBV Ihr Weg zum finanzellen Erfolg Florian Mock infach Ihr Weg zum finanzellen Erfolg Geld Florian Mock FBV Die Grundlagen für finanziellen Erfolg Denn Sie müssten anschließend wieder vom Gehaltskonto Rückzahlungen in Höhe der Entnahmen vornehmen, um

Mehr

SEP 114. Design by Contract

SEP 114. Design by Contract Design by Contract SEP 114 Design by Contract Teile das zu entwickelnde Programm in kleine Einheiten (Klassen, Methoden), die unabhängig voneinander entwickelt und überprüft werden können. Einheiten mit

Mehr

SEPA Lastschriften. Ergänzung zur Dokumentation vom 27.01.2014. Workshop Software GmbH Siemensstr. 21 47533 Kleve 02821 / 731 20 02821 / 731 299

SEPA Lastschriften. Ergänzung zur Dokumentation vom 27.01.2014. Workshop Software GmbH Siemensstr. 21 47533 Kleve 02821 / 731 20 02821 / 731 299 SEPA Lastschriften Ergänzung zur Dokumentation vom 27.01.2014 Workshop Software GmbH Siemensstr. 21 47533 Kleve 02821 / 731 20 02821 / 731 299 www.workshop-software.de Verfasser: SK info@workshop-software.de

Mehr

Installation einer C++ Entwicklungsumgebung unter Windows --- TDM-GCC und Eclipse installieren

Installation einer C++ Entwicklungsumgebung unter Windows --- TDM-GCC und Eclipse installieren Installation einer C++ Entwicklungsumgebung unter Windows --- TDM-GCC und Eclipse installieren 1 32- oder 64-bit Windows feststellen In den nachfolgenden Schritten ist es nötig, dass Sie wissen, ob Sie

Mehr

40-Tage-Wunder- Kurs. Umarme, was Du nicht ändern kannst.

40-Tage-Wunder- Kurs. Umarme, was Du nicht ändern kannst. 40-Tage-Wunder- Kurs Umarme, was Du nicht ändern kannst. Das sagt Wikipedia: Als Wunder (griechisch thauma) gilt umgangssprachlich ein Ereignis, dessen Zustandekommen man sich nicht erklären kann, so dass

Mehr

Kurzanleitung zu. von Daniel Jettka 18.11.2008

Kurzanleitung zu. von Daniel Jettka 18.11.2008 Kurzanleitung zu Tigris.org Open Source Software Engineering Tools von Daniel Jettka 18.11.2008 Inhaltsverzeichnis 1.Einführung...1 2.Das Projektarchivs...3 2.1.Anlegen des Projektarchivs...3 2.2.Organisation

Mehr

Objektorientierte Programmierung. Kapitel 12: Interfaces

Objektorientierte Programmierung. Kapitel 12: Interfaces 12. Interfaces 1/14 Objektorientierte Programmierung Kapitel 12: Interfaces Stefan Brass Martin-Luther-Universität Halle-Wittenberg Wintersemester 2012/13 http://www.informatik.uni-halle.de/ brass/oop12/

Mehr

AGROPLUS Buchhaltung. Daten-Server und Sicherheitskopie. Version vom 21.10.2013b

AGROPLUS Buchhaltung. Daten-Server und Sicherheitskopie. Version vom 21.10.2013b AGROPLUS Buchhaltung Daten-Server und Sicherheitskopie Version vom 21.10.2013b 3a) Der Daten-Server Modus und der Tresor Der Daten-Server ist eine Betriebsart welche dem Nutzer eine grosse Flexibilität

Mehr

Programmieren. 10. Tutorium 4./ 5. Übungsblatt Referenzen

Programmieren. 10. Tutorium 4./ 5. Übungsblatt Referenzen Programmieren 10. Tutorium 4./ 5. Übungsblatt Inhalt I. Übungsblatt 4 II. III. - Rückgabe und Besprechung - Vorbereitung auf Wiederholung/ Nachtrag - Operatorpräzedenzen IV. Übungsblatt 5 - Vorstellung

Mehr

Patch Management mit

Patch Management mit Patch Management mit Installation von Hotfixes & Patches Inhaltsverzeichnis dieses Dokuments Einleitung...3 Wie man einen Patch installiert...4 Patch Installation unter UliCMS 7.x.x bis 8.x.x...4 Patch

Mehr

ICS-Addin. Benutzerhandbuch. Version: 1.0

ICS-Addin. Benutzerhandbuch. Version: 1.0 ICS-Addin Benutzerhandbuch Version: 1.0 SecureGUARD GmbH, 2011 Inhalt: 1. Was ist ICS?... 3 2. ICS-Addin im Dashboard... 3 3. ICS einrichten... 4 4. ICS deaktivieren... 5 5. Adapter-Details am Server speichern...

Mehr

Erfolgreiche Webseiten: Zur Notwendigkeit die eigene(n) Zielgruppe(n) zu kennen und zu verstehen!

Erfolgreiche Webseiten: Zur Notwendigkeit die eigene(n) Zielgruppe(n) zu kennen und zu verstehen! Erfolgreiche Webseiten: Zur Notwendigkeit die eigene(n) Zielgruppe(n) zu kennen und zu verstehen! www.wee24.de. info@wee24.de. 08382 / 6040561 1 Experten sprechen Ihre Sprache. 2 Unternehmenswebseiten

Mehr

0. Einführung. C und C++ (CPP)

0. Einführung. C und C++ (CPP) C und C++ (CPP) 0. Einführung Prof. Dr. Marc Rennhard Institut für angewandte Informationstechnologie InIT ZHAW Zürcher Hochschule für Angewandte Wissenschaften marc.rennhard@zhaw.ch Marc Rennhard, 05.01.2010,

Mehr

Vermeiden Sie es sich bei einer deutlich erfahreneren Person "dranzuhängen", Sie sind persönlich verantwortlich für Ihren Lernerfolg.

Vermeiden Sie es sich bei einer deutlich erfahreneren Person dranzuhängen, Sie sind persönlich verantwortlich für Ihren Lernerfolg. 1 2 3 4 Vermeiden Sie es sich bei einer deutlich erfahreneren Person "dranzuhängen", Sie sind persönlich verantwortlich für Ihren Lernerfolg. Gerade beim Einstig in der Programmierung muss kontinuierlich

Mehr

Agile Vorgehensmodelle in der Softwareentwicklung: Scrum

Agile Vorgehensmodelle in der Softwareentwicklung: Scrum C A R L V O N O S S I E T Z K Y Agile Vorgehensmodelle in der Softwareentwicklung: Scrum Johannes Diemke Vortrag im Rahmen der Projektgruppe Oldenburger Robot Soccer Team im Wintersemester 2009/2010 Was

Mehr

Suche schlecht beschriftete Bilder mit Eigenen Abfragen

Suche schlecht beschriftete Bilder mit Eigenen Abfragen Suche schlecht beschriftete Bilder mit Eigenen Abfragen Ist die Bilderdatenbank über einen längeren Zeitraum in Benutzung, so steigt die Wahrscheinlichkeit für schlecht beschriftete Bilder 1. Insbesondere

Mehr

MORE Profile. Pass- und Lizenzverwaltungssystem. Stand: 19.02.2014 MORE Projects GmbH

MORE Profile. Pass- und Lizenzverwaltungssystem. Stand: 19.02.2014 MORE Projects GmbH MORE Profile Pass- und Lizenzverwaltungssystem erstellt von: Thorsten Schumann erreichbar unter: thorsten.schumann@more-projects.de Stand: MORE Projects GmbH Einführung Die in More Profile integrierte

Mehr

GITS Steckbriefe 1.9 - Tutorial

GITS Steckbriefe 1.9 - Tutorial Allgemeines Die Steckbriefkomponente basiert auf der CONTACTS XTD Komponente von Kurt Banfi, welche erheblich modifiziert bzw. angepasst wurde. Zuerst war nur eine kleine Änderung der Komponente für ein

Mehr

Windows Server 2012 RC2 konfigurieren

Windows Server 2012 RC2 konfigurieren Windows Server 2012 RC2 konfigurieren Kurzanleitung um einen Windows Server 2012 als Primären Domänencontroller einzurichten. Vorbereitung und Voraussetzungen In NT 4 Zeiten, konnte man bei der Installation

Mehr

Sie werden sehen, dass Sie für uns nur noch den direkten PDF-Export benötigen. Warum?

Sie werden sehen, dass Sie für uns nur noch den direkten PDF-Export benötigen. Warum? Leitfaden zur Druckdatenerstellung Inhalt: 1. Download und Installation der ECI-Profile 2. Farbeinstellungen der Adobe Creative Suite Bitte beachten! In diesem kleinen Leitfaden möchten wir auf die Druckdatenerstellung

Mehr

Visual Basic Express Debugging

Visual Basic Express Debugging Inhalt Dokument Beschreibung... 1 Projekt vorbereiten... 1 Verknüpfung zu Autocad/ProStructures einstellen... 2 Debugging... 4 Autocad/ProSteel Beispiel... 5 Dokument Beschreibung Debuggen nennt man das

Mehr

Beschreibung und Bedienungsanleitung. Inhaltsverzeichnis: Abbildungsverzeichnis: Werkzeug für verschlüsselte bpks. Dipl.-Ing.

Beschreibung und Bedienungsanleitung. Inhaltsverzeichnis: Abbildungsverzeichnis: Werkzeug für verschlüsselte bpks. Dipl.-Ing. www.egiz.gv.at E-Mail: post@egiz.gv.at Telefon: ++43 (316) 873 5514 Fax: ++43 (316) 873 5520 Inffeldgasse 16a / 8010 Graz / Austria Beschreibung und Bedienungsanleitung Werkzeug für verschlüsselte bpks

Mehr