-
GEBIET DER BESCHREIBUNG
-
Die
vorliegende Beschreibung betrifft allgemein verwaltete Laufzeitumgebungen
und insbesondere eine Vorrichtung und ein Verfahren zum Wiederherstellen
einer Synchronisation bei objektorientierten Software-Anwendungen
in verwalteten Laufzeitumgebungen.
-
HINTERGRUND
-
Das
Erfordernis der erhöhten Übertragbarkeit
von Software-Anwendungen (d.h. der Möglichkeit, eine gegebene Software-Anwendung
auf verschiedenen Plattformen mit unterschiedlicher Hardware, unterschiedlichen
Betriebssystemen usw. auszuführen)
sowie das Erfordernis einer Verkürzung der
Vermarktungszeit für
unabhängige
Softwarehändler
(ISVs) haben zu einer verstärkten
Entwicklung und Verwendung von verwalteten Laufzeitumgebungen geführt.
-
Verwaltete
Laufzeitumgebungen sind typisch für dynamische Programmiersprachen
wie zum Beispiel Java, C# usw. Eine Software-Engine (z.B. eine Java
Virtual Machine (JVM), Common Language Runtime (CLR) etc.), die üblicherweise
als Laufzeitumgebung bezeichnet wird, führt die Anweisungen der dynamischen
Programmiersprache aus. Die Laufzeitumgebung legt sich zwischen
die auszuführenden
Anweisungen der dynamischen Programmiersprache (z.B. ein Java-Programm
oder ein Quellcode) und die Zielplattform der Ausführung (d.h.
die Hardware und das/die Betriebssystem(e) des Computers, der das
dynamische Programm ausführt),
so daß das
dynamische Programm auf eine plattformunabhängige Art ausgeführt werden
kann.
-
Die
Anweisungen von dynamischen Programmiersprachen (z.B. Java-Anweisungen)
werden nicht statisch kompiliert und für die Ausführung durch die Zielplattform
(d.h. das Betriebssystem und die Hardware des Ziel-Verarbeitungssystems
oder der Zielplattform) direkt mit einem nativen oder Maschinencode
verbunden. Statt dessen werden die Anweisungen von dynamischen Programmiersprachen
statisch in eine Zwischensprache (z.B. in einen Bytecode) umgesetzt,
und die Zwischensprache wird danach von einem Just-in-Time-(JIT)-Compiler
in den nativen oder Maschinencode übertragen, der von dem Ziel-Verarbeitungssystem
oder der Zielplattform ausgeführt
werden kann. In der Regel ist der JIT-Compiler mit einer Laufzeitumgebung
versehen, die von dem Betriebssystem der Ziel-Verarbeitungsplattform,
zum Beispiel einem Computersystem, bereitgestellt wird. Die Laufzeitumgebung
und insbesondere der JIT-Compiler überträgt daher plattformunabhängige Programmanweisungen
(z.B. Java-Bytecodes, C#-Bytycodes etc.) in den nativen Code (d.h. den
Maschinencode, der von dem darunterliegenden Ziel-Verarbeitungssystem
oder der Zielplattform ausgeführt
werden kann).
-
Um
die Gesamtproduktivität
zu erhöhen, stellen
viele dynamische Programmiersprachen und die diese unterstützenden
verwalteten Laufzeitumgebungen eine Infrastruktur bereit, die es
ermöglicht, gleichzeitig
ablaufende Programmtechniken wie zum Beispiel ein Multithreading
anzuwenden. Insbesondere enthalten viele dynamische Programmiersprachen
Synchronisationsmerkmale oder Synchronisationsoperationen, die die
gleichzeitige Ausführung
von Ausführungssträngen ermöglichen
und dabei die gemeinsame Verwendung oder den Zugriff auf ein gegebenes
Objekt und dessen Variablen erlauben, ohne daß dadurch eine Konflikt- oder
Konkurrenzsituation entsteht. Zum Beispiel wird in "Compositional Pointer
and Escape Analysis for Java Programs" von John Whaley und Martin Rinard (Oktober
1999) das Sperren von Objekten im Falle von möglichen gleichzeitigen Objektzugriffen
diskutiert. Um die unnötige Verwendung
von Ressourcen zur Vermeidung von Sperren einzuschränken, werden
Analyseninformationen herangezogen, um zu bestimmen, wann Objekte
Verarbeitungssträngen
und Verfahren nicht entweichen können.
Auf diese Weise kann der Softwaredesigner sicherstellen, daß ein global
zugängliches Objekt "strangsicher" ist (d.h. in einer
Multithread-Laufzeitumgebung ohne Konflikte und Konkurrenzsituationen
verwendet werden kann).
-
Unglücklicherweise
verlangsamt die mit der Objektsynchronisation verbundene Verarbeitung
die Ausführungszeit.
Zum Beispiel kann bei einigen gut bekannten Java-Anwendungen und Benchmarks der Aufwand
für die
Synchronisation zwischen zehn und zwanzig Prozent der Gesamt-Ausführungszeit
ausmachen. Außerdem
erfolgt die Synchronisation gewöhnlich
als Sicherheitsmaßnahme
gegen Konkurrenzsituationen während
der Laufzeit (insbesondere bei Objektbibliotheken) ohne Rücksicht
darauf, ob die Synchronisation während
der Laufzeit tatsächlich erforderlich
ist oder nicht.
-
Es
können
bekannte Escape-Analysetechniken angewendet werden, um die Gesamt-Ausführungsgeschwindigkeit
von Code einschließlich
unnötiger
Synchronisationen zu erhöhen.
Im allgemeinen führen
bekannte Escape-Analysetechniken eine Gesamtprogrammanalyse durch,
die das Entfernen von Synchronisationsoperationen bei nicht globalen
Objekten und bei globalen Objekten ermöglicht, für die während der laufenden Ausführung des
Programms keine Konkurrenzsituationen entstehen. Die bekannten Escape-Analysetechniken
beruhen auf einem statisch verbundenen Codemodell, bei dem angenommen
wird, daß während der
Laufzeit keine neuen Objektklassen geladen werden. Einige populäre Programmiersprachen
wie zum Beispiel Java und CLI enthalten jedoch ein Merkmal zum dynamischen
Laden von Klassen, das eine dynamische Verbindung von Verfahren
oder Funktionen ermöglicht,
die innerhalb des Kontexts einer Laufzeit aufgerufen werden können. In
einigen Fällen
kann das dynamische Laden einer Klasse in einer Laufzeitumgebung,
die ein vorher optimiertes Programm ausführt (z.B. ein Programm, aus
dem die Synchronisation zur Gänze
oder teilweise entfernt wurde), dazu führen, daß sich das Programm unsicher
verhält
(z.B. aufgrund Datenkonkurrenz) oder abstürzt. Die bekannten, auf einem
statisch verbundenen Codemodell beruhenden Escape-Analysetechniken
können
zwar dazu verwendet werden, die Synchronisation aus einem statisch
verbundenen Code zu entfernen, diese Techniken unterstützen jedoch
nicht die Wiederherstellung der Synchronisation eines vorher desynchronisierten
(d.h. optimierten) Codes, bei dem später durch die Auswirkungen
einer dynamisch geladenen Klasse wieder eine Syn chronisation erforderlich
wird. Außerdem können andere
offene Merkmale wie zum Beispiel eine Reflexion und native Verfahren
eine Escape-Analyse ungültig
machen und dadurch zu unsicheren Ausführungsbedingungen führen.
-
Die
Aspekte der vorliegenden Erfindung sind in den anhängenden
unabhängigen
Patentansprüchen
genannt.
-
KURZBESCHREIBUNG DER ZEICHNUNGEN
-
1 ist
eine Blockdarstellung einer beispielhaften Architektur, die zur
Ausführung
der beschriebenen Vorrichtung und des beschriebenen Verfahrens zur
Wiederherstellung einer Synchronisation verwendet werden kann.
-
2 zeigt
beispielhaft einen auf Java basierenden Code für eine Klasse, für die nach
einer Escape-Analyse die Synchronisation entfernt wurde.
-
3 zeigt
beispielhaft einen auf Java basierenden Code für eine andere Klasse, die,
wenn sie dynamisch geladen wird, das Entfernen der Synchronisation
für die
Aufrufstelle A hinfällig
werden läßt.
-
4 ist
ein Flußdiagramm
für ein
Beispiel, wie der Just-in-Time-Compiler der 1 konfiguriert werden
kann, um die Synchronisation von Aufrufstellen wiederherzustellen,
die von einer dynamisch geladenen Klasse betroffen sind.
-
5 ist
ein detaillierteres Flußdiagramm
für ein
Beispiel, wie der Just-in-Time-Compiler
der 1 konfiguriert werden kann, um die Synchronisation von
betroffenen Aufrufstellen wiederherzustellen.
-
6 ist
ein Beispiel für
einen Pseudo-Code, der eine Art aufzeigt, wie der Just-in-Time-Compiler
der 1 konfiguriert werden kann, um für jeden
mit einer betroffenen Aufrufstelle verbundenen Verarbeitungsstrang
eine Sperren/Freigabekompensation durchzuführen.
-
7 zeigt
beispielhaft einen auf Java basierenden Code, für den trotz des dynamischen
Ladens einer mit der Aufrufstelle A verbundenen Klasse das Entfernen
der Synchronisation gültig
bleibt.
-
8 und 9 zeigen
schematisch eine beispielhafte Art, wie der Just-in-Time-Compiler der 1 konfiguriert
werden kann, um eine Freigabe-Kompensationsoperation durchzuführen.
-
10 ist
ein Beispiel für
einen Code, der eine Art aufzeigt, wie das Cookie COMP_UNLOCK_TAG
dazu verwendet werden kann, eine Rückkehradresse in einem Stapelabwickelkontext
wiederherzustellen.
-
11 zeigt
ein Beispiel für
ein Prozessorsystem, das dazu verwendet werden kann, die beschriebene
Vorrichtung und das beschriebene Verfahren zu verwirklichen
-
GENAUE BESCHREIBUNG
-
Die 1 ist
eine Blockdarstellung einer beispielhaften Architektur 100,
die dazu verwendet werden kann, die beschriebene Vorrichtung und
das beschriebene Verfahren zur Wiederherstellung einer Synchronisation
zu verwirklichen. In der beispielhaften Architek tur 100 werden
eine oder mehrere Software-Anwendungen 102, die aus einer
oder aus mehreren dynamischen Programmiersprachen und/oder Anweisungen
bestehen, zu einem Sprachencompiler 104 geführt. Die
Anwendungen 102 können
in einer plattformunabhängigen
Sprache wie zum Beispiel Java oder C# geschrieben sein. Es kann
auch jede andere dynamische oder plattformunabhängige Computersprache oder
Anweisung verwendet werden. Außerdem
können
einige oder alle der Anwendungen 102 in dem System gespeichert sein,
auf dem die Anwendungen ausgeführt
werden. Zusätzlich
oder alternativ können
einige oder alle der Anwendungen auf einem System gespeichert sein, das
von dem System getrennt (und möglicherweise weit
davon entfernt) ist, auf dem die Anwendungen 102 ausgeführt werden.
-
Der
Sprachencompiler 104 kompiliert eine oder mehrere der Anwendungen 102 statisch,
um einen kompilierten Code 106 zu erzeugen. Der kompilierte
Code 106 ist ein Zwischensprachecode oder eine Zwischenspracheanweisung
(z.B. ein Bytecode, wenn die kompilierte(n) Anwendung(en) in Java
geschrieben ist/sind), der oder die im Binärformat in einem Speicher (nicht
gezeigt) gespeichert wird. Wie bei den Anwendungen 102 kann
der kompilierte Code 106 lokal im Zielsystem 108 gespeichert
werden, auf dem der kompilierte Code 106 auszuführen ist.
Das Zielsystem 108 kann ein Computersystem und dergleichen
sein, etwa das, das genauer weiter unten in Verbindung mit der 11 beschrieben
wird. Das Zielsystem 108 kann mit einem oder mit mehreren
Endnutzern und dergleichen in Verbindung stehen. Zusätzlich oder
alternativ kann der kompilierte Code 106 über eine
Kommunikationsverbindung oder über
Kommunikationsverbindungen, zum Beispiel ein lokales Netzwerk, das
Internet, ein zellulares oder ein anderes drahtloses Kommunikationssystem etc.
zu dem Zielsystem 108 geliefert werden.
-
Ein
oder mehrere Abschnitte des kompilierten Codes 106 (z.B.
eine oder mehrere Softwareanwendungen) können auf dem Zielsystem 108 ausgeführt werden.
Das heißt,
daß ein
Betriebssystem 110 wie zum Beispiel Windows, Linux etc.
eine Laufzeitumgebung 112 bereitstellt, die einen oder
mehrere Abschnitte des kompilierten Codes 106 ausführt. Wenn
der kompilierte Code 106 zum Beispiel Java-Bytecode enthält, beruht
die Laufzeitumgebung 112 auf einer Java Virtual Machine
(JVM) und dergleichen, die Java-Bytecode ausführt. Die Laufzeitumgebung 112 lädt einen
Abschnitt oder mehrere Abschnitte des kompilierten Codes 106 (d.h.
die Zwischenspracheanweisungen oder den Zwischensprachecode) in
einen Speicher (nicht gezeigt), auf den die Laufzeitumgebung 112 zugreifen
kann. Vorzugsweise lädt
die Laufzeitumgebung 110 eine ganze Anwendung (oder möglicherweise
mehrere Anwendungen) in den Speicher und überprüft den kompilierten oder Zwischensprachecode 106 auf
Typsicherheit.
-
Nachdem
die Laufzeitumgebung 112 eine Anwendung oder mehrere Anwendungen)
in den Speicher geladen hat, können
die Zwischenspracheanweisungen, die mit Verfahren oder Objekten
verbunden sind, die von der ausgeführten Anwendung aufgerufen
oder anderweitig zur Ausführung
der Anwendung benötigt
werden, von einem Just-in-Time-(JIT)-Compiler 114 verarbeitet
werden. Der JIT-Compiler 114 kompiliert die Zwi schenspracheanweisungen,
um nativen oder Maschinencode zu erzeugen, der von einem oder von
mehreren Prozessoren (wie zum Beispiel dem Prozessor 1122 der 11)
im Computersystem 108 ausgeführt wird.
-
Der
JIT-Compiler 114 kann den nativen Code (d.h. den Maschinencode,
der mit dem Computersystem 108 kompatibel ist und der daher
davon ausgeführt
werden kann) in einem JIT-Speichercache (JIT-IMC) 116 speichern.
Auf diese Weise kann die Laufzeitumgebung 112 nativen Code,
der mit einem vorher kompilierten Verfahren verbunden ist, das mehr
als einmal aufgerufen wird, wieder verwenden. Mit anderen Worten
können
Zwischenspracheanweisungen, die in nativen Code kompiliert und im JIT-IMC 116 gespeichert
wurden, von der Laufzeitumgebung 112 mehrfach wiederverwendet
und ausgeführt
werden.
-
Der
JIT-IMC 116 wird zwar so dargestellt, als ob er sich in
der Laufzeitumgebung 112 befindet, es sind jedoch auch
andere Konfigurationen für
den JIT-IMC 116 möglich.
Zum Beispiel kann der JIT-IMC 116 Teil einer anderen Datenstruktur
in anderen Laufzeitmodulen, Sitzungen oder Umgebungen (nicht gezeigt)
sein, die vom Betriebssystem 110 bereitgestellt werden.
In anderen Beispielen, insbesondere solchen mit virtuellen Aufrufen,
kann der JIT-IMC 116 so ausgeführt sein, daß nativer
Code, der mit den aufgerufenen Verfahren verbunden ist, in bekannten
Datenstrukturen wie zum Beispiel virtuellen Verteilungstabellen
gespeichert ist.
-
Im
allgemeinen stellen dynamische Programmiersprachen wie zum Beispiel
Java zwei Arten der Synchronisation zur Verfügung, damit Softwaredesigner
ausführungsstrangsicheren
Code oder Softwareobjekte erzeugen können. Auf ein synchronisiertes
Softwareobjekt kann zu einem gegebenen Zeitpunkt nur ein Ausführungsstrang
zugreifen, wodurch eine Konflikt- oder Konkurrenzsituation zwischen
den beim Objekt verwendeten Parameter oder Variablen verhindert
wird. Mit anderen Worten können
globale Objekte und andere Objekte, auf die von mehr als einem Ausführungsstrang
zugegriffen werden kann, dadurch strangsicher gemacht werden, daß Software-Sperr-
und Freigabemechanismen eingeführt
werden, die verhindern, daß mehrere
Ausführungsstränge gleichzeitig
auf das Objekt zugreifen. Eine dieser Synchronisationsarten erlaubt
es, einen Codeblock (d.h. ein oder mehrere Statements) zur Synchronisation
freizugeben. Eine andere Art der Synchronisierung ermöglicht es,
Verfahren (d.h. den Aufruf eines Codeblocks) zur Synchronisation
freizugeben.
-
Dynamische
Programmiersprachen ermöglichen
in der Regel zum Zwecke der Synchronisation von Codeblocks und Verfahren
Synchronisationsstatements sowohl auf einem hohen Niveau oder auf Sprachenniveau
als auch auf einem niedrigen Niveau oder einfachen verwalteten Laufzeitniveau. Zum
Beispiel wird bei Java das Schlüsselwort "synchronisiert" auf der Sprachenebene
(d.h. dem hohen Niveau) dazu verwendet, einen Block oder ein Verfahren
als durch Synchronisation geschützt
zu erklären.
Der dem Schlüsselwort "synchronisiert" der Sprachenebene
auf der niedrigen Ebene oder der verwalteten Laufzeitebene entsprechende
einfache Ausdruck ist bei Java "monitorenter" und "monitor exit". Zur Vereinfachung
der folgenden Beschreibung werden die einfachen Synchronisationsausdrücke auf
dem niedrigen Niveau als "Sperren" und "Freigeben" bezeichnet, und
die Synchronisationsstatements auf dem hohen Niveau oder dem Sprachenniveau
werden durch die Verwendung des Schlüsselworts "synchronisiert" bezeichnet.
-
Wie
oben angegeben, umfassen die bekannten Escape-Analysetechniken in
der Regel eine Analyse des Gesamtprogramms auf der Basis der Annahme,
daß keine
zusätzlichen
Objektklassen geladen werden, nachdem die Programmausführung begonnen
hat (d.h. während
der Laufzeit). Insbesondere können
die Algorithmen zur Durchführung
der Escape-Analyse kontextsensitive Vorgehensweisen anwenden, die
interprozedurale Analysen eines kompilierten Programms durchführen, um
festzustellen, ob zu Aufgerufenen (z.B. aufgerufenen Verfahren,
Objekten etc.) weitergegebene Argumente statische Variablen zugeordnet
sind. Diese Ganzprogramm- oder interprozeduralen Analysen beruhen
auf der Annahme, daß während der
Laufzeit keine neuen Objektklassen geladen werden, und werden im
allgemeinen als geschlossene Analysen bezeichnet. Geschlossene Analysen
führen
oft zu einer aggressiven Optimierung des synchronisierten Codes.
Mit anderen Worten können
alle Synchronisationsoperationen für nicht globale Objekte und
globale Objekte ohne Konkurrenzsituationen zum Zeitpunkt der geschlossenen Analyse
elimiert werden, wodurch die Geschwindigkeit deutlich ansteigt,
mit der der Code durch den JIT-Compiler 114 ausgeführt werden
kann.
-
Die
hier beschriebene Synchronisations-Wiederherstellungsvorrichtung
und das hier beschriebene Synchronisations-Wiederherstellungsverfahren
machen es möglich,
in einer verwalteten Laufzeitumgebung (z.B. einer JVM-Umgebung) just-in-time
Code zu kompilieren, der anfänglich durch
aggressives Entfernen von Synchronisationsoperationen auf der Basis
der Annahme, daß keine neuen
Klassen dynamisch geladen werden (d.h. während der Laufzeit geladen
werden), optimiert wurde. Die anfängliche Optimierung (d.h. Desynchronisation)
kann auf einer herkömmlichen
oder bekannten Escape-Analyse beruhen, die eine Analyse vom geschlossenen
Typ durchführt,
die zu der Entfernung von Synchronisationsoperationen führt, die
mit zum Beispiel globalen Objekten ohne Konkurrenzsituationen oder
mit nicht globalen Objekten verbunden sind. Im Gegensatz zu den
bekannten Techniken können die
beschriebene Synchronisations-Wiederherstellungsvorrichtung und
das beschriebene Synchronisations-Wiederherstellungsverfahren im
Kontext des dynamischen Ladens von Klassen verwendet werden. Insbesondere
ermöglichen
es die beschriebene Resynchronisationsvorrichtung und das beschriebene
Resynchronisationsverfahren, daß ein
JIT-Compiler (z.B. der JIT-Compiler 114) feststellt, ob
eine neu geladene Klasse das Verhalten des gerade ausgeführten Programms
so verändert,
daß die
Optimierungsentscheidungen (z.B. das Entfernen der Synchronisationsoperationen)
nicht mehr gültig
sind, die während
der anfänglichen
Escape-Analyse vom geschlossenen Typ getroffen wurden. Mit anderen
Worten erfolgt, wenn der JIT-Compiler 114 während der Laufzeit
oder während
der Ausführung
des Programms oder des Codes, den er gerade verarbeitet, auf eine
neu geladene Klasse trifft, eine Escape-Analyse des ganzen Programms einschließlich der
neu geladenen Klasse, und für
den Code oder das Programm werden die Synchronisationsoperationen wiederhergestellt,
die erforderlich sind, um Konflikte oder Konkurrenzsituationen zwischen
mehreren Ausführungssträngen zu
vermeiden. Wie genauer im folgenden beschrieben ist, können die
hier beschriebene Synchronisations-Wiederherstellungsvorrichtung und das
hier beschriebene Synchronisations-Wiederherstellungsverfahren die
Synchronisation für
Aufrufstellen, die vorher als Folge einer anfänglichen geschlossenen Escape-Analyse
desynchronisiert wurden, wieder herstellen (wieder einführen). Zusätzlich analysieren
die beschriebene Synchronisations-Wiederherstellungsvorrichtung
und das beschriebene Synchronisations-Wiederherstellungsverfahren
den Laufzeitkontext und erzeugen einen Kompensationscode für zum Beispiel
Sperr- und Freigabeoperationen, damit die richtige Synchronisationssemantik
erhalten bleibt.
-
Um
die beispielhafte Situation, wie eine dynamisch hinzugefügte Klasse
(d.h. eine während
der Laufzeitausführung
eines Programms hinzugefügte Klasse)
das Ergebnis einer anfänglichen
Escape-Analyse vom geschlossenen Typ zunichte macht, ist in den 2 und 3 ein
entsprechender Java-Code dargestellt. Die 2 zeigt
den Java-Code für
eine Entfernung der Synchronisation für die Aufrufstelle A während der
anfänglichen
geschlossenen Escape-Analyse des Codes der 2. Das heißt, daß die Synchronisation
von der Aufrufstelle A entfernt werden kann, da zumindest anfänglich "obj" dem Verfahren "caller" nicht entkommen
kann. Mit anderen Worten kann auf "obj" nicht
von einem Ausführungsstrang
zugegriffen werden, der außerhalb des
Ausführungsstrangs
liegt, der gerade das Verfahren "caller" ausführt. Im
Ergebnis kann die synchronisierte Version von "foo" nach
der anfänglichen geschlossenen
Escape-Analyse durch eine unsynchronisierte Version von "foo" ersetzt werden.
-
Die 3 zeigt
einen Java-Code, der eine neue Klasse "Class2" beinhaltet, die von "Classl" abgeleitet ist.
Wenn, während
das Programm ausgeführt
wird, das den Code der 2 enthält, das Objekt "Class2" dynamisch geladen
wird, kann das in Classl definierte öffentliche statische Feld "global" auch ein Fall für die Class2
sein. Da das virtuelle Verfahren "do_nothing" in Class2 dem "obj" ein "global" zuordnet, wird "obj" für mehrere
Ausführungsstränge zugänglich (d.h.
es entweicht dem einen Ausführungsstrang).
Im Ergebnis ist das Entfernen der Synchronisation für die Aufrufstelle
A nicht länger
gültig, und
der fortgesetzte Ablauf des Programms kann eine Resynchronisation
des Codes erfordern, der mit der Aufrufstelle A verbunden ist.
-
Die 4 ist
ein Flußdiagramm
für eine
beispielhafte Art, wie der Just-in-Time-Compiler 114 der 1 konfiguriert
werden kann, um die Synchronisation wiederherzustellen oder um die
Aufrufstellen zu resynchronisieren, die von einer dynamisch geladenen
Klasse betroffen sind, wie zum Beispiel der Class2, die in Verbindung
mit den 2 und 3 oben beschrieben
wurde. Zu Beginn stellt die Laufzeitumgebung 112 fest,
ob offene Merkmale auftreten (z.B. eine neue Klasse geladen wird
(Block 400)), und hebt gegebenenfalls eine vorher gemachte Desynchronisationsentscheidung
auf. Wenn der JIT- Compiler 114 im
Block 400 feststellt, daß es keine neu geladene Klasse
gibt, bleibt der JIT-Compiler beim Block 400. Wenn dagegen
der JIT-Compiler 114 feststellt, daß eine neue Klasse geladen
wurde (Block 400), führt
der JIT-Compiler 114 eine Escape-Analyse des gesamten,
gerade ausgeführten
Programms einschließlich
der neu geladenen Klasse durch (Block 402). Die Escape-Analyse
kann mit jeder gewünschten
Technik ausgeführt
werden, die Objekte identifiziert, die einem Ausführungsstrang entweichen
(d.h. Objekte, auf die gleichzeitig von mehr als einem Ausführungsstrang
zugegriffen werden kann).
-
Nach
der Escape-Analyse (Block 402) stellt der JIT-Compiler 114 fest,
ob die während
einer anfänglichen
Analyse des ganzen Programms vor dem Laden der neuen Klasse (Block 400)
gemachten Annahmen weiter gelten (Block 404). Mit anderen
Worten stellt der JIT-Compiler 114 fest, ob die Escape-Analyse
(Block 402) festgestellt hat, daß die neu geladene Klasse das
Verhalten des Programms so verändert,
daß nun
mehrere Ausführungsstränge Zugriff
auf ein Objekt haben, das vorher strangsicher war (d.h. auf das
zu einem Zeitpunkt nur ein Ausführungsstrang
zugreifen konnte und das daher nicht dem gegenwärtigen Ausführungsstrang entweicht) und
das daher gegenwärtig
nicht mehr strangsicher ist. Insbesondere kann eine neu geladene
Klasse zur Ungültigkeit
der Entscheidung in einer früheren
Escape-Analyse und des Optimierungsvorgangs zum Entfernen unnötiger Synchronisationsstatements
für eine
Aufrufstelle führen.
Die während
einer früheren Escape-Analyse
durchgeführte
Desynchronisation beruht gewöhnlich
auf der Annahme, daß ein
oder mehrere Objekte strangsicher sind. Zum Beispiel werden die
während
einer anfänglichen
Escape-Analyse am Code der 2 gemachten
Annahmen durch das Laden der neuen Klasse (d.h. der Class2) im Code
der 3 ungültig.
-
Wenn
der JIT-Compiler 114 feststellt, daß die bei einer früheren Escape-Analyse
gemachten Annahmen) nicht mehr gelten (da z.B. ein oder mehrere Objekte
nicht mehr strangsicher sind und eine Resynchronisation erforderlich
sein kann), identifiziert der JIT-Compiler 114 die Aufrufstellen,
die von den nun unrichtigen Annahmen betroffen sind (Block 406).
Nachdem die betroffenen Aufrufstellen identifiziert wurden (Block 406),
stellt der JIT-Compiler 114 für jede dieser Stellen die Synchronisation
wieder her (Block 408), wie es genauer in Verbindung mit
der 5 im folgenden beschrieben wird.
-
Die 5 ist
ein detailliertes Flußdiagramm für eine beispielhafte
Art, wie der JIT-Compiler 114 konfiguriert werden kann,
um die Synchronisation für die
betroffenen Aufrufstellen wiederherzustellen (Block 408 in
der 4). Zuerst setzt der JIT-Compiler 114 alle
gegenwärtig
aktiven Ausführungsstränge aus
(Block 500). Das Aussetzen der Ausführungsstränge stellt sicher, daß die wie
im folgenden beschriebene Korrektur und Sperr/Freigabekompensation
auf sichere Weise ausgeführt
werden können (d.h.
ohne Schäden
am gegenwärtig
ausgeführten Prozeß anzurichten).
Da in der Praxis das dynamische Laden einer neuen Klasse während der
Laufzeit eine relativ seltene Aktivität ist, hat der Zeitaufwand (d.h.
die Zeitverzögerung),
der mit einem vollständigen
Aussetzen aller Ausführungsstrangaktivitäten verbunden
ist, einen im wesentlichen vernachlässigbaren Einfluß auf die
Gesamtproduktivität.
-
Nachdem
die gegenwärtig
ausgeführten Ausführungsstränge ausgesetzt
wurden (Block 500), stellt der JIT-Compiler 14 für die betroffenen
Aufrufstellen wieder ihre ursprünglichen
synchronisierten Versionen her (Block 502). Wenn zum Beispiel die
Zielcodeadressen geändert
wurden, um auf eine desynchronisierte Version eines Verfahrens zu
verweisen, werden die Zielcodeadressen erneut geändert (d.h. korrigiert), um
wieder die ursprünglichen Zielcodeadressen
zu erhalten.
-
Nach
der Korrektur (Block 502) führt der JIT-Compiler 114 für jeden
ausgesetzten Ausführungsstrang
erforderlichenfalls eine Sperr/Freigabekompensation aus (Block 504).
Eine Sperr/Freigabekompensation ist erforderlich, wenn nach dem
Aufruf eines nicht synchronisierten Codes vor der Korrektur (Block 502)
und vor der Rückkehr
zum aufrufenden Programm ein oder mehrere Ausführungsstränge ausgesetzt wurden. In solchen
Fällen
ermöglicht
es das Ausheben des Aussetzens der Ausführungsstrangaktivitäten unmittelbar
nach der Korrektur (Block 502), daß der JIT-Compiler 114 eine
synchronisierte Version eines Verfahrens aufruft, während die
bereits begonnene (d.h. vor der Aussetzung im Block 500 begonnene)
unsynchronisierte Version des gleichen Verfahrens weiter ausgeführt wird.
Als Folge davon kann auf das/die Objekt(e), die mit dem Verfahren
verbunden sind, gleichzeitig durch zwei Ausführungsstränge zugegriffen werden, was
zu einem unsicheren Zustand für
eine oder mehrere Ressourcen führen
kann.
-
Die
Art und Weise, wie die Sperr/Freigabekompensation angewendet wird,
kann anhand des in den 2 und 3 gezeigten
Beispielcodes verstanden werden. Wenn zum Beispiel der JIT-Compiler 114 feststellt,
daß nach
dem Laden von Class2 das Aufheben der Synchronisation für den Aufrufstelle
A nicht länger
gültig
ist (d.h. daß die
Escape-Analyse im
Block 402 der 4 ein oder mehrere Objekte identifiziert
hat, die ihren Ausführungssträngen entkommen),
führt der
JIT-Compiler 114 eine Resynchronisierung für die Aufrufstelle
A durch (d.h. stellt die Synchronisierung wieder her) (Block 408 der 4).
Wenn bei der Ausführung
der Sperr/Freigabekompensation (Block 504 der 5)
der JIT-Compiler 114 feststellt, daß ein erster Ausführungsstrang gegenwärtig mit
dem aktiven Aufruf einer unsynchronisierten Version von "foo" verbunden ist, ist
eine Sperr/Freigabekompensation erforderlich. Das heißt, daß, wenn
der erste Ausführungsstrang "global.do_nothing(this)" auszuführen beginnt
und gleichzeitig das "global" zu einem Fall für Class2 wird,
die über
das "do_nothing"-Verfahren dem "obj" ein "global" zuordnet, kann der
zweite Ausführungsstrang
gleichzeitig "global.foo()" ausführen. Als
Folge davon kann der JIT-Compiler 114 versuchen, am gleichen "obj" sowohl die synchronisierte
als auch die unsynchronisierte Version von "foo" auszuführen, wodurch
die Synchronisationssemantik verletzt und ein unsicherer gemeinsamer
Ressourcenzustand erzeugt wird.
-
Die 6 zeigt
einen beispielhaften Pseudeo-Code, der eine Art aufzeigt, wie der
JIT-Compiler 114 der 1 konfiguriert
werden kann, um für
jeden mit einer betroffenen Aufrufstelle verbundenen Ausführungsstrang
eine Sperr/Freigabekompensation durchzuführen (Block 504 der 5).
Im allgemeinen durchlauft das beispielhafte Verfahren der 6 alle
Stapelblöcke
(d.h. die mit den gegenwärtig
aktiven Ausführungssträngen ver bundenen
Blöcke)
von der Oberseite bis zum Boden des Stapels. Das heißt, daß das in
der 6 beispielhaft gezeigte Verfahren für jede betroffene
Stelle <x,y> (d.h. für jede Aufrufstelle,
deren Synchronisation wiederhergestellt werden muß) dadurch
die richtige Synchronisationssemantik beibehält, daß es die Sperroperation für z, dem
Aufrufer von <x,y> und dem letzten Block
auf dem Stapel (d.h. der auf dem Stapel aktiv ist), wieder ausführt. Wenn
z ein statisches Verfahren ist, wird die Sperroperation am Klassenobjekt
durchgeführt. Wenn
z dagegen ein Fallverfahren ist (d.h. einen virtuellen Aufruf beinhaltet),
lokalisiert der JIT-Compiler 114 das Objekt, an dem die
Sperroperation auszuführen
ist, mit einer beliebigen gewünschten
Technik.
-
Die
Funktion "compensate_lock_unlock()" kann dazu verwendet
werden, eine Sperroperation durchzuführen, um die richtige Sperrsequenz
beizubehalten. Außerdem
stellt diese Funktion sicher, daß die richtige Freigabesequenz
ausgeführt
wird. Wenn zum Beispiel der vom JIT-Compiler 114 verarbeitete Code
auf Java beruht, müssen
die Sperr- und Freigabeoperationen
richtig zusammengeführt
werden, um ein synchronisiertes Verfahren auszuführen. Insbesondere gibt es
zwei Plätze,
an denen nach dem Verlassen eines synchronisierten Verfahrens und
während
des destruktiven Abwickelns eines synchronisierten Verfahrens während einer
Ausnahmebehandlung Freigabeobjekte ausgeführt werden müssen.
-
Das
in der 6 gezeigte beispielhafte Verfahren ermöglicht es
dem JIT-Compiler 114,
nur für jene
Abschnitte des Codes eine Kompensation durchzuführen, die von einer neu hinzugefügten Klasse
betroffen sind, wobei diejenigen Abschnitte des Codes unbehandelt
bleiben, die von der neu geladenen Klasse nicht beeinflußt werden.
Mit anderen Worten kann das einfache Korrigieren des Epilog-Codes
der unsynchronisierten Version eines Verfahrens den Code mehr als
erforderlich de-optimieren, da die Laufzeitumgebung für andere
Aufrufstellen, die von der neu geladenen Klasse nicht beeinflußt werden, eine
de-synchronisierte Version des Verfahrens erforderlich machen kann
(oder die ohne das Entstehen von Konkurrenzsituationen verwendet
werden können).
Die Escape-Analyse des JIT-Compilers 114 kann alle jene
Aufrufstellen, die von einer neu geladenen Klasse beeinflußt werden
(d.h. die Aufrufstellen, für
die die ursprüngliche
Annahme, daß die
Synchronisation sicher aufgehoben werden kann, nicht mehr gilt),
und jene Aufrufstellen einzeln identifizieren, für die weiter sicher eine desynchronisierte
Version des Verfahrens verwendet werden kann.
-
Die 7 zeigt
einen beispielhaften Java-Code, bei dem das Entfernen der Synchronisation trotz
des dynamischen Ladens von Class2 (2) gültig bleibt.
Insbesondere bleibt der Aufruf des unsynchronisierten "foo" an der Aufrufstelle
B gültig, wenn
Class2 neu geladen wird, da bei der Escape-Analyse festgestellt
wird, daß der
Typ von "obj" nicht vom Typ Class2
ist.
-
Die 8 und 9 zeigen
beispielhaft, wie das compensate_lock_unlock-Verfahren des beispielhaften
Pseudo-Codes der 6 ausgeführt werden kann. Allgemein
ermöglicht
es die compensate_lock_unlock-Funktion dem JIT-Compiler 114,
die Rückkehradresse
am Boden eines Blockstapels bei einem gerade ausgeführten unsynchronisierten
Verfahren auf einen Stubcode umzuleiten oder zu "entführen", der eine geeignete
Freigabeoperation ausführt.
Bei dem in der 8 gezeigten Beispiel identifiziert
der JIT-Compiler 114 den Ort der Rückkehradresse eines unsynchronisierten
Verfahrens "foo", für das eine
Freigabeoperation auszuführen
ist. Wie in der 9 gezeigt, ersetzt der JIT-Compiler 114 die
Rückkehradresse
im Blockstapel des unsynchronisierten Verfahrens foo' durch die Adresse
des Stubcodes. Wenn die Ausführung
des unsynchronisierten Verfahrens foo' beendet ist, wird damit die Steuerung
auf den geeigneten Stubcode übertragen.
Der Stubcode führt
die erforderliche Freigabeoperation aus und ermöglicht es dem JIT-Compiler 114,
zu der ursprünglichen
Rückkehradresse
zurückzuspringen.
Der Stubcode enthält
eine fest kodierte Handhabungsadresse, die die reale Adresse des
Objekts darstellt, für
die eine Freigabeoperation auszuführen ist.
-
Die 10 zeigt
einen beispielhaften Pseude-Code, der eine Art zeigt, wie das Cookie COMP_UNLOCK_TAG
in Verbindung mit der in den 8 und 9 gezeigten
beispielhaften Technik dazu verwendet werden kann, eine Rückkehradresse in
einem Stapelabwickelkontext wiederherzustellen. In einem Stapelabwickelkontext
benutzt die Laufzeitumgebung (z.B. eine virtuelle Maschine) in der
Regel die Rückkehradresse,
um den nächsten
Block im Stapel zu finden. Zum Beispiel speichert die bekannte Open
Runtime Plattform (ORP) die Startadresse und die Endadresse für jedes
Verfahren in einer Tabelle. Ist eine Codeadresse gegeben, kann die
virtuelle Maschine dann anhand der Tabelle das Verfahren finden,
das die Codeadresse enthält.
Durch ein Umleiten oder Entführen
der Rückkehradresse
wie in Verbindung mit den 8 und 9 beschrieben kann
anhand der Tabelle jedoch nicht mehr der richtige nächste Block
festgestellt werden.
-
Der
Pseudo-Code der 10 enthält das Cookie COMP_UNLOCK_TAG,
das anzeigt, daß der diesem
Tag benachbarte Code der Kompensations-Freigabe-Stubcode ist. Wie
in der 9 gezeigt, befinden sich die reale Rückkehradresse
und das COMP_UNLOCK_TAG zusammen unmittelbar vor dem Stubcode. Wenn
der JIT-Compiler 114 in der Tabelle kein zugehöriges Verfahren
finden kann, prüft
er daher, ob das benachbarte Wort das Cookie COMP_UNLOCK_TAG ist.
Wenn das Cookie gefunden wird, gibt der JIT-Compiler 114 die
reale Rückkehradresse
zurück,
die unmittelbar vor dem Cookie steht. Das Cookie COMP_UNLOCK_TAG
ist per definitionem eine illegale Codesequenz, so daß der JIT-Compiler 114 das
Cookie nicht mit kompiliertem Code verwechselt.
-
Im
Falle eines destruktiven Stapelabwickelprozesses kompensiert der
JIT-Compiler 114 das Fehlen
der Freigabeoperation durch die Identifikation des gegenwärtig aktiven
Stapelrahmens über
das Cookie COMP_UNLOCK_TAG, wie es in Verbindung mit der 10 beschrieben
wurde, wodurch der reale Objektbezug aus der Handhabungsadresse
im Stubcode extrahiert und die Freigabeoperation am Objekt durchgeführt wird.
-
Die
Sperr/Freigabeoperation wurde vorstehend beispielhaft in Verbindung
mit Verfahren oder Aufrufen beschrieben, der Fachmann erkennt jedoch,
daß die
beschriebe nen Techniken und Vorrichtungen leicht an die Verwendung
mit Codeblöcken angepaßt werden
können,
um ein ähnliches
oder identisches Ergebnisse zu erhalten.
-
Die 11 ist
eine Blockdarstellung eines beispielhaften Prozessorsystems 1120,
das dazu verwendet werden kann, die beschriebenen Vorrichtungen
und Verfahren zu implementieren. Zum Beispiel können die beschriebenen Verfahren
in einem Speicher gespeicherte und durch einen mit dem Speicher
verbundenen Prozessor ausgeführte
Anweisungen sein. Wie in der 11 gezeigt,
umfaßt das
Prozessorsystem 1120 einen Prozessor 1122, der
mit einem Verbindungsbus oder Netzwerk 1124 verbunden ist.
Der Prozessor 1122 kann jeder geeignete Prozessor, jede
geeignete Prozessoreinheit oder jeder geeignete Mikroprozessor sein,
wie zum Beispiel ein Prozessor aus der Intel Itanium®-Familie, der
Intel X-Scale®-Familie,
der Intel Pentium®-Familie usw. Es ist zwar
in der 11 nicht gezeigt, das System 1120 kann
jedoch auch ein Multiprozessorsystem sein und damit einen oder mehrere
zusätzliche Prozessoren
enthalten, die mit dem Prozessor 1122 identisch oder ihm ähnlich sind
und die mit den Verbindungsbus oder Netzwerk 1124 verbunden
sind.
-
Der
Prozessor 1122 der 11 ist
mit einem Chipsatz 1128 verbunden, der eine Speichersteuerung 1130 und
eine Eingabe/Ausgabe-(I/O)-Steuerung 1132 enthält. Wie
es allgemein bekannt ist, umfaßt
ein Chipsatz in der Regel I/O- und Speicherverwaltungsfunktionen
sowie eine Anzahl von Allzweck- und/oder Spezialregistern, Zeitgeber
usw., auf die von dem oder den Prozessoren, die mit dem Chipsatz
verbunden sind, zugegriffen werden kann und die von diesen Prozessoren
verwendet werden können.
Die Speichersteuerung 1130 führt Funktionen aus, die es
dem Prozessor 1122 (oder den Prozessoren, wenn es mehrere
gibt) ermöglichen,
auf einen Systemspeicher 1134 zuzugreifen, der jede gewünschte Art
von flüchtigem
Speicher umfassen kann, etwa zum Beispiel einen statischen Direktzugriffsspeicher
(SRAM), einen dynamischen Direktzugriffsspeicher (DRAM) usw. Die
I/O-Steuerung 1132 führt
Funktionen aus, die es dem Prozessor 1122 ermöglichen, über einen
I/O-Bus 1140 mit peripheren Eingabe/Ausgabe-(I/O)-Vorrichtungen 1136 und 1138 in
Verbindung zu treten. Die I/O-Vorrichtungen 1136 und 1138 können jede
Art von I/O-Vorrichtung sein, etwa zum Beispiel eine Tastatur, ein
Bildschirmdisplay oder ein Monitor, eine Maus usw. In der 11 sind
die Speichersteuerung 1130 und die I/O-Steuerung 1132 innerhalb
des Chipsatzes 1128 als separate Funktionsblöcke dargestellt,
die von diesen Blöcken
durchgeführten
Funktionen können
jedoch in einer einzigen Halbleiterschaltung integriert sein oder
unter Verwendung von zwei oder mehr separaten integrierten Schaltungen
ausgeführt
werden.
-
Es
wurden bestimmte Verfahren, Vorrichtungen und Produkte beschrieben;
der Umfang des Patents ist darauf jedoch nicht beschränkt. Im
Gegenteil, das Patent deckt alle Verfahren, Vorrichtungen und Produkte
ab, die unter den Umfang der folgenden Patentansprüche fallen.