-
HINTERGRUND
DER ERFINDUNG
-
Die vorliegende Erfindung ist auf
das Kompilieren von Computerprogrammen gerichtet. Insbesondere betrifft
sie das sogenannte Inlinen von virtuellen Verfahren (Methods).
-
1 zeigt
ein typisches Computersystem 10. Ein Mikroprozessor 12 empfängt Daten
und Befehle für
einen Betrieb mit diesen von einem auf einer Platte angeordneten
(On-Board) Cache-Speicher oder einem weiteren Cache-Speicher 18,
möglicherweise über die
Vermittlung eines Cache-Controllers 20, der wiederum derartige
Daten von einem Systemlese-/Schreib-Speicher ("RAM") 22 durch
einen RAM-Controller 24,
oder von verschiedenen Peripherieeinrichtungen über einen Systembus 26,
empfangen kann.
-
Die Daten- und Befehlsinhalte des
RAM 22 werden normalerweise von Peripherieeinrichtungen wie
einer Systemplatte 27 geladen worden sein. Andere Quellen
umfassen eine Kommunikationsschnittstelle 28, die Befehle
und Daten von anderen Computersystemen empfangen kann.
-
Die Befehle, die der Mikroprozessor
ausführt,
sind Maschinenbefehle. Diese Befehle werden letztlich von einem
Programmierer bestimmt, aber es gibt selten einen Programmierer,
der mit den spezifischen Maschinenbefehlen vertraut ist, zu denen
seine Anstrengungen schließlich
führen.
In einer typischeren Weise schreibt der Programmierer einen "Quellcode" in einer Sprache
einer höheren
Ebene, aus dem eine Computersoftware – die konfiguriert ist dies
zu tun, diese Maschinenbefehle oder einen "Objektcode" erzeugt.
-
2 zeigt
diese Sequenz. Der Block 30 der 2 stellt einen Kompilierer-Prozess (Compiler-Prozess) dar, den
ein Computer unter der Anweisung eines Kompilierer-Objektcodes ausführt. Dieser Objektcode
wird typischer Weise auf der Systemplatte 27 oder irgendeinem
anderen Medium, das von einer Maschine lesbar ist, gespeichert und
durch eine Übernagung
von elektrischen Signalen in den Systemspeicher 24 geladen,
um das Computersystem zu konfigurieren, um als ein Kompilierer (Compiler) zu
arbeiten. Der fortwährende
Speicher des Kompilierer-Objektcodes kann aber anstelle davon in
einem Serversystem entfernt von der Maschine sein, die die Kompilierung
ausführt.
Die elektrischen Signale, die die digitalen Daten führen, über die
die Computersysteme den Code austauschen, sind beispielhafte Formen
von Trägerwellen,
die die Information transportieren.
-
Der Kompilierer (Compiler) wandelt
einen Quellcode in einen weiteren Objektcode um, den er in einem
von einer Maschine lesbaren Speicher, wie einem RAM 24 oder
einer Platte 27, platziert. Ein Computer wird Befehlen
dieses Objektcodes bei der Ausführung
einer Anwendung 32 folgen, die typischer Weise einen Ausgang
von einem Eingang erzeugt. Der Kompilierer 30 ist selbst
eine Anwendung, und zwar eine, bei der der Eingang der Quellcode
ist und der Ausgang ein Objektcode ist, aber der Computer, der die
Anwendung 32 ausführt,
ist nicht notwendiger Weise der gleiche wie derjenige, der den Kompilierer-Prozess ausführt.
-
Der Quellcode muss nicht unbedingt
von einem menschlichen Programmierer direkt geschrieben werden.
Integrierte Entwicklungsumgebungen automatisieren den Quellcode-Schreibprozess
oft bis zu dem Ausmaß,
dass für
viele Anwendungen ein sehr geringer Teil des Source-Codes (Quellcodes) "manuell" erzeugt wird. Auch
wird ersichtlich werden, dass der Ausdruck Kompilierer (Compiler)
in der folgenden Diskussion breit verwendet wird, wobei er sich
auf Umwandlungen auf einen Code auf einer niedrigen Ebene (low-level
code), beispielsweise auf den Byte-Code, der der JavaTM virtuellen
Maschine virtuellen Maschine eingegeben wird, erstreckt, den Programmierer
fast niemals direkt schreiben (Sun, das Sun Logo, Sun Microsystems,
und Java sind Marken oder registrierte Marken von Sun Microsystems,
Inc., in den Vereinigten Staaten und anderen Ländern). Obwohl es so erscheint,
als ob 2 einen Stapel-Prozess
(Batch-Prozess) vorschlägt,
bei dem der gesamte Objektcode einer Anwendung erzeugt wird, bevor
irgendein Teil davon ausgeführt
wird, kann der gleiche Prozessor ferner den Code sowohl kompilieren
als auch ausführen,
wobei in diesem Fall der Prozessor seine Kompilierer-Anwendung gleichzeitig
mit – und
in der Tat in einer Weise, die abhängig sein kann von – seiner
Ausfuhrung des Ausgangsobjektcodes des Kompilierers ausführen kann.
-
Die verschiedenen Befehls- und Datenquellen,
die in 1 gezeigt sind,
bilden eine Geschwindigkeitshierarchie. Mikroprozessoren erzielen
einen großen
Grad ihrer Geschwindigkeit durch ein "Pipelining" der Befehlsausführung: Frühere Stufen von einigen Befehlen
werden gleichzeitig mit späteren
Stufen von früheren
ausgeführt.
Um die Pipeline bei der sich ergebenden Geschwindigkeit versorgt
zu halten, liefern sehr schnelle On-Board-Register die Operanden
und Versatzwerte (Offset-Werte), von denen angenommen wird, dass
sie am häufigsten
verwendet werden. Andere Daten und Befehle, bei denen es wahrscheinlich
ist, dass sie verwendet werden, werden in dem On-Board-Cache gehalten,
auf den ein Zugriff ebenfalls schnell ist. Ein Signalabstand zu dem
Cache-Speicher 18 ist größer als für einen On-Board-Cache, so dass ein Zugriff auf
ihn, obwohl er sehr schnell ist, nicht so schnell wie für einen On-Board-Cache ist.
-
In der Geschwindigkeitshierarchie
befindet sich als nächstes
das System RAM 22, welches gewöhnlicher Weise relativ groß ist und
deshalb gewöhnlicher
Weise aus einem relativ kostengünstigem dynamischen
Speicher besteht, der eine Tendenz aufweist, wesentlich langsamer
zu sein als der kostenaufwendige statische Speicher, der für Caches verwendet
wird. Sogar innerhalb eines derartigen Speichers ist jedoch ein
Zugriff auf Orte in der gleichen "Seite" im Vergleich mit einem Zugriff auf
Orte auf unterschiedlichen Seiten relativ schnell. Beträchtlich
langsamer als jede Vorgehensweise ist die Ermittlung von Daten von
dem Platten-Controller, aber diese Quelle ist gewöhnlicher
Weise nicht annähernd so
langsam wie das Herunterladen von Daten durch eine Kommunikationsstrecke 28 sein
kann.
-
Die Geschwindigkeitsdifferenzen zwischen diesen
verschiedenen Quellen können
sich über
vier Größenordnungen
erstrecken, so dass Entwerfer von Kompilierern beträchtliche
Anstrengungen darauf richten, dass Kompilierer veranlasst werden,
ihre Ausgangsbefehle so zu organisieren, dass sie eine Hochgeschwindigkeits-Ressowcenverwendung
maximieren und die langsamsten Ressourcen so weit wie möglich vermeiden.
Diese Anstrengung wird durch die gemeinsame Programmiertechnik einer Aufteilung
eines Programms in einen Satz von Prozeduren, die an jeweilige spezifische
Aufgaben (Tasks) gerichtet sind, kompliziert.
-
Die stärkste Komplexität ergibt
sich aus Prozeduren, die andere Prozeduren auf einer niedrigeren
Ebene aktivieren, um ihre Arbeiten auszuführen. Das heißt, verschiedene "Aufrufer" ("Caller") Prozeduren transferieren
eine Steuerung an eine gemeinsame "Aufgerufener" ("Callee") Prozedur in einer
derartigen Weise, dass dann, wenn der Aufgerufene aussteigt (exits),
er eine Steuerung an immer die Prozedur zurückgibt, die ihn aufgerufen
hat. Von dem Standpunkt des Programmierers ist diese Organisation
vorteilhaft, weil es das Schreiben von Codes modularer und somit
besser behandelbar macht. Sie erlaubt auch eine Code-Wiederverwendung:
Eine gemeinsame Prozedur muss nicht an jede Stelle kopiert werden, an
der sie verwendet werden soll. Dies bedeutet, dass irgendwelche Überarbeitungen
nicht an zahlreichen Stellen wiederholt durchgeführt werden müssen. Aber
eine derartige Organisation fügt
einen Zusatz hinzu: Der Zustand des Aufrufers muss gespeichert werden,
Cache-Verfehlungen und Seiten-Fehler können auftreten, und Prozessor-Pipelines
müssen
oft ausgeräumt
werden. Mit anderen Worten, das System geht in der Geschwindigkeitshierarchie
herunter.
-
Somit führen optimierende Kompilierer ("Compiler") oft ein "Inlining" von kurzen oder
häufig verwendeten
Prozeduren aus: Sie kopieren den Körper der Prozedur – ohne den
Prozedur-Prolog und Epilog – an
jede Stelle, an der der Source-Code ihn aufruft. Mit anderen Worten,
der Compiler kann eine Wiederverwendung gegenüber einem Betriebsverhalten
opfern. Aber der Programmierer zieht noch einen Nutzen aus der Codeschreibe-Modularität. Ein Inlining
weist einen zusätzlichen
wichtigen Nutzen auf Ein Kompilieren der inlined-Prozedur in einen spezifischen
Aufruflcontext (Calling context) legt einem optimierenden Kompilierer
mehr Information frei und erlaubt dadurch dem Optimierer, einen
effizienteren Maschinencode zu erzeugen.
-
Gewisse modernere Programmiersprachen komplizieren
den Inlining-Prozess. Um dies zu würdigen, sollte man sich die
grundlegenden Funktionen/Merkmale von Objekt-orientierten Sprachen
ins Gedächtnis
zurückrufen.
In derartigen Sprachen, wobei die Java-Programmiersprache und C++
Beispiele davon sind, wird der Code in Einheiten von "Objekten" geschrieben, die
Exemplare (Instanzen) von "Klassen" sind. Die Definition
einer Klasse listet die "Mitglieder" ("Members") von irgendeinem
Objekt auf, die ein Exemplar (Instance) der Klasse ist. Ein Mitglied
kann eine Variable sein. Oder sie kann eine Prozedur sein, die in
diesem Kontext typischer Weise als ein "Verfahren" ("Method") bezeichnet wird.
-
3 zeigt
eine Vorgehensweise, in der man die Java-Programmiersprache verwenden
kann, um Klassen zu definieren. Ihr erstes Codesegment definiert
Objekte einer Klasse A, so dass sie unter anderem jeweilige Gleitpunkt-variablen
Mitglieder h und w und ein Verfahrensmitglied ml einschließen, das
(in dem Beispiel) mit den Mitgliedsvariablen des Objekts arbeitet,
um einen Gleitpunktwert zurückzugeben, der
deren Produkt darstellt. Jedes Exemplar der Klasse A wird seine
jeweiligen Mitgliedsvariablen h und w aufweisen, und es wird auch
ein Verfahren (Method) vorhanden sein, dessen Name ml ist und das
auf diesem Exemplar aufgerufen werden kann. (Obwohl, wie nachstehend
diskutiert werden wird, dieses Verfahren unter Umständen nicht
den gleichen Betrieb für
sämtliche
Exemplare ausführen kann).
-
4 zeigt
die Verwendung der ml-en. Da das Verfahren ml ein Klassenmitglied
ist, kann es lediglich dadurch aktiviert werden, dass es bei einem Exemplar
dieser Klasse "aufgerufen" ("called on") wird. Somit deklariert
die erste Anweisung (Statement) des linken Codefragments der 4 die Variable a eine Referenz
auf ein Objekt einer Klasse A zu enthalten. Sie ordnet einem Objekt
dieser Klasse auch einen Speicher zu und initialisiert die Variable
a mit einer Referenz auf ein neu zugeordnetes Klasse A-Objekt. Die
zwei Anweisungen danach platzieren Werte in zwei Mitgliedsvariablen
dieses Objekts. Die letzte Anweisung übergibt die Objektreferenz
in der Variablen a an eine Prozedur X.foo.
-
Diese Objektreferenz kann an diese
Prozedur übergeben
werden, weil, so wie dies das rechte Codefragment anzeigt, foo als
einen Parameter des Typs A aufweisend definiert wurde und die Variable
a zu dieser Klasse gehört.
Wie dieser Code anzeigt, ruft die Definition von foo das Verfahren
(Method) ml auf seinem Parameter o auf. Dies ist legal, weil ein Verfahren
mit diesem Namen ein Mitglied der Klasse A ist. Wenn die Objektreferenz
in der Variablen a an die Prozedur foo übergeben wird, wird das Verfahren ml
mit Werten der Variablen h und w dieses Objekts ausgeführt.
-
Eine zentrale Funktion bzw. ein zentrales Merkmal
von derartigen Objekt-orientierten Sprachen ist die "Vererbung" ("Inleritance"). Man kann in einer
neuen Klassendefinition deklarieren, dass die neue Klasse ein "Kind" ("Child") einer anderen "Eltern" ("Parent") Klasse ist. Das
extends-Schlüsselwort
in der Definition der Klasse B in 3 deklariert die
Kind-Beziehung dieser Klasse zu der Klasse A. Dies bedeutet, dass
sämtliche
Objekte der Klasse B so angesehen werden, dass sie auch Objekte
der Eltern-Klasse
sind, obwohl dies umgekehrt nicht notwendiger Weise zutrifft. Da
sie auch zu der Klasse A gehören,
werden sämtliche
Objekte der Klasse B jeweilige Werte der Mitgliedsvariablen h und
w einschließen
und das Verfahren ml kann auf ihnen aufgerufen werden. Dies trifft
sogar zu, obwohl die Definition der Klasse B nicht explizit diese
Mitglieder auflistet: Die Klasse B erbt sie von der Klasse A. Somit wird
der Kompilierer einem Objekt b der Klasse B erlauben, an foo übergeben
zu werden, obwohl die Signatur von foo erfordert, dass sein Parameter
eine Referenz auf ein Objekt der Klasse A sein soll.
-
Wie bis hierhin beschrieben, kompliziert
der Vererbungsmechanismus den Inlining-Prozess nicht besonders.
Der Kompilierer kopiert einfach in den Objektcode von foo den Objektcode,
der sich aus der Definition des Verfahrens ml in der Klasse A ergibt, und
dieser inlined-Code kann verwendet werden, obwohl an foo manchmal
Referenzen auf Objekte der Klasse B übergeben werden.
-
Nun sei aber die Definition der Klasse
C der 3 betrachtet.
Die Definition führt
eine "Außerkraftsetzung" (overriding) des
geerbten Verfahrens ml aus. Weil sei ein Kind der Klasse A ist,
umfasst die Klasse C notwendigerweise ein Mitgliedsverfahren ml,
aber die Klasse C gibt diesem Verfahren eine andere Definition als
ihre Definition für
andere Objekte der Klasse A. Wenn an foo ein Objekt c der Klasse
C übergeben
wird – dies
ist legal, da die Klasse C ein Kind der Klasse A ist – so erfordert
der Aufruf des Verfahrens ml bei einem Objekt o einen anderen Code
als den Code, der benötigt
wird, wenn an foo ein Objekt übergeben
wird, dessen Klasse A oder B ist. Verfahren (Methods), die außer Kraft
gesetzt (overridden) werden dürfen,
werden als "virtuell" ("virtual") bezeichnet, und
virtuelle Verfahren, die außer
Kraft gesetzt worden sind, werden als "polymorph" ("polymorphic") bezeichnet. Aufrufe
an derartige Verfahren werden als "virtuelle Aufrufe" ("virtual calls") bezeichnet, die
durch die Tatsache unterschieden werden, dass der Ort des aufgerufenen
Verfahrens zur Laufzeit (runtime) berechnet werden muss. Demzufolge
kann ein Inlining von einer Form eines polymorphen Verfahren unrichtige
Ergebnisse hervorrufen.
-
Aggressive optimierende Kompilierer
führen ein
Inlining von polymorphen Verfahren in einigen Fällen trotzdem aus. Sie vermeiden
unrichtige Ergebnisse durch "Überwachen" des inlined Codes
mit einem Test, um zu bestimmen, ob die inlined-Form des Verfahrens
konsistent mit der spezifischen Klasse des "Empfänger"-Objekts ist, auf
dem das Verfahren aufgerufen wird. Wenn dem so ist, wird ein virtueller Aufruf
vermieden.
-
Es würde dennoch besser sein, wenn
ein aufgerufenes Verfahren direkt inlined werden könnte, d.
h. ohne einen Schutzcode, der eine Ausführung an einen virtuellen Aufruf
richten kann, weil ein derartiges Schützen sowohl Kosten mit sich
bringt als auch den Kompilierer um bestimmte Optimierungsgelegenheiten
bringt. Deshalb kann ein optimierender Kompilierer den Code suchen,
um zu bestimmen, ob irgendeine reale Möglichkeit besteht, dass eine
Form des Aufgerufenen – Verfahrens
(Callee Method) außer
der Kandidat für
ein Inlining jemals an der Aufrufstelle aufgerufen werden würde, wo
ein Inlining in Erwägung
gezogen wird, und dann den Aufgerufenen (calle) zu inlinen, wenn
dem nicht so ist. Unglücklicherweise
kann der Kompilierer diese Bestimmung in einer Dynamik-Kompilierungsumgebung,
die eine Klasse des Verfahrens des Aufgerufenen (Callee Method)
außer
Kraft setzt, geladen werden kann, nachdem der Aufrufer kompiliert
ist und eine Ausführung des
sich ergebenden Codes begonnen hat, nicht schlüssig durchführen.
-
Somit haben einige Entwickler vorgeschlagen,
dass Inlining eventuell auszuführen,
d. h. den Aufrufer unter der Annahme zu kompilieren, dass der Aufgerufene
nicht außer
Kraft gesetzt worden ist, und dann den Aufrufer später zu kompilieren,
wenn eine später
geladene Klasse ihn außer
Kraft setzt. Dies ist einfach genug, wenn der Aufrufer gerade nicht
ausgeführt
wird, wenn die Rekompilierung auftritt: Die neue Kompilierung ersetzt
die alte, und die nächste Aktivierung
des Aufrufers erhält
den korrigierten Code. Die Sache ist aber komplizierter, wenn der
Aufrufer gegenwärtig
gerade ausgeführt
wird, wenn das Ereignis, das die Rekompilierung notwendig macht, auftritt.
In diesem Fall müssen
derartige Systeme in der Lage sein, einen Ausführungszustand entsprechend
zu einer Kompilierung eines Verfahrens auf den "äquivalenten" Ausführungszustand
für eine
andere Kompilierung dieses Verfahrens zu ändern. Dieser Prozess ist als "Ersetzung auf dem
Stapel" ("On-Stack Replacement") bezeichnet worden.
-
Die Self-Sprache (Selbst-Sprache)
war unter Umständen
das erste Programmiersystem, um eine On-Stack-Ersetzung zu implementieren.
In dieser Sprache erzeugt der Kompilierer Strukturen in Assoziation
mit verschiedenen "Deoptimierungspunkten" (deoptimization
points") in dem
kompilierten Code für ein
Verfahren (Method). Diese Strukturen enthalten Information, die
ermöglichen,
dass ein "Quellzustand" ("source state") des Verfahrens,
d. h. der Zustand der Variablen des Verfahrens, wie von der Interpretation
des Quellcodes definiert, aus dem "Maschinenzustand" ("machine
state") des kompilierten Codes
an dem assoziierten Deoptimierungspunkt zurückgewonnen werden. Wenn eine
weitere Kompilierung später
zu einer Invalidierung einer Annahme, von der die Kompilierung abhängt, führt, beispielsweise,
dass der Aufgerufene nicht außer
Kraft gesetzt worden ist, muss diese Kompilierung an einem derartigen
Deoptimierungspunkt auftreten. Das Self-System gewinnt dann den
Quellzustand zurück, rekompiliert
das Verfahren ohne die verletzte Annahme, berechnet den entsprechenden
Maschinenzustand der neuen Kompilierung aus dem Quellzustand und
ersetzt Registerwerte und Einträge
in dem Stapelrahmen (stack frame) des Verfahrens, um sie konsistent
mit dem neuen Maschinenzustand zu machen.
-
Es lässt sich leicht ersehen, dass
eine derartige n-Stack-Ersetzungsmöglichkeit relativ komplex ist.
Ferner können
die Datenstrukturen, die benötigt werden,
um dies zu unterstützen,
alarmierend voluminös
werden. Auch verhindert die Aufrechterhaltung von Deoptimierungspunkten,
dass ein Codeplaner (Code-Scheduler) einen Code umordnet, den derartige
Deoptimierungspunkte trennen.
-
Chambers C. et al.; "An Efficient Implementation
of SELF, a dynamically-typed object-oriented Language Based on Prototypes", OOPSLA Conference
Proceedings, Special Issue of Sigplan Notices, Vol. 24, Nr. 10,
Oktober 1989, Seiten 49–70
beschreibt die Self-Sprache, eine dynamisch-geschriebene Objektorientierte
Sprache und wie sie effizient implementiert werden kann. Sie verwendet
ein Nachrichten-Lining (messaging lining) und inkrementale Rekompilierungs-Inlined-Stapel-Rahmen
(incremental recompilation inlined stack frames), die bei der Debugging-Zeit
rekonstruiert werden.
-
ZUSAMMENFASSUNG
DER ERFINDUNG
-
Die vorliegende Erfindung erlaubt,
dass eine signifikante Anzahl von Orten mit virtuellen Aufrufen direkt
inlined werden, ohne dass irgendwelche Beschränkungen auf die Kompilierung
des Aufrufers auferlegt werden, und sie hält die Aufgabe einer Erfassung
und einer Reaktion auf Annahme-Invalidierungsänderungen
relativ einfach. In Übereinstimmung
mit der vorliegenden Erfindung inlined der Kompilierer virtuelle
Aufrufe direkt nur an Stellen, an denen der Kompilierer zur Kompilierungszeit
wissen kann, dass der Empfängerausdruck
während
irgendeiner Laufzeitausführung
des aufrufenden Verfahrens notwendigerweise auf ein Objekt Bezug
nehmen wird, dessen Klasse geladen wurde, als diese besondere Ausführung des
Aufrufers begann. Wenn diese Bedingung erfüllt wird, dann muss eine Kompilierung,
die einen Aufgerufenen außer
Kraft setzt, während
der Ausfuhrung des Aufrufenden nicht dazu führen, dass der Aufrufende für eine Rekompilierung unterbrochen
wird, weil die Implementierung des Aufgerufenen-Verfahrens durch den Empfänger in
diesem Fall nicht die außer
Kraft setzende sein kann, und die Inlined-Implementierung deshalb die richtige sein
muss. Dies beseitigt vollständig
die Notwendigkeit, Einträge
in dem Stapelrahmen (stack frame) des Aufrufenden zu ersetzen.
-
In einer Ausführungsform werden derartige Stellen
leicht gefunden, indem Aufrufstellen identifiziert werden, an denen
die Empfängeneferenz
ein Aufrufer-Argument ist, an das der Aufrufende keine Zuweisung
vornimmt. Wenn Parameterwerte nach dem Wert übergeben werden, dann ist die
Empfängeneferenz
notwendigerweise eine lokale Variable, an die nur der Aufrufer eine
Zuweisung vornehmen kann, so dass das Objekt, auf das die Variable
Bezug nimmt, vorher existierend sein muss, wenn der Aufrufer nicht
selbst eine Zuweisung daran vornimmt.
-
In einem Aspekt der Erfindung stellt
die vorliegende Erfindung ein Computersystem nach Anspruch 1 bereit.
In einem anderen Aspekt stellt die vorliegende Erfindung ein Verfahren
nach Anspruch 9 bereit.
-
KURZBESCHREIBUNG
DER ZEICHNUNGEN
-
Die nachstehende Erfindungsbeschreibung bezieht
sich auf die beiliegenden Zeichnungen. In den Zeichnungen zeigen:
-
1,
die voranstehend diskutiert wurde, ein Blockdiagramm eines typischen
Computersystems des Typs, der konfiguriert werden kann, um ein Quellprogramm
(Source-Programm) zu kompilieren;
-
2,
die voranstehend diskutiert wurde, ein Blockdiagramm des Übergangs
von einem Quellcode (Source Code) zu der Ausführung der Anwendung;
-
3,
die voranstehend diskutiert wurde, eine Quellcode-Auflistung, die
typische Klassendeklarationen aufführt;
-
4,
die voranstehend diskutiert wurde, eine Quellcode-Auflistung, die
die Verwendung eines Objekts darstellt, das ein Exemplar einer in 3 deklarierten Klasse ist;
-
5 ein
Flussdiagramm des Entscheidungsprozesses, der von der illustrierten
Ausführungsform
der Erfindung verwendet wird, um zu bestimmen, ob ein virtueller
Anruf direkt inlined werden soll;
-
6 ein
Quellcode-Fragment, welches verschiedene Aufrufstellen darstellt,
die Kandidaten für ein
Inlining sein können;
-
7 ein
Flussdiagramm, das eine Auferlegung von verschiedenen Versionen
des Vorexistenzkriteriums der vorliegenden Erfindung darstellt;
-
8 ein
Codefragment, das die Klassendefinition einer Klasse darstellt,
die ein unveränderliches
Mitglied aufweist;
-
9 ein
Diagramm von verschiedenen Datenstrukturen, die die illustrierte
Ausführungsform verwendet,
um zu bestimmen, wann Aufrufprozeduren zu rekompilieren sind, die
unter den Annahmen kompiliert worden sind, dass bestimmte von ihren Aufgerufenen
nicht außer
Kraft gesetzt worden sind;
-
10 ein
ausführlicheres
Diagramm einer Abhängigkeitsstruktur,
die 9 einschließt; und
-
11 ein
Diagramm ähnlich
wie 9, wobei das Ergebnis
einer Ladung einer Klasse dargestellt ist, die ein Direkt-Inlined-Verfahren
eines Aufgerufenen außer
Kraft setzt.
-
AUSFÜHRLICHE
BESCHREIBUNG EINER ILLUSTRATIVEN AUSFÜHRUNGSFORM
-
In einer typischen Umgebung mit einer
dynamischen Kompilierung, bei der die Lehren der vorliegenden Erfindung
umgesetzt werden können,
wird nicht die gesamte Kompilierung fertiggestellt, bevor eine Ausführung eines
Teils des sich ergebenden Objektcodes beginnt. Zum Beispiel kann
der Kompilierungsprozess "faul" ("lazy") in dem Sinne sein,
dass er nur beispielsweise eine Hauptprozedur kompiliert, ohne zu
Anfang irgendwelche der Prozeduren zu kompilieren, die die Hauptprozedur
aufrufen kann. Anstelle der Aufrufe an diese Prozeduren kann er zum
Beispiel Rumpfeinheiten (Stubs) bereitstellen, die bestimmen, ob
der Quellcode der Prozedur des Aufgerufenen kompiliert oder interpretiert
werden sollte. Somit wird eine Prozedur eines Aufgerufenen nicht
kompiliert, außer
wenn die Ausführung
des Objektcodes einen derartigen Stub erreicht.
-
Nun sei die sich ergebende Kompilierung
in einer derartigen Umgebung betrachtet. Die Kompilierung schreitet
in der normalen Weise voran, wobei Quellcode-Anweisungen in Maschinencode-Anweisungen umgewandelt
werden, bis ein Punkt erreicht wird, an dem der Quellcode einen
Prozeduraufruf darstellt. Das gewöhnliche Ergebnis ist einfach
ein Objektcode zum Implementieren des Prozeduraufrufs, d. h. zum
Speichern des Kontext des Aufruferprozesses und zum Übergeben
von irgendwelchen Argumenten und einer Steuerung an den Aufrufer. Aber
Verfahren (Methods), die bestimmte Kriterien erfüllen, sind gute Kandidaten,
um anstelle davon inlined zu sein, so dass der Kompilierer derartige
Kandidaten in Übereinstimmung
mit einem Entscheidungsprozess, wie beispielsweise derjenige, den
das vereinfachte Flussdiagramm der 5 darstellt, identifiziert.
-
Der Entscheidungsblock 40 stellt
ein grundlegendes Inlining-Kriterium, nämlich die Größe, dar. Wenn
das Verfahren des Aufgerufenen lang ist, ist es nicht ein guter
Inlining-Kandidat. In den meisten Fällen wird der Kompilierer auch
von einer Betrachtung irgendein polymorphes Verfahren beseitigen,
z. B. ein virtuelles Verfahren, das auf einer Variablen aufgerufen
wird, deren statischer Typ einen Abkömmling aufweist, der dieses
Verfahren außer
Kraft setzt, so dass die bestimmte Form des Verfahrens, das ausgeführt werden
wird, bis zur Laufzeit nicht bekannt ist. Obwohl eine derartige
Beseitigung nicht eine absolute Anforderung für die breiteren Lehren der
vorliegenden Erfindung ist, ist sie ein Merkmal der meisten Implementierungen,
so dass 5 einen Block 44 einschließt, um ein
derartiges Kriterium darzustellen.
-
Obwohl nicht wesentlich, sind die
Kriterien der Blöcke 40 und 44 typisch,
und die meisten Implementierungen werden zusätzlich andere Schwellenkriterien
auferlegen, wie der Block 46 anzeigt. Der Block 48 zeigt
an, dass der Kompilierer zusätzlich
die Anforderung auferlegt, dass der Empfängerausdruck des Verfahrens
etwas erfüllen
muss, was als ein Vorexistenz-Kriterium bezeichnet wird. Wie ersichtlich werden
wird, ist eine Auferlegung dieses Kriteriums praktischer in einer
Umgebung mit einer dynamischen Kompilierung, um virtuelle Anrufe
direkt zu inlinen.
-
Der Zweck dieses Kriteriums kann
verstanden werden, indem eine Aufrufprozedur m 1 betrachtet wird,
die eine Verfahrensaktivierung o.m2 ( ) an irgendeiner gegebenen
Aufrufstelle enthält,
an der die Variable o deklariert ist, um eine Referenz auf ein Objekt
der Klasse 0 zu enthalten. Es wird angenommen, dass zu der Zeit
der Kompilierung der Aufruferprozedur ml kein Abkömmling der
Klasse 0, der ein Verfahren m2 der Klasse 0 außer Kraft setzt, geladen worden
ist, und, dass die Stelle andere Schwellenkriterien genauso erfüllt. In
einer Umgebung mit einer dynamischen Kompilierung kann dieses Kriterium
jedoch unter Umständen
später
verletzt werden, da ein Laden und ein Kompilieren gleichzeitig mit
der Ausführung
des sich ergebenden Codes voranschreiten. Um derartige Änderungen
zu erfassen und auf sie zu reagieren, müssten bei Abwesenheit der Lehren
der vorliegenden Erfindung Schritte vorgenommen werden, die einer
Attraktivität
eines direkten Inlinings entgegenwirken könnten.
-
Die vorliegende Erfindung hält die Aufgabe einer
Erfassung und Reaktion auf derartige Änderungen relativ einfach,
erlaubt aber, dass eine signifikante Anzahl von Stellen mit virtuellen
Anrufen direkt inlined werden. In Übereinstimmung mit der vorliegenden
Erfindung führt
der Kompilierer ein Inlining von virtuellen Aufrufen direkt nur
an Stellen durch, von denen der Kompilierer zur Kompilierungszeit
wissen kann, dass der Empfängerausdruck
während
irgendeiner Laufzeitausführung
des Aufrufverfahrens nicht notwendiger Weise auf ein Objekt Bezug
nehmen wird, dessen Klasse geladen wurde, als diese besondere Ausführung des
Aufrufers begann. Wenn dieses Kriterium erfüllt ist, dann wird der Objektcode für das Aufrufverfahren
ml keine speziellen Vorkehrungen benötigen, um sicherzustellen,
dass die Annahmen, auf denen das Inlining des virtuellen Aufrufs o.m2
( ) basiert war, gültig
bleiben. Wie gegenwärtig erläutert werden
wird, besteht die einzige Anforderung darin, die Annahmen zu überprüfen – und die Aufruferprozedur
wenn erforderlich, zu rekompilieren – bevor das erste Objekt irgendeiner
neu geladenen Klasse zugeordnet wird. Es wird nachstehend auch gezeigt
werden, dass die sich ergebende Rekompilierung niemals irgendeine
Ausführung
des Aufrufercodes, in den der Aufgerufene unter Annahmen inlined
wurde, deren Verletzung die Rekompilierung bedingt hat, unterbrechen
muss.
-
Es sei zunächst darauf hingewiesen, dass
jedes zugeordnete Objekt notwendigerweise eine Klasse aufweist,
die geladen ist. Somit reicht ein Nachweis, dass ein Objekt beim
Eintritt in ein Verfahren zugeordnet ist, aus, um zu zeigen, dass
es das Kriterium erfüllt,
das heißt,
dass seine Klasse beim Eintritt in das Verfahren geladen wurde.
Es ergibt sich, dass viele Stellen, die dieses "Zuordnungsfähigkeits" ("Allocatedness") Kriterium erfüllen, relativ
einfach identifiziert werden können. 6 verwendet ein Codefragment
in der Java-Programmiersprache, welches eine einfache Technik zur
Durchführung
darstellt, die hier als eine Analyse mit invariantem Argument (Invariant-Argument-Analyse)
bezeichnet wird. Es sei das Empfängerobjekt
betrachtet, auf das die Objektvariable o in dem Verfahrensaufruf
o.m2 ( ) Bezug nimmt. Eine Inspektion des Aufruferverfahrens ml
enthüllt,
dass dessen Körper
keine Zuweisung an eine Objektvariable o innerhalb des Aufruferverfahrens
enthält.
Nun bedeutet die Abwesenheit einer Anweisung an eine Variable innerhalb
einer Prozedur nicht alleine, dass das Objekt, auf das die Variable Bezug
nimmt, bereits zugeordnet worden sein muss, als die Prozedur begonnen
wurde; ein unterschiedlicher Ausführungszweig (Ausführungs-Thread)
oder eine andere Prozedur, die von der Aufrufenden-Prozedur aufgerufen
wird, könnte
einer nicht-lokalen Variablen eine Referenz auf ein neu zugeordnetes
Objekt zuweisen. Aber die Variable o des Empfängerobjekts, die ihren Wert übergeben
hat, als das Aufruferverfahren ml aufgerufen wurde, ist eine lokale
Variable, so dass das Objekt, auf das es Bezug nimmt, notwendigerweise
vor dem Eintritt der Aufrufenden-Prozedur zugeordnet sein muss,
wenn das Aufruferverfahren nicht selbst ihm einen Wert zuweist.
Dies bedeutet, dass eine Ausführung
des Aufrufers niemals für
eine Rekompilierung – und
eine Ersetzung der Werte, die mit ihm auf dem Aufruf-Stapel (call
stack) assoziiert sind – unterbrochen
werden muss, sogar wenn ein außer
Kraft setzendes Verfahren gleichzeitig mit der Ausführung des
Aufrufers kompiliert wird und somit die Annahme invalidiert (für ungültig erklärt), auf
die das Inlining gestützt
wurde.
-
Somit besteht eine Aufrufstelle die
Analyse mit invariantem Argument, wenn ihr Empfängerausdruck ein Aufruferargument
ist, dessen Wert beim Eintritt in den Aufrufer übergeben wird und der Aufrufer
danach ihm nicht einen neuen Wert zuweist. Da sämtliche Prozedurparameter nach
dem Wert in der Java-Programmiersprache übergeben werden, werden Empfänger, die
dieses Kriterium erfüllen,
leicht erkannt. In einigen anderen Sprachen, bei denen Parameter
nach der Referenz übergeben
werden, muss Vorsicht walten gelassen werden, um Nach-Referenz-Parameter
nicht als Invariante Argumente zu betrachten und als mögliche Zuweisungen
Exemplare (Instances) zu erkennen, in denen der Aufrufer den Empfänger (Receiver)
an einen Aufrufenden (Callee) nach der Referenz übergibt.
-
7 zeigt
den Schritt 48 der 5 mit
näheren
Einzellieiten und zeigt, dass auch andere alternative Vorexistenz-Kriterien
vorhanden sein können. Der
Block 50 stellt den eben erläuterten Test einer Identifizierung
von Stellen dar, an denen die Empfängervariable ein invariantes
Argument ist. Wie der Schritt 54 der 5 zeigt,
führt dieser
Test zu einem direkten Inlining, wenn der Aufrufer niemals einen Wert
der Empfängervariablen
zuweist. Sogar dann, wenn der Aufrufer eine derartige Zuweisung
nicht durchführt,
kann ein direktes Inlining noch geeignet sein, und einige Ausführungsformen
der Erfindung können
entsprechend einen Test wie denjenigen, den der Block 56 der 7 darstellt, einschließen. Die Stellen
der 6, die die Aufrufe
02.m2 ( ) und o3.m2 ( ) enthalten, sind Beispiele von Stellen (Sites), die
diesen Test bestehen.
-
Für
den Fall von o2.m2 ( ) zeigt die vorangehende Anweisung "O o2 = o", dass der Aufrufer
eine Zuweisung eines Werts an die Variable o2, die die Empfänger-Objekt-Referenz
enthält,
vornimmt, aber der dadurch zugewiesene Wert derjenige eines Invarianten
Argument ist. Die Vorexistenz des Objekts, auf das sich eine lokale
Variable bezieht, kann daraus abgeleitet werden, dass ihm nur der
Wert einer anderen derartigen Variablen zugewiesen ist. Somit muss
o2.m2 die gleiche Implementierung wie o.m2 sein. Es sei darauf hingewiesen,
dass eine lokale Variable dieses Kriterium rekursiv erfüllen kann:
Eine lokale Variable, der nur der Wert von o2 zugewiesen ist, würden den
Test ebenfalls bestehen.
-
Für
den Fall des Aufrufs o3.m2 ( ) macht die vorangehende Anweisung "O o3 = o.clone (
)" eine Zuweisung
an die Variable o3, die die Empfängerreferenz
enthält.
Der Wert ist nicht der gleiche wie derjenige der Variablen o, aber
er ist eine Referenz auf ein Objekt, welches notwendigerweise den
gleichen dynamischen Typ wie das Invariante Argument o aufweist.
Eine Implementierung o3.m2 muss deshalb die gleiche Implementierung
o.m2 sein und somit die gleiche wie die Inlined-Implementierung.
Deshalb erfüllt
die beteiligte Anruf- bzw. Aufrufstelle wieder das Vorexistenz-Kriterium
der vorliegenden Erfindung und der Aufgerufene an dieser Stelle
kann inlined werden.
-
Die Stelle der 6, die den Aufruf o.f.m3 ( ) enthält, kann
unter Umständen
einen anderen alternativen Vorexistenz-Test erfüllen, der als der Test mit unveränderlichem
Feld (immutable-field test) bezeichnet werden wird. Eine Aufrufstelle
besteht diesen Test, wenn der Empfängerausdruck auf ein Mitglied
eines vorher existierenden Objekts verweist, das in dem Sinn "unveränderlich" ist, dass es nur
von dem Konstruktur-Verfahren (Constructor Method) von der Klasse
dieses Objekts zugewiesen werden kann. Zum Beispiel sei angenommen,
dass die Klasse 0 ihr Mitglied f in der Weise definiert,
die 8 darstellt, nämlich als
ein "privates" Mitglied, dem kein anderes
Mitgliedsverfahren als der Klasse-Konstruktur einen Wert zuweist.
Da das "private" ("private") Schlüsselwort
der Java-Programmiersprache anzeigt, dass ein Zugriff auf ein Mitglied,
dessen Deklaration so modifiziert ist, auf Mitglieder der gleichen Klasse
beschränkt
ist, wird dem Mitglied f ein Wert nur auf die Konstruktion des Objekts
hin, von dem es ein Mitglied ist, nämlich dem Objekt, auf das die
Variable o Bezug nimmt, zugewiesen. Der Wert, der dem Mitglied o.
f. zugewiesen werden soll, wird an den Konstruktur des Objekts o
als ein Argument übergeben.
Somit muss das Mitgliedsobjekt, auf dass sich die Variable f bezieht,
notwendigerweise vor der Aktivierung des Konstruktors zugeordnet
worden sein. Wenn aber das Objekt, auf das die Variable o Bezug nimmt,
vorher existiert und vor der Konstruktion dieses Objekts dasjenige,
auf das o. f. Bezug nimmt, zugeordnet wurde, dann muss auch das
Objekt, auf das o. f. Bezug nimmt, vorher existierend sein. Das
gleiche würde
zutreffen, wenn das Mitglied f anstelle von "privat" als "abschließend" ("final") deklariert worden wäre, da dieses
Schlüsselwort
die Randbedingung auferlegt, dass der so modifizierte Wert einer
Variablen nicht geändert
werden kann, nachdem er zu Anfang zugewiesen wird. In beiden Fällen erfüllt die
Anrufstelle, an der der Empfängerausdruck
o. f. ist, ein Vorexistenz-Kriterium.
-
In jedem der Inlining-Fälle, die
eben erwähnt wurden,
besteht keine Notwendigkeit, in dem Objektcode der Aufrufenden-Prozedur
irgendwelche Vorkehrungen zum Überarbeiten
des Inlined-Teils, wenn die Annahmen, auf denen das Inlining gestützt wurde,
verletzt werden, einzubauen. Derartige Vorkehrungen können anstelle
davon in zweckdienlicher Weise vollständig in den Kompilierungs-Betriebsvorgängen implementiert
werden, die die Verletzung der Annahmen verursachen, wie nun unter
Bezugnahme auf 9 erläutert werden
wird.
-
Der Objektcode, der sich aus einer
Kompilierung der Aufruferprozedur ml ergibt, wird in einen Block 70 mit
kompiliertem Code gelegt, in dem ihm möglicherweise ein Header-Teil
(Anfangsblock-Teil) 72 vorausgeht, der eine Housekeeping-Information (Aufräum-Information)
enthält.
In einer Umgebung mit einer dynamischen Kompilierung kann der kompilierte
Code vorläufig
sein: Seine fortgesetzte Gültigkeit
kann davon abhängen,
welcher Code danach geladen wird. Wenn die Prozedur das virtuelle
Verfahren m2 in Übereinstimmung
mit den voranstehend beschriebenen Kriterien direkt inlined hat,
hängt die fortgesetzte
Gültigkeit
des Objektcodes im Block 70 insbesondere davon ab, ob die
Anzahl der Implementierungen des Aufgerufenen-Verfahrens m2 die
gleiche ist wie sie war, als die Aufrufer-Prozedur ml kompiliert
wurde. In der dargestellten Ausfuhrungsform, die nur "monomorphe" Verfahren direkt
inlined, hängt die
Gültigkeit
des Inlined-Codes von der Annahme ab, dass nur eine derartige Implementierung
vorhanden ist.
-
Um einen Mechanismus zur Reaktion
bereitzustellen, wenn diese Annahme verletzt wird, schafft der Kompilierungsbetrieb
eine "Abhängigkeitsstruktur", wobei 10 ein typisches Format
davon zeigt. Eine fortgesetzte Gültigkeit
eines Objektcodeblocks kann von verschiedenen Typen von Annahmen
abhängen,
die eine Abhängigkeitsstruktur
dokumentieren könnten,
so dass das erste Feld der Struktur den Einzelimplementierungs-Typ
der Abhängigkeit
spezifiziert, was in diesem Fall bedeutet, dass die Gültigkeit
der Prozedur ml von der Annahme abhängt, dass das Aufgerufenen-Verfahren
m2 nicht außer
Kraft gesetzt worden ist. Das nächste "Ziel"("target")-Feld der Struktur identifiziert, dass
angenommen wird, dass der Aufgerufene nur einzeln implementiert
ist, während
ihr letztes "abhängiges" ("dependent") Feld die Aufrufprozedur
identifiziert, deren Gültigkeit
von dieser Annahme abhängt.
In der dargestellten Ausführungsform
nehmen diese Identifikationen die Form von Zeigern auf den "Verfahrensblock" 74 ( 9) in Assoziation mit der
Aufgerufenen-Prozedur m2 und mit dem Behälter 70 mit kompiliertem
Code im Zusammenhang mit der Aufgerufenen-Prozedur ml an. Der Kompilierer
verwendet einen Verfahrensblock, um verschiedene Information zu
speichern, die sämtlichen
Implementierungen ihres assoziierten Verfahrens gemeinsam ist.
-
Als Beispiel zeigt 9 den Verfahrensblock 74 des
Verfahrens m2, so dass er getrennt von dem Behälter mit kompiliertem Code
des Aufgerufenen-Verfahrens sowie von einer Klassendatei 76,
die verschiedene Information über
die Klasse 0, den statischen Typ des Empfängerausdrucks
des Verfahrens m2 an der Aufrufstelle von Interesse, enthält. Die
Klassendatei enthält
typischer Weise viele Arten von Klassen-bezogener Information, die
die Zeichnung nicht darstellt, wie den Quellcode der Mitgliedsverfahren
in der Form eines Java Virtual Machine Byte-Codes. Als weiteres
Beispiel ist die Klassendatei so dargestellt, als ob sie beim Laden
mit einem getrennten Verfahren und Virtualfunktionstabellen 78 und 80 versehen
worden ist, die jeweils auf die Verfahrensblöcke und Behälter mit kompiliertem Code für die verschiedenen
Mitgliedsverfahren der Klasse 0 zeigen.
-
Die verbleibenden Felder, die 10 zeigt, unterstützen die
Implementierung der Abhängigkeitsstruktur
der dargestellten Ausführungsform
als ein Element von zwei doppelt verbundenen Listen. Wenn die Abhängigkeitsstruktur 84 geschaffen
wird, um die Abhängigkeit
des Aufrufers ml davon, dass der Aufgerufene m2 nur einzeln implementiert
ist, zu dokumentieren, wird sie in einer verbundenen Liste 86 von Strukturen
angeordnet, die andere Abhängigkeiten darstellen.
Insbesondere dann, wenn irgendwelche Abhängigkeitsstrukturen geschaffen
worden sind, um andere Annahmen zu dokumentieren, auf denen die Gültigkeit
des Aufrufer-Objektcodes gestützt
ist, spezifiziert ein Zeiger in dem Header des Behälters mit kompiliertem
Code des Aufrufers m 1 den Ort der ersten Struktur in einer verbundenen
Liste von derartigen Strukturen, wie der Pfeil 88 andeutet,
und die neue Abhängigkeitsstruktur
wird zu dem Kopf der Liste hinzugefügt, indem ihr "nächste abhängige Abhängigkeit"-Feld mit dem Ort der ersteren ersten
Struktur gefüllt
wird, ihr "frühere abhängige Abhängigkeit"-Feld mit dem Ort
der neuen Struktur gefüllt
wird, und ihr Listenkopfzeiger umgeleitet wird, wie der Pfeil 90 andeutet.
-
Nachdem die neue Struktur in einer
Liste von Strukturen installiert worden ist, die Annahmen dokumentieren,
auf denen die Gültigkeit
des Aufrufer-Objektcodes gestützt
ist, wird sie auch in einer Liste 94 von Strukturen installiert,
die Abhängigkeiten
von einem oder einem anderen der Merkmale des Aufgerufenen dokumentieren.
Insbesondere findet sie in dem "Abhängige" ("dependents")-Feld des Verfahrensblocks
des Aufgerufenen-Verfahrens m2 einen Zeiger auf die Struktur an
dem Kopf dieser Liste, installiert die neue Struktur, indem geeignete
Einträge
in den Feldern "nächste Zielabhängigkeit" und "frühere Zielabhängigkeit" von diesen Strukturen
gemacht werden und der Listenkopfzeiger umgeleitet bzw. umgerichtet
wird.
-
Wenn eine danach geladene Klasse
A ein Abkömmling
der Klasse 0 ist, in der das Direkt-Inlined-Aufgerufenen-Verfahren
m2 definiert ist, wird dieser Abkömmling per Definition irgendwelche
nichtprivate Verfahren der Klasse 0 erben, so dass seine Verfahrenstabelle
irgendeinen Eintrag empfangen wird, der auf den Verfahrensblock 74 mit
gemeinsamem Merkmal dieses Verfahrens verweist, und dieser Block
wird aktualisiert werden. Wenn die Klasse A das Verfahren m2 jedoch
nicht außer
Kraft setzt, wird es keine Änderung
in dem "Implementierungs-Feld" des Verfahrens,
das anzeigt, wie viele verschiedene m2-Implementierungen kompiliert worden
sind, geben. Vorausgesetzt, dass dieses Feld anzeigt, dass das Verfahren
m2 nur eine Implementierung aufweist, kann das Objektcode-Programm
die Nutzen von dem direkten Inlining dieses Verfahrens fortgesetzt
nutzen.
-
Nun sei betrachtet, was passiert,
wenn die Verfahren (Methods), die gerade kompiliert werden, anstelle
davon diejenigen einer Abkömmlings-Klasse B
sind, die ein m2 außer
Kraft setzt. Das Ergebnis ist eine andere Implementierung von diesem
Verfahren, wie der Behälter 96 mit
kompiliertem Code der 11 anzeigt.
Somit wird das Implementierungsfeld des Verfahrensblocks 74 des
Verfahrens m2 aktualisiert, um anzuzeigen, dass nun zwei Abhängigkeiten vorhanden
sind, und die Abhängigkeitsliste,
auf die das "Abhängigkeits"-Feld des Verfahrensblocks
des Verfahrens m2 verweist, wird nach Einzelimplementierungs-Abhängigkeiten
durchsucht.
-
Diese Suche identifiziert die Abhängigkeitsstruktur 84,
die anzeigt, dass die Kompilierung der Prozedur ml darauf gestützt war,
dass das Verfahren m2 einzeln implementiert ist. Die Prozedur ml
wird entsprechend rekompiliert, ohne das direkte Inlining, wie der
Block der 11 mit dem
Einbau eines virtuellen Aufrufs in einem neuen Behälter für den kompilierten
Code für
das Verfahren m2 anzeigt. Sämtliche Bezugnahmen
auf das Verfahren ml, wie die zugehörigen Einträge in der virtuellen Tabelle
seiner Klasse und seiner Vorfahrens-Klasse, werden so aktualisiert,
dass irgendein Code, der die Prozedur ml aufruft, an den neuen Behälter 96 mit
kompiliertem Code anstelle an den Behälter 70 für den vorangehenden Code,
dessen Code nicht mehr gültig
ist, gerichtet werden wird. Da die Gültigkeit des neuen Codes nicht von
der Annahme abhängt,
dass O.m2 außer
Kraft gesetzt worden ist, wird die Abhängigkeitsstruktur 84 von
beiden Listen entfernt. Die Kompilierung der Klasse B kann nun abgeschlossen
werden und irgendeine Prozedur, die darauf wartete, diese Klasse als
Exemplar zu etablieren, kann dies tun.
-
Um die Einfachheit zu würdigen,
die diese Erfindung anbietet, sei die Situation betrachtet, bei der
ein Ausführungs-Thread
mitten bei der Ausführung
der Prozedur ml des Aufgerufenen ist, wenn die Klasse B kompiliert
wird. In einem derartigen Fall ist eine zweite Implementierung des
Verfahrens m2 während
der Ausführung
einer Prozedur, die unter der Annahme kompiliert wurde, dass das
Verfahren m2 nur eine Implementierung aufweist, in Existenz gekommen.
Wenn ein direktes Inlining an Stellen ausgeführt worden wäre, die
nicht die Kriterien der vorliegenden Erfindung erfüllt haben,
dann hätte
die Prozedur ml unter Umständen
das Inlining mit einem Code zum Überprüfen der
Anzahl von m2-Implementierungen schützen müssen, und Einträge in dem Stapel-Rahmen
(stack frame) des Verfahrens m2 müssten ersetzt werden. Aber
keine derartige On-Stack-Ersetzung (Ersetzung auf dem Stapel) ist notwendig,
wenn die Kriterien der vorliegenden Erfindung beachtet werden, weil
kein Exemplar der Klasse B vor einem Eintritt in die Prozedur ml,
wie durch das direkte Inlining kompiliert, zugeordnet gewesen sein
kann, und das Verfahren m2 an der Inlining-Stelle auf einem Objekt,
das noch nicht zugeordnet worden ist, als diese Ausführung der
Prozedur ml begann, nicht aufgerufen werden kann.
-
Somit vereinfacht die vorliegende
Erfindung ein Inlining in dynamischen Kompilierungs-Umgebungen und bildet
somit einen signifikanten Fortschritt in dem technischen Gebiet.
-
Ausführungsformen der Erfindung
sind auf einer programmierbaren Verarbeitungsvorrichtung wie einem
Computer oder einem Prozessor implementiert worden. Verfahren (Methods)
in Übereinstimmung
mit den Ausführungsformen
der Erfindung können
als ein von einem Computer lesbarer Code, der auf einem Trägermedium
getragen wird, das ein Signal oder ein Speichermedium sein kann,
verkörpert
werden.