Die Register Stack Engine (RSE) ist ein programmiertechnischer Mechanismus zur effizienten Handhabung des Stapelspeichers (Stack) bei der IA-64, der Intel-Architektur für 64-Bit-Prozessoren.
Die Parameter einer Funktion werden in Registern übergeben, von denen ein Teil wie ein Stack arbeitet. Der logische Stack setzt sich aus den Registern und einem Teil des Hauptspeichers zusammen, sodass er beliebig groß werden kann (so groß wie der Hauptspeicher) und gerade aktuelle Stackframes schnell für Berechnungen in der ALU verfügbar sind (so schnell wie die Register). Die RSE ist ein Hardwaremechanismus, der durch einige Maschinenbefehle kontrolliert wird und abhängig vom Betriebsmodus sowie der Implementierung des Prozessors unterschiedlich stark selbstständig arbeitet, d.h. die Register mit dem Speicher mit freier Bandbreite unabhängig vom ausgeführten Befehlsstrom, quasi im Hintergrund, synchronisiert.
Inhaltsverzeichnis |
Die Intel-64-Bit-Architektur definiert 128 frei verwendbare Register (16 mal mehr als bei der IA-32), von denen die Register 0 bis 31 wie normale statische Register funktionieren (acht bei der IA-32). Die Register 32 bis 127 unterstützen spezielle Mechanismen, die beim Aufruf von Unterprogrammen und Funktionen das sonst notwendige langwierige Sichern- und Rücksichern von Registerinhalten auf dem Stack (im Hauptspeicher) reduzieren oder ganz vermeiden.
Bei einem Funktionsaufruf werden die Funktionsargumente, Rücksprungadresse und Rückgabewerte grundsätzlich nicht auf dem Stack übergeben sondern in Registern. Die Register 32 bis 127 dienen dabei selbst als Stack, der Register-Stack-Pointer zeigt auf das Register, das als nächstes für die Übergabe von Funktionsparametern zur Verfügung steht. Sind keine Register mehr verfügbar, werden welche aus einem früheren Stackrahmen in den Hauptspeicher (meistens nur in den Prozessorcache) geschrieben.
Bei jedem Funktionsaufruf werden die Register rotiert, sodass die Funktionsparameter immer beginnend bei Register 32 zu finden sind. Nach diesen folgen die lokalen Daten der Funktion und dann die Register, die für die Weitergabe an untergeordnete Funktionen bestimmt sind. Man spricht von Input-, Local- und Output-Registern, die über Kürzel (in
, loc
und out
) bequem angesprochen werden können. Die Länge dieser drei Bereiche wird während der Kompilierung festgelegt.
Technisch gesehen werden die Register bei der Rotation nicht kopiert sondern lediglich umbenannt (Register Mapping). Dazu wird ein Zeiger angepasst, der auf das jeweils gültige Register 32 zeigt.
Die folgende Darstellung dient der Erklärung der Funktionsweise und kann vom Programm nicht beeinflusst werden.
Die 96 dynamischen Register sind in 4 Partitionen geteilt:
die ersten zwei Partitionen enthalten Register, die für Stackrahmen von übergeordneten Funktionen verwendet werden.
Da das aktuelle input Register 0 immer die Nummer 32 hat, sieht das Registerfile ca. so aus:
32 (logisch) -> | current (input|local|output) | invalid | clean | dirty| <- 127
Ist der alte Stackrahmen z.B. 32 - 36, dann muss der Zeiger auf den Beginn der 'current' - Partition 4 Register nach rechts verschoben werden, da der alte Stackrahmen aber nicht an den Registerpositionen 28 - 32 gespeichert wird, ist er dann an den Positionen 123 - 127 zu finden, wie bei einem Ringpuffer.
Tatsächlich kann sich das aktuelle Register 32 aber ein beliebiges Register sein, das heißt tatsächlich kann die obige Darstellung so gespeichert sein:
32 ("physikalisch") -> | clean | dirty | current | invalid | <- 127
Wenn die invalid - Partition für den nächsten Funktionsaufruf zu klein ist, werden Register aus der clean zur invalid hinzugefügt. Wenn invalid und clean zu klein sind, müssen Register aus der dirty synchronisiert werden, bevor der Funktionsaufruf erfolgen kann. Bei der automatischen Synchronisation wird wenn möglich die dirty - zugusten der clean - Partition vergkleinert, d.h. Register die die Daten früherer Stackrahmen enthalten werden mit dem Hauptspeicher synchronisiert, denn clean - Register können sofort neu verwendet werden. Oder die invalid - Partition wird zugunsten der clean - Partition verkleinert, d.h. Daten aus früheren Stackrahmen werden aus dem Hauptspeicher geladen, um bei der Rückkehr in eine übergeordnete Funktion wieder verfügbar zu sein. Kurz gesagt, die clean - Partition sollte besonders groß sein.
Bei einem task switch müssen daher auch nicht 127 Register gesichert werden, sondern nur die Register der dirty - Partition sowie die statischen Register 0 bis 32.
Die Synchronisation mit dem Hauptspeicher ist nur nötig, wenn keine Register mehr verfügbar sind. Das wäre zum Beispiel der Fall bei einer Aufruftiefe von 96 Funktionen, die keine Parameter übernehmen (pro Funktionsaufruf muss nur die Rücksprungaddresse gesichert werden). Optional kann die Synchronisation mit dem Hauptspeicher auch im Hintergrund geschehen. Falls die Load/Store-Einheit nicht beschäftigt ist, kann sie spekulativ entweder die Anzahl der verfügbaren Register erhöhen, indem sie Register von früheren Aufrufen in den Hauptspeicher schreibt, oder Register früherer Stackrahmen aus dem Hauptspeicher auslesen, damit sie bei der Rückkehr aus Funktionen schnell verfügbar sind. Falls die Synchronisation spekulativ erfolgt, spricht man vom eager mode, sonst vom lazy mode. Die spekulative Synchronisation kann ein- und ausgeschaltet werden, sofern sie implementiert ist.
Die Register Stack Engine ist eine Generalisierung der Register Windows, wie sie bei SPARC-Prozessoren auftauchen. Dort ist die Größe der Register-Fenster immer gleich, während sie bei der Register Stack Engine beliebig festgelegt werden kann.
Quelle: Intel Itanium Architecture Software Developer's Manual Volume 2 Kapitel 6