Superskalare Prozessorarchitekturen

14.09.2004 von Christian Siemers
Superskalare Prozessoren wie der Pentium 4 und der Athlon durchbrechen die magische Grenze und verarbeiten mehr als einen Befehl pro Takt. Dies erfordert gravierende Änderungen im Design gegenüber einer RISC-CPU.

Die Befehlssequenzialität, die für von-Neumann-ähnliche Pipeline-Architekturen als letzte Forderung übrig bleibt, scheint die maximale Performance auf 1 Instruktion/Takt festzulegen. Innerhalb der RISC-CPU war eine Beschleunigung auf dieses Maß ein großer Erfolg und nur über zusätzliche Maßnahmen wie eine Forwarding Unit erreichbar. In diesem Artikel wird auf der Ebene der Single-CPU aber noch mehr verlangt: Der Prozessor soll die "Schallgrenze" von 1 Instruktion/Takt durchbrechen. Dies erweist sich als mehr als eine pure Erweiterung der RISC-Philosophie. Die folgenden Maßnahmen erreichen erst in ihrer Kombination dieses Ziel:

Diese Maßnahmen werden im Folgenden beschrieben. Diesen Artikel und eine ganze Reihe weiterer Grundlagenthemen zu Prozessoren finden Sie auch in unserem tecCHANNEL-Compact "Prozessor-Technologie". Die Ausgabe können Sie in unserem Online-Shop versandkostenfrei bestellen. Ausführliche Infos zum Inhalt des tecCHANNEL-Compact "Prozessor-Technologie" finden Sie hier.

Die Beschreibung des Ziels

Die Entwicklung von superskalaren Mikroprozessoren erfolgte nicht losgelöst von den bisherigen Prozessoren. Es sind zumeist (mit Ausnahmen) Prozessoren, die das obere Ende der Rechenleistung innerhalb einer Prozessorfamilie darstellen. Innerhalb dieser Prozessorfamilie ist eine für alle Mitglieder kompatible binäre Maschinensprache oder ein Maschinencode definiert. Diese Maschinensprache soll auch für einen superskalaren Prozessor unverändert gelten.

In sequenziellen Ausführungsmodellen (bis hin zum RISC-Prozessor) definiert die Maschinensprache, wie ein Prozessor einen Befehl ausführt. Dies muss für superskalare Rechner abgeschwächt werden, wo die binäre Sprache lediglich definiert, was ausgeführt wird. Dieser geringe Unterschied ist durchaus wichtig, da bei paralleler Ausführung kein exakter Determinismus der Ausführungsreihenfolge in der CPU mehr möglich ist.

Es bleibt jedoch bei der Aussage, dass sich die CPU stets in einem präzisen Zustand befindet. Dies bedeutet speziell, dass bei Unterbrechungen etwa durch Interrupts oder Ausnahmen (Exceptions) der Zustand der CPU sicherbar und wiederherstellbar ist, falls der Prozessor die Ausnahmen behandelt. Dieses Konzept ist eng mit dem Modell der sequenziellen Ausführung verknüpft und bedeutet sowohl für RISC-CPUs wie für superskalare Prozessoren entsprechende Hardware, um dies zu gewährleisten.

Das Ziel der superskalaren Rechnerarchitektur ist es, durch parallele Ausführung von Instruktionen die durchschnittliche Bearbeitungszeit pro Instruktion zu senken. Dabei soll die Instruktionslatenzzeit, also die Zeit, die maximal pro Befehl verstreicht, gegenüber der RISC-CPU nicht ansteigen.

Anforderungen an das Design

Dies beinhaltet folgende Einzelheiten für das Design einer superskalaren CPU:

Zusammenfassend kann man sagen, dass ein superskalarer Prozessor zwar die Instruktionssequenzialität nicht mehr einhalten kann, jedoch immer noch die Ergebnissequenzialität bietet (und bieten muss).

Programmdarstellung, Abhängigkeiten und parallele Ausführung

Die Programmierung eines Rechners kann man typischerweise in die Erstellung des Sourcecodes in einer Hochsprache, die Übersetzung in Assembler und Binärcode sowie die weiteren Vorgänge unterteilen. Der Linker-Lauf ist in diesem Zusammenhang nicht von Interesse, da er im Wesentlichen nur Adressen setzt. Das Bild zeigt eine kurze C-Routine aus einem Sortierprogramm sowie die Übersetzung in einen nicht optimierten Assembler-Code.

Diese Übersetzung zeigt sehr deutlich die Kontrollflussabhängigkeit dieser Architekturen: Der Prozessor erreicht jeden Befehl, indem er den vorhergehenden ausführt (PC Increment) oder indem ein Sprungbefehl auf die Codespeicherstelle zeigt (PC Update). Beide Kontrollflussabhängigkeiten muss die CPU bei der parallelen Ausführung von Instruktionen berücksichtigen.

Zunächst ist das Assembler-Programm im Bildteil (b) in drei Basisblöcke aufgeteilt:

Dabei gilt: Ein Basisblock ist die maximale Anzahl von Instruktionen (um eine gewählte Stelle) mit exakt einem Eintritts- und einem Austrittspunkt.

Ein Basisblock entspricht damit der um den gewählten Punkt maximalen Folge von Instruktionen ohne Verzweigungsbefehl (Ausnahme: letzter Befehl) und ohne Sprungmarke (Ausnahme: am ersten Befehl). Dieser Block wird immer vollständig durchlaufen, falls das Programm seinen Anfang erreicht. Somit kann die CPU einen Basisblock prinzipiell in paralleler Weise ausführen. Der Basisblock ist damit die Größe im Befehlsfluss, auf die sich Compiler und superskalare Architekturen konzentrieren.

Weitere Parallelität entsteht durch die Zusammenfassung mehrerer Basisblöcke, etwa durch die spekulative Ausführung von Branch-Befehlen. Trifft die Ausführungsannahme nicht zu, muss der Prozessor alle Ergebnisse und die InstruktionsPipeline entsprechend korrigieren. Zwischen der superskalaren Rechnerarchitektur und dem Compiler-Bau (insbesondere Codegenerator und Optimierer) besteht daher eine erhebliche Wechselwirkung. Kapitel 6 erläutert diese Abhängigkeiten näher. Innerhalb eines Basisblocks existieren jedoch auch Datenabhängigkeiten, die wie im Fall der RISC-CPU weiter zu untersuchen sind. Diese Datenabhängigkeiten bestehen zwischen verschiedenen Instruktionen, falls diese auf die gleichen Speicherstellen beziehungsweise Register lesend oder schreibend zugreifen.

WAR- uns WAW-Hazards

Im Gegensatz zu den RISC-CPUs, die lediglich den Read-after-Write-(RAW) Hazard kennen (siehe Grundlagen der RISC-Architektur), können bei superskalaren CPUs auf Grund der parallelen Ausführung von Instruktionen auch Write-after-Read-(WAR) sowie Write-after-Write-(WAW) Hazards auftreten. Diese Abhängigkeiten stellt die folgende Abbildung dar, die sich auf den ersten Basisblock des Beispiels bezieht:

WAW-Hazards treten auf, da die Reihenfolge des schreibenden Zugriffs auf ein Register nicht mehr durch die Sequenz der Befehle gegeben ist. Die Instruktionen führt die CPU ja nun parallel zueinander aus. Der korrekte, letzte Schreibzugriff muss daher detektierbar sein. Dadurch kann entweder eine Zerstückelung der parallelen Ausführung erfolgen oder durch spezielle Zusätze eine Entscheidung zum korrekten Wert nachträglich getroffen werden. Während die erste Lösung unerwünscht ist, impliziert Letzteres zusätzliche Register zur Aufnahme aller Zwischenwerte sowie eine Entscheidungseinheit.

Gründe für WAW-Hazards sind:

Die andere Sorte der neuen Abhängigkeiten sind WAR-Hazards. Sie treten auf, da die CPU unbedingt gewährleisten muss, dass sie zur Berechnung die gültigen, in diesem Fall älteren Werte in einem Register oder einer Speicherstelle nutzt. Die parallele Ausführung garantiert dies keineswegs mehr, da eine Zugriffsreihenfolge nicht mehr bestimmbar ist. WAR- und WAW-Abhängigkeiten werden als virtuell oder künstlich bezeichnet, da sie geeignete Maßnahmen wirkungslos machen. RAW hingegen bleibt eine reale, unauflösbare Datenabhängigkeit.

Die Mikroarchitektur einer typischen superskalaren CPU

Das folgende Bild zeigt den vereinfachten Ablauf eines Programms. Die Instruction Fetches einschließlich der Sprungvorhersage (Branch Prediction) laden das statische Programm. Das Ausführungsfenster bearbeitet dann die Befehle mit weitest gehender Parallelität. Am Schluss erfolgt ein Reorder & Commit, der die Ergebnisse in die gültige Reihenfolge sortiert. Insbesondere die spekulative Ausführung kann dabei Probleme erzeugen, da sie eventuell Befehle bearbeitet, die dem Programmfluss entsprechend nicht zur Ausführung gekommen wären. Es ist eine Aufgabe der Commit-Einheit, diese Ergebnisse dann zu verwerfen.

Bild 5.3:

Die Mikroarchitektur einer superskalaren CPU ist in ihrem typischen Aufbau in folgendem Bild dargestellt. Diese Darstellung verzichtet bewusst auf den üblicherweise integrierten Teil für Floating-Point-Zahlen. Der Organisation des superskalaren Prozessors unterliegt weiterhin eine Pipeline-Struktur entsprechend den Darstellungen zur RISC-CPU. Die Pipeline in den superskalaren Rechnern ist hierbei meist mit einer größeren Anzahl von Stufen versehen, wie bereits aus obiger Zeichnung hervorgeht. Die Funktionsblöcke haben folgende Aufgaben:

Instruction Fetch und Predecode

Die Fetch-Phase des Prozessors umfasst mehrere Teilaktionen, an denen die Blöcke Predecode, Instruction Cache und Instruction Buffer beteiligt sind. Der Instruction Cache entspricht im Wesentlichen der gewohnten Funktionalität. Die maximal erreichbare Performance eines superskalaren Rechners macht es unabdingbar, einen Instruktions-Cache zu nutzen. Die minimale Fetch-Rate muss mindestens gleich der maximalen Performance (in Instruktionen pro Takt) sein. Reserven sind zum Beispiel für Cache Misses notwendig. Sollte sich innerhalb einer Cache Line eine Branch-Instruktion befinden, so können je nach Strategie auch Zugriffe auf die Adressen des alternativen Astes erforderlich sein.

Die vorgeschaltete Predecode Unit erzeugt die notwendigen Steuerinformationen. Diese ermöglichen unter Umständen eine schnellere Auswertung in nachgeschalteten Stufen und werden innerhalb des Cache gespeichert. Der Instruktions-Buffer kann eventuell entfallen, dient jedoch als einheitlicher, von Cache Misses unabhängiger Pufferspeicher zum inneren Teil der CPU. Dieser Instruktions-Buffer enthält die Instruktionen, deren Fetch-Phase bereits abgeschlossen ist.

Exakt betrachtet stellt die Phase des Instruction Fetch einen Instruction Block Fetch dar, weil mehrere Instruktionen anschließend zur parallelen Ausführung kommen sollen. Die Strategie zum Laden eines kompletten Blocks erstreckt sich mindestens auf einen Basisblock, den kein Verzweigungsbefehl unterbricht. Der Program Counter erhöht sich darin bei jeder Instruktion. Unbedingte Sprünge können in einem Block ebenso auftreten. Allerdings führen sie vermehrt zu Cache Misses. Konditionierte Sprünge erfordern eine gesonderte Behandlung, da sie zu Verzögerungen in der Bearbeitung führen können. Die Gesamt-Performance eines superskalaren Rechners hängt entscheidend von dem Aufwand zur Vorhersage der Spungrichtung ab. Folgende Teile gehören zur Behandlung der Branches:

Eine passende Kodierung innerhalb der Maschinensprache erleichtert die Erkennung eines konditionierten Sprungbefehls erheblich. Die Predecode Unit und zwischengespeicherte Informationsbits tragen ihr Übriges dazu bei. Sie ist allerdings besonders dann nützlich, wenn der Maschinencode aus Kompatibilitätsgründen nicht "selbstdekodierend" angelegt ist.

Probleme bei Sprüngen

Die Bestimmung der Sprungrichtung ist meist nicht unmittelbar möglich, da sie sich in der Regel erst aus den Berechnungen des vorhergehenden Instruktionsblocks ergibt. Diese Ergebnisse sind zum Zeitpunkt des Fetch aber noch gar nicht bekannt. Aus diesem Grund kommt eine Sprungrichtungsvorhersage (Sprung oder kein Sprung) mit diversen Verfahren zum Einsatz. Das einfachste Verfahren besteht in der statischen Vorhersage der Sprungrichtung durch Compiler-Informationen (kodiert) oder durch Erfahrungswerte (etwa: Rücksprünge erfolgen immer, da sie meist innerhalb von Schleifen auftreten).

Die andere grundsätzliche Methode verwendet dynamische Informationen, die während des Programmlaufs entstehen und in Branch Prediction Tables gehalten werden. Eine typische Implementierung besteht in Zählern (saturierend, ohne Überlauf!), die die genommenen Richtungen speichern. Sie sagen die Sprungrichtung je nach Zählerstand vorher. Simulationen bestätigen, dass dieser Ansatz selbst bei geringen Zählertiefen (zwei Bit) effektiv arbeitet. Allerdings benötigt ein Prozessor etliche Zähler, um die einzelnen Verzweigungen voneinander unterscheiden zu können (typisch: 1024 Zähler je zwei Bit).

Aktuelle Mikroprozessoren fassen solche Zähler-Arrays zu korrelationsbasierten Vorhersageautomaten zusammen, um auch die Vorgeschichte von Verzweigungen und Abhängigkeiten zu anderen Programmteilen erfassen zu können. Der Aufwand hierfür ist gerechtfertigt, da falsche Vorhersagen einen erheblichen Durchsatzverlust verursachen.

Der Prozessor muss dabei alle nach der Verzweigung ausgeführten Instruktionen rückgängig machen und einen neuen Instruktionsteilblock einladen. Die Berechnung des Sprungziels erfolgt in dem gewohnten Umfang. Branch-Befehle enthalten die Sprungzielangabe zumeist als PC-relative Information, so dass eine einfache Integeraddition wie im Fall der RISC-CPU das neue Sprungziel liefert.

Die weitere Ausführung der Instruktionen bietet auch im Fall eines Sprungs Optimierungspotenzial. Bei einem RISC-Prozessor führte ein genommener Branch zu einer Verzögerung, da die Phase des Instruction Fetch für die nächste Instruktion bereits abgelaufen war und das Ergebnis ungültig wurde. Für superskalare Prozessoren bietet sich hier eine stärkere Entkopplung zwischen Ausführung und Fetch von Instruktionen an.

Gewöhnlicherweise wird das Update des Program Counter durch die Zwischenspeicherung im Instruction Buffer aufgefangen. Alternative Techniken verfolgen sogar beide Kontrollflusspfade, um zumindest bei einfachen Verzweigungen immer den garantiert richtigen Pfad auszuführen.

Instruction Decoding, Renaming und Dispatch

Während der nächsten Phase lädt der Prozessor die Instruktionen aus dem Instruction Buffer und dekodiert sie. Dann prüft er sie auf Datenabhängigkeiten und ordnet sie den zur Verfügung stehenden Hardware-Einheiten zu. Dies beinhaltet demnach mehrere Teilphasen, die im Folgenden behandelt werden.

Während der Decode-Phase erzeugt die entsprechende Pipeline-Stufe im Prozessor ein oder mehrere Tupel von Erkennungsbits pro Instruktion. Diese Erkennungstupel beinhalten eine geordnete Liste von

In dem statischen, vom Compiler erzeugten Programm bestehen die Speicherelemente aus den im Programmiermodell sichtbaren Teilen, also den logischen Registern und dem Hauptspeicher. Bei einer parallelen Ausführung entstehen dadurch jedoch die virtuellen Hazards WAR and WAW und schränken die Parallelität ein. Daher entspricht bei superskalaren Prozessoren vor allem jedes logische Register oft mehreren physikalischen Speicherstellen. Während in einer streng sequenziellen Ausführung jedes Register in der richtigen Reihenfolge beschrieben oder gelesen wird, bildet der superskalare Prozessor diese Zeitsequenz auf verschiedene Speicherorte ab.

Register Renaming

Das folgende Bild zeigt das Verhalten in dieser Phase, die neben dem Decode auch ein Rename enthält. Die Zuordnung von Tupeln zu den Instruktionen erleichtert das Umbenennen erheblich. Wichtig ist dabei, dass auch nachfolgende Anweisungen, die ein umbenanntes Register als Input nutzen, ebenfalls entsprechend angepasst werden. Das im Bild gezeigte Renaming nutzt einen Pool von physikalischen Registern, deren Anzahl die der logischen übersteigt. Eine Mapping-Tabelle ordnet jedem logischen Register ein physikalisches aus einer Freiliste zu und verkleinert die Freiliste entsprechend.

Das Verfahren ordnet in dem Beispiel r3 (bisher R1) auf R2 zu. Dabei bleibt R1 weiterhin mit dem Lesewert für die Addition belegt. R1 wird erst nach der Operation frei und kann dann neu zugeordnet werden. Die oft nötige Doppelzuordnung erklärt die größere Anzahl von physikalischen Registern gegenüber den logischen Registern. Sind für die Zuordnung von Instruktionen zu physikalischen Einheiten keine physikalischen Register mehr frei, stoppt der Dispatch und wartet auf den Abschluss gerade laufender Befehle.

Die Freigabe von physikalischen Registern erfolgt durch den letzten Lesevorgang. Nach diesem Zugriff werden sie nicht mehr benötigt, und der Dispatcher kann die Register wieder in die Freiliste eintragen. Dieser Verwaltungsvorgang und insbesondere die Feststellung des letzten Lesevorgangs sind aber nicht trivial. Eine Methode besteht in der Installation eines Zählers zu jedem physikalischen Register. Jedes Source-Mapping des Registers inkrementiert ihn, jeder Lesevorgang dekrementiert ihn. Sobald der Zähler die 0 erreicht, ist das Register wieder frei, sofern das zugehörige logische Register in der Zwischenzeit umbenannt wurde.

Reorder Buffer

Einfacher, jedoch weniger effektiv, ist es, auf die Umbenennung durch eine spätere Instruktion, die Wertzuweisung und vor allem das Commit (Wertvalidierung, siehe nachfolgenden Teil) zu warten. In diesem Fall ist das Register garantiert frei für neue Zuordnungen. Zudem lässt diese späte Freigabe der Register eine garantierte Wiederherstellung im Fall eines Interrupt Requests zu.

Eine andere Methode nutzt ein 1:1-Verhältnis zwischen logischen und physikalischen Registern sowie zusätzlich einen Pufferspeicher mit einem Eintrag für jede aktive Instruktion. Dieser so genannte Reorder Buffer dient auch als Mittel zur Herstellung der korrekten Berechnungsreihenfolge. Aktive Instruktionen sind zur Ausführung freigegeben (dispatched) oder bereits ausgeführt, jedoch noch nicht in ihrem Wert validiert (committed). Dieser Pufferspeicher ist zumeist logisch als FIFO-Speicher, in Hardware als Ringspeicher mit Kopf- und Endzeiger implementiert. Jede Instruktion landet in einem Eintrag am Ende des Speichers, sobald sie zur Ausführung ansteht.

Die Wertzuweisung des Ergebnisses einer Instruktion erfolgt nun in den Reorder Buffer bei gleichzeitiger Zuweisung der entsprechenden Referenzen auf diesen Puffer. Die Referenzen erfolgen wiederum durch Mapping Tabellen, und zwar zum Zeitpunkt der Dekodierung. Im Beispiel erhält das Source-Register r3 den Eintrag rob6, während das Ergebnis in rob8 steht. Erreicht die Instruktion nun den Kopf-Pointer des Puffers, wird der Eintrag herausgenommen und in das Register übertragen, falls dies bereits möglich ist. Der Zugriff auf den Reorder Buffer erfolgt nach den Regeln des FIFO-Zugriffs, so dass zwei Situationen den Fortlauf blockieren können: nicht beendete Instruktionen beziehungsweise Referenzen auf den rob-Eintrag sowie ein voller Reorder Buffer. Letztere Situation stoppt die Dekodierungen vorübergehend, die erste muss für die superskalare CPU grundsätzlich lösbar sein.

Gemeinsam ist beiden Verfahren, dass sie virtuelle WAW- und WAR-Hazards durch dynamisches Register Renaming auflösen, während reale RAW-Hazards erhalten bleiben. Letztere führen tatsächlich zu Verzögerungen in der Pipeline.

Instruction Issueing und parallele Ausführung

Die Erstellung von Instruktionstupeln, die alle notwendigen Informationen zur Ausführung beinhalten, führt zur nächsten Pipeline-Stufe. Sie teilt den parallel zueinander vorhandenen Hardware-Ressourcen die Instruktionen zu. Hier beginnt das Ausführungsfenster, das eigentliche Herz der superskalaren CPU.

Das so genannte Instruction Issue testet zur Laufzeit, ob alle zur Ausführung benötigten Daten und Ressourcen wie beispielsweise eine Integer-ALU vorhanden sind. Idealerweise sollte eine Instruktion nur dann zur Ausführung kommen, wenn alle Eingangsdaten vorhanden sind.

Das folgende Beispiel zeigt den Ablauf des Sortiercodes aus dem anfangs gezeigten Sortierprogramm in einer Modellarchitektur mit zwei Integer-, einer Branch- und einer Speicherzugriffseinheit. Da der Assembler-Code nicht besonders effektiv ist, fällt auch die Ausführung in der superskalaren Modell-CPU nicht optimal aus.

Jeder vertikale Schritt im Bild bedeutet einen Zeittakt. Es wird angenommen, dass der Befehl ble zu keinem Sprung führt, blt hingegen schon, da dieser die Schleife begründet. Das Register r3 ist durch Renaming auf entsprechende physikalische Register abgebildet. Aus Gründen der Übersichtlichkeit verzichtet der Beispielcode auf das Renaming der anderen Register, obwohl auch sie entsprechend gemappt werden müssen.

Instruction Issue Buffer

Die Organisation des Instruction Issue Buffer ist auf verschiedene Weisen möglich, wobei drei Basiskonzepte gängig sind:

Die Single-Queue-Methode: Für eine einzige Warteschlange wird kein Register Renaming benötigt, sofern keine Out-of-Order-Zuweisung möglich ist. Einfache Reservierungsbits verwalten die Verfügbarkeit von Operanden, indem sie jedes Register als reserviert anzeigen, falls eine Registermodifizierung ansteht. Die Vollendung des Schreibzugriffs löscht diese Reservierung wieder. Die Instruction Issue gibt einen Befehl zur Ausführung frei, falls keine Reservierungen bei den Operanden des Befehls mehr vorhanden sind.

Die Multiple-Queue-Methode: Innerhalb der einzelnen Queues gibt die Instruction Issue die Instruktionen in der entsprechenden Reihenfolge frei. Da jedoch mehrere parallele Warteschlangen existieren, denen entsprechende Befehlsgruppen zugeordnet sind, können Instruktionen durchaus in Out-of-Order-Reihenfolge ablaufen. Aus diesem Grund kann ein Register Renaming erforderlich sein, eventuell aber in einer eingeschränkten Form. Im obigen Beispiel, in dem je eine Warteschlange für Floating-Point-, Load-/Store- und Integer-Operationen vorgesehen ist, könnten zum Beispiel lediglich die Register, die aus dem Speicher heraus mit neuen Werten geladen werden, umbenannt werden. Dies würde ein Voreilen dieser Load-Operationen vor die anderen Warteschlangen ermöglichen.

Die Methode der Reservation Stations erlaubt die Freigabe von beliebigen Operationen in Out-of-Order-Reihenfolge. Jede Operation erhält ein Feld mit Einträgen zum Operationstyp und zur Operandenverwaltung. Die Instruction Issue testet die Operanden ständig auf Verfügbarkeit und trägt sie dann als Wert oder Pointer in die Operandenfelder ein. Die Instruction Issue gibt die Ausführung der Operation frei, sobald alle Operanden vorhanden sind. Die reale Ausführung dieser Reservierungen ist oft auf einzelne Befehlsgruppen aufgeteilt, um Datenpfade zu reduzieren.

Die Behandlung von Speicherzugriffen

Das RISC-Konzept, Speicherzugriffe nur für explizite Load-/Store-Befehle zu erlauben, nutzen auch superskalare Rechner, da dieses Verfahren den Speicherzugriff deutlich kanalisiert und so die Optimierung erleichtert. Die Begründung bei RISC-Architekturen lag dagegen in der Möglichkeit, Speicherzugriffe ohne weitere Arithmetik in einer vierstufigen Pipeline zu behandeln. Zusätzliche Arithmetik hätte die Anzahl der Stufen vergrößert.

Für superskalare CPUs gilt diese Einschränkung für Speicherzugriffe per se nicht. Die Befehle laufen nicht mehr innerhalb eines starren Pipeline-Schemas ab, das für jede Stufe eine bestimmte Aktion vorsieht. Die Beschränkung auf die expliziten Load-/Store-Befehle bedeutet eine Aufwandsbegrenzung, prinzipiell sind jedoch alle Verfahren auf die arithmetischen und logischen Befehle erweiterbar.

Abgesehen von den im Folgenden gezeigten Vorgängen in der CPU sind für superskalare Rechner dezidierte, hierarchische Speicherkonzepte notwendig, um die entsprechenden Zugriffsgeschwindigkeiten nutzen zu können. Die Hierarchie ist zumeist dreistufig: L1-Cache (primär, klein, sehr schnell), L2-Cache (sekundär, größer, etwas langsamer) und der eigentliche Hauptspeicher (groß, langsam, eventuell mit Virtual Memory Management).

Dieses Konzept bietet genügend Flexibilität, wobei der Primär-Cache bis auf wenige Ausnahmen auf dem Prozessorchip integriert ist. Die aktuelle Halbleiterfertigung hat die Obergrenze der Transistoren auf einem Chip in den letzten Jahren so weit gesteigert, dass auch ein L2-Cache im Megabyte-Bereich auf dem Chip üblich ist. Intels Itanium (siehe Kapitel 7.2) soll neben dem L1- und L2-Cache demnächst sogar noch 24 MByte L3-Cache auf dem Prozessorchip erhalten.

Die Behandlung von Load-/Store-Befehlen unterscheidet sich grundlegend von Befehlen ohne externen Speicherzugriff, weil ihre Ausführung freigegeben wird, ohne dass die Operanden bereits bekannt sind. Die Bestimmung der Speicherstelle benötigt eine Adresskalkulation, die ihrerseits Bestandteil der Ausführungsphase ist. Das Ergebnis dieser Kalkulation ist eine logische Adresse, die eine Adressübersetzung schließlich in eine physikalische Adresse umsetzt. Diese Address Translation ist nicht zwingend Bestandteil eines Prozessors, häufig aber integriert. Oft kommt dabei ein Translation Look-aside Buffer (TLB) zum Einsatz, der kürzlich geladene Pages speichert.

Überlappende Operationen

Die Adressübersetzung und der Speicherzugriff können - und werden - als überlappende Operation ausgeführt, um Zeit zu sparen. Die CPU versucht den Zugriff sofort nach dem Ende der Adresskalkulation. Ein integrierter Cache kann dabei zu einer deutlichen Beschleunigung führen. Weitere Möglichkeiten für einen schnelleren externen Speicherzugriff bestehen in der überlappenden Ausführung durch mehrere Load-/Store-Pipelines. Auch die Überlappung mit Instruktionen ohne Speicherzugriff sorgt für mehr Effizienz, sofern dabei keine Hazards auftreten. Externe Speicherzugriffe in einer Out-of-Order-Reihenfolge kommen dagegen nur sehr selten zum Einsatz. Diese sind äußerst problematisch und bringen erhebliche Komplikationen für das gesamte Rechnersystem mit sich.

Die Speicherzugriffe erfolgen häufig indirekt über ein Register, das die Adresse für den Zugriff enthält. Die Adresskalkulation erfolgt wie oben dargestellt innerhalb der Ausführungsphase. Eine Out-of-Order-Ausführung dieser Befehle bedeutet jedoch nicht nur, dass die Registerinhalte gegenüber WAR- und WAW-Hazards geschützt sein müssen. Vielmehr wäre dies auch für den gesamten Speicher nötig, was angesichts der Anzahl der Speicherzellen unmöglich ist. Als Folge davon kann der Prozessor nur auf eng begrenzte Subsets von Speicherstellen, die zum aktiven Satz zählen, indirekt Out-of-Order ansprechen.

Die Möglichkeit, nur einen Speicherzugriff pro Zeiteinheit (Takt) auszuführen, ist sehr begrenzend und stellt den Flaschenhals der gesamten Architektur dar. Beispiele hierzu zeigt Kapitel 6 anhand von konkreten Programmen. Die parallele Ausführung von Speicherzugriffen hingegen benötigt Multiported-Speicherhie-rarchien, weil ansonsten der Speicher wiederum serialisiert. Meist wird deshalb der primäre Cache als Multiport-Speicher ausgeführt. Falls er eine ausreichende Größe hat, ist dadurch die Zugriffshäufigkeit auf höhere Speicherebenen gering genug, um den Flaschenhals zu entschärfen. Die Multiport-Fähigkeit wird durch Speicherzellen, die mehrere Zugänge haben, durch mehrere Speicherbänke mit entsprechender Verteilung der Speicherinhalte oder durch mehrfache serielle Zugriffe pro Takt erreicht. Der Speicher muss zudem nonblocking sein, falls die überlappende Ausführung eines Speicherzugriffs erlaubt ist. In diesem Fall darf ein Cache-Miss nicht zur Blockade von anderen Operationen führen.

Die Überwachung von Hazards kann über die Einführung von Store Address Buffers erfolgen. Durch Adressvergleich können sie feststellen, ob Ladevorgänge mit noch ausstehenden Schreibvorgängen zu Hazards führen. Die Logik im Bild detektiert insbesondere die echten RAW-Hazards, eine Erweiterung für die virtuellen Hazards ist natürlich möglich.

Die Commit-Phase der Befehlsausführung

Die wesentliche Eigenschaft der superskalaren CPU ist die parallele Ausführung von Instruktionen. Die daraus entstehenden Ergebnisse muss die CPU aber in der korrekten sequenziellen Reihenfolge (Ergebnissequenzialität) abspeichern. Diese Anforderung wird noch durch zusätzliche Verfahren wie spekulative Ausführung oder Out-of-Order-Ausführung verschärft. Daher muss die CPU immer wieder in einen präzisen Zustand versetzt werden. Er ist insbesondere zu zwei Zeitpunkten absolut zwingend notwendig: wenn die superskalare CPU einen Interrupt Request abhandelt oder aus einem solchen zurückkehrt (Recovery). Hierfür kommen zwei Techniken zur Anwendung:

Das erste Verfahren nutzt Checkpoints, an denen der tatsächliche Zustand der CPU ein Update erfährt und sie den bisherigen in einem History Buffer (mit entsprechender Tiefe) speichert. Hierdurch ist jederzeit ein präziser Zustand auch für die Vergangenheit herstellbar. Die Commit-Phase muss nur den korrekten Zustand herstellen und die nicht weiter benötigten aus dem History Buffer entfernen.

Das zweite Verfahren steht in engem Zusammenhang mit dem Reorder Buffer und wird daher bevorzugt. Dazu teilt man den Zustand der Maschine in zwei Teile auf. Jede ausgeführte Instruktion setzt den physikalischen Zustand neu. Der architekturelle Zustand wird dagegen erst in der sequenziellen Reihenfolge der Operationen beschrieben, wenn ein eventueller spekulativer Status geklärt ist. Hierzu ist es notwendig, dass ein Reorder Buffer den spekulativen Zustand mitführt. Während der Commit-Phase wird dann dieser Zustand in den architekturellen Zustand (und den physikalischen) übernommen, die Register werden aus dem Reorder Buffer in den Register File geschrieben und die Store-Operationen durchgeführt. Die Einträge im Reorder Buffer erhalten hierzu weitere Informationen wie Program-Counter-Wert, Interrupt-Bedingungen und vieles mehr. Datenwerte muss der Reorder Buffer nur dann halten, wenn kein physikalisches Register File mit mehr als den logischen Registern vorhanden ist.

Die Einführung eines Reorder Buffers mit allen notwendigen Informationen erweist sich so als gute Lösung zur Herstellung eines präzisen Zustands bei gleichzeitiger Durchführung des Register Renamings.

Einige Beispiele für superskalare Architekturen

Die folgenden Beispiele stellen zunächst zwei Architekturen zusammenfassend dar, um die konkrete Implementation von superskalaren CPUs zu erläutern. Als Beispiele dienen hierbei die MIPS R10000- und die DEC Alpha-21164-Architektur, die beide mit relativ gut strukturierten Befehlssätzen arbeiten. Obwohl nicht mehr aktuell, stellen beide CPUs auf Grund ihres Designs sehr gute Beispiele für superskalare Architekturen dar.

Eine Diskussion des Pentium 4 schließt dieses Kapitel ab. Dieser Prozessor stellt mit seinem gewachsenen CISC-Befehlssatz sozusagen das Gegenteil dessen dar, was bislang hier diskutiert wurde: superskalare Mikroprozessoren als Weiterentwicklung von RISC-Prozessoren.

MIPS R10000

Die MIPS R10000-CPU kann als Beispiel für eine typische CPU superskalaren Aufbaus gelten, wobei im Folgenden die exakten Eigenschaften dargestellt sind. Die R10000 kann zu einem Zeitpunkt vier Instruktionen vom Instruktions-Cache laden. Die Informationen in diesem Cache enthalten je vier zusätzliche Predecode-Bits zur Unterscheidung des Instruktionstyps. Die Ausführung von Branch-Befehlen wird vorhergesagt, indem jedem der insgesamt 512 Einträge im Instruktions-Cache ein 2-Bit-Zähler ohne Überlauf zugeordnet ist, der die bisherige Verzweigungsgeschichte protokolliert.

Aufbau der MIPS R10000-CPU.

Vorhergesagte Sprünge führen in einem zusätzlichen Takt zum Laden der neuen Instruktionen aus dem Cache. Ein Resume-Cache speichert in diesem Fall die Instruktionen, die ohne Sprung nachfolgen. Auf diese Weise kann die Pipeline sie bei einer falschen Sprungvorhersage schnellstmöglich nachladen. Der Umfang dieses Resume-Speichers beträgt vier Instruktionsblöcke, so dass er vier Branch-Befehle pro Zeit managen kann.

Das Register Renaming benutzt ein physikalisches Register File, das mit 64 Registern die doppelte Größe des logischen Files (32 Register) besitzt. Eine Remapping-Tabelle ordnet jedem logischen Register aus den Befehlsoperanden einen Eintrag in einem physikalischen Register zu.

Die drei Instruction Queues für Speicherzugriffe, Integer- und Floating-Point-Operationen haben eine Tiefe von 16 Einträgen, wobei zu einem Zeitpunkt vier Instruktionen gleichzeitig zugeordnet werden können. Die Einträge entsprechen in etwa den Reservation Stations. Jeder Eintrag enthält Pointer auf die Operanden, jedoch keine Validbits. Eine Instruktion kann erst dann zur Ausführung kommen, wenn alle Operanden ein "not busy" anzeigen.

Die fünf Funktionseinheiten bestehen aus einem Adressaddierer, zwei Integer-ALUs, einem Floating-Point-Addierer und einer Floating-Point-Einheit für Multiplikation, Division und Quadratwurzel. Die beiden Integer-ALUs sind nicht vollkommen identisch. Beide beinhalten die Basisoperationen Addition/Subtraktion/Logik, eine ist jedoch zusätzlich auf Shift-Operationen, die andere auf Multiplikation und Division spezialisiert.

Generell versucht die R1000-CPU, ältere Operationen zuerst auszuführen, für Speicheroperationen erfolgt eine einfache Detektion von Hazards. Die R10000-CPU besitzt einen primären On-Chip-Cache mit 32 KByte Größe. Er ist 2-Wege-assoziativ und arbeitet mit einer Linesize von 32 Bytes. Zudem unterstützt die CPU noch einen Sekundär-Cache.

Die Herstellung eines präzisen Zustands gewährleistet ein Reorder Buffer. Er kann vier Instruktionen pro Takt in der ursprünglichen Reihenfolge des Programms für gültig erklären. Dieses Verfahren definiert den Inhalt des physikalischen Registers zum neuen gültigen Wert und gibt das Register wieder frei.

Exception-Bedingungen werden zwischengespeichert, um bei Ausnahmen den vorhergehenden Zustand wiederherzustellen. Auch die Branch-Vorhersage erstellt eine Kopie aller Register, um bei einer falschen Vorhersage die korrekten Werte schnell wieder herstellen zu können.

Alpha 21164

Die DEC Alpha-21164-CPU stellt eine vereinfachte Architektur für superskalare CPUs dar. Dies scheint ein Tribut an die für 1995 sehr hohe Taktrate von 500 MHz zu sein. Die Alpha-CPU besitzt einen Instruction Cache von acht KByte, der vier Instruktionen zeitgleich in einen von zwei Instruction Buffer laden kann. Diese Instruction Buffer sind jeweils vier Einträge groß, die Befehle darin kommen ausschließlich in sequenzieller Reihenfolge zur Ausführung. Deshalb muss jeder Buffer erst geleert sein, bevor der andere zum Einsatz kommt. Dies bedeutet zwar eine Restriktion in der Ausführung, vereinfacht das Design jedoch erheblich.

Die Branch-Vorhersage erfolgt wie beim R1000 durch einen 2-Bit-Zähler je Instruktion im Puffer. Im Unterschied zur MIPS-CPU ist aber nur eine laufende Sprungvorhersage möglich, jeden weiteren Branch-Befehl verzögert der Alpha 21164, bis der vorangegangene vollständig ausgeführt ist.

Die Zuführung ausführbereiter Instruktionen erfolgt durch eine Single Queue, eine parallele Zuführung ist also nicht möglich. Deshalb benötigt die Alpha-CPU auch kein Register Renaming, das Design bleibt dadurch einfach. Zur Ausführung von Instruktionen stehen vier Funktionseinheiten zur Verfügung: Zwei Integer-ALUs, wovon eine auf Shift und Multiplikation, die andere auf Branch Evaluation spezialisiert ist, ein Floating-Point-Addierer und ein Floating-Point-Multiplizierer. Das Cache-Konzept umfasst einen Instruktions-Cache (acht KByte), einen primären Daten-Cache (acht KByte, direct mapped) und einen gemeinsamen, externen Sekundär-Cache (96 KByte, 3-Wege-assoziativ). Die primären Caches können Datenzugriffe in einem Takt abarbeiten und sind in der Lage, ausstehende Fehlzugriffe zu behandeln (sechs "Miss Address File"-Einträge).

Ein einfacher Reorder Buffer erfasst die Ergebnissequenz und erklärt sie entsprechend für gültig. Dies beinhaltet das Update der Integer-Register in der originalen Programmsequenz, während Floating-Point-Operationen ihre Register auch Out-of-Order updaten dürfen. Deshalb können die Floating-Point-Register auch in einem nicht präzisen Zustand sein, den die CPU gelegentlich korrigieren muss.

Intel Pentium 4

Die Architektur des Intel Pentium 4 ist über viele Stufen gewachsen. Ihr Ursprung beruht noch auf dem 1978 vorgestellten Intel 8086. Zu diesem Zeitpunkt war RISC noch eine Nischenarchitektur. So verwundert es nicht, dass dieser Mikroprozessor die CISC-Architektur verfolgt.

Die Befehlsbearbeitung im Pentium 4 ist zweigeteilt: Die Fetch- und Dekodierphase sind für das Interface zum Instruktionsspeicher verantwortlich, die interne Ausführungs-Pipeline andererseits sorgt für eine davon entkoppelte Ausführung der Befehle. Zunächst fällt das Fehlen eines L1-Instruktions-Cache auf, für originäre Befehle steht nur der aktuell bis zu zwei MByte große L2-Cache bereit. Der Verzicht ist zum einen möglich, weil Intel den Fetch-/Decode- und den Execute-Teil voneinander getrennt hat. Dadurch ist ein L1-Cache mit einem Zugriff von einem Takt nicht nötig. Zum anderen ersetzt ein so genannter Trace-Cache im Execute-Teil den L1-Cache. Er speichert die bereits durchlaufenen Befehle, einschließlich der Branch Prediction über Branch Target Buffer. Dieses Verfahren bewährt sich sehr bei Schleifen, denn diese durchlaufen die Instruktionspfade in der Regel mehrfach. Zentrales Element des Pentium4 ist jedoch die Umsetzung der Assembler-Befehle in µOps (Mikrooperationen). Sie schaffen das notwendige Interface, um die CISC-Befehlsstruktur an das RISC-Ideal anzugleichen.

Dazu übersetzt der Pentium 4 jeden Befehl je nach Komplexität in mehrere µOps. Diese durchlaufen dann ganz im RISC-Ideal eine mehrstufige Pipeline. Bis Anfang 2004 nutzte der Pentium 4 eine 20-stufige Pipeline. Sechs Pipelines stehen ihm zur Verfügung, in denen er drei Fetches von µOps aus dem Trace-Cache starten kann. Inzwischen hat Intel das Design für Taktraten bis zu 5 GHz optimiert. Dafür musste man die Pipeline auf rekordverdächtige 31 Stufen verlängern.

Ausblick

Die Entwicklung einer superskalaren Hardware ist ein wichtiger Schritt für leistungsfähigere Prozessoren. Doch ohne eine entsprechende Unterstützung durch die Software liegen ihre Fähigkeiten oft brach. Deshalb werden wir im nächsten Teil unserer Serie speziell auf neue Compiler-Technologien zur Leistungssteigerung aktueller Rechnerarchitekturen eingehen.

Diesen Artikel und eine ganze Reihe weiterer Grundlagenthemen zu Prozessoren finden Sie auch in unserem tecCHANNEL-Compact "Prozessor-Technologie". Die Ausgabe können Sie in unserem Online-Shop versandkostenfrei bestellen. Ausführliche Infos zum Inhalt des tecCHANNEL-Compact "Prozessor-Technologie" finden Sie hier. (ala)