Herkömmliche CPUs arbeiten die anstehenden Befehle im Wesentlichen sequenziell ab. Vor allem die Prozessoren der Intel-32-Bit-Architektur (IA-32) erreichen ihre Rechenleistung durch steigende Taktfrequenzen und eine immer längere Fließbandverarbeitung. So weist die Netburst-Architektur im Pentium 4 eine 20-stufige Pipeline auf, mit der Taktfrequenzen von bis zu 10 GHz möglich sein sollen. Beim Athlon XP/MP sind es immerhin 10 Integer- und 15 FPU-Pipeline-Stufen.
Ein erster Ansatz der Parallelisierung zeigt sich in superskalaren Architekturen. Hier verrichten in einem Mikroprozessor mehrere Ausführungs-Pipelines ihren Dienst. Der Pentium 4 hat beispielsweise drei Ganzzahl- und zwei Gleitkomma-Ausführungseinheiten, während der Athlon XP/MP mit jeweils drei arbeitet.
Reicht die mit diesen Techniken erzielte Rechenleistung nicht aus, können in Dualprozessor- (DP) oder Multiprozessor-Systemen (MP) mehrere CPUs parallel arbeiten. Diese Lösung ist insbesondere im Server- und Workstation-Bereich populär. Der maßgebliche Nachteil der Multiprozessor-Technik sind jedoch die hohen Kosten. Auf Grund der geringen Stückzahlen sind schon die Mainboards erheblich teurer als ihre Pendants für nur eine CPU. Dazu kommen eine aufwendigere und damit teurere Spannungsversorgung und natürlich die Kosten für den zweiten Prozessor.
Ineffiziente Prozessoren
Ein großer Teil der Prozessor-Ressourcen bleibt sowohl im Single- als auch im DP/MP-Betrieb ungenutzt. Intel gibt an, dass bei Anwendungen mit einer typischen Verteilung der Befehle bei der 32-Bit-Netburst-Architektur vom Pentium 4 und Xeon nur etwa 35 Prozent der Ressourcen eingesetzt werden. Dieses traurige Ergebnis ist den verschiedenen Engpässen der kontrollflussorientierten von-Neumann-Architektur zuzuschreiben.
Aber auch die durch das Amdahlsche Gesetz beschriebene Tatsache ist hierfür verantwortlich, dass sich ein Teil der Befehlsabläufe auf Grund von Datenabhängigkeiten nicht parallelisieren lässt. Dies gilt vor allem für die Parallelisierung auf Befehlsebene (Instruction Level Parallelism, ILP), wie sie im Rahmen von Pipeline- und superskalaren Systemen innerhalb eines Mikroprozessors realisiert werden.
Viel einfacher ist es, nebenläufige oder gänzlich unabhängige Vorgänge parallel abzuarbeiten. Die Verteilung der Prozesse oder Threads auf verschiedene Mikroprozessoren wird bereits seit einiger Zeit von Betriebssystemen unterstützt (Thread Level Parallelism - TLP). Die Effizienz der einzelnen CPUs steigt aber durch den Einsatz mehrerer Prozessoren auch nicht, wie im Bild "Standard-CPUs" der Vergleich zwischen "Superscalar" und "Multiprocessing" zeigt.
Prozesse und Threads
Bevor wir weiter auf Hyper-Threading eingehen, gilt es zunächst, die zu Grunde liegenden Begriffe und Hintergründe auf der Software-Seite zu definieren und zu erläutern. Unter einem Software-Prozess versteht man eine Instanz eines Programms in der Ausführung. Hierzu gehören insbesondere [1]:
das zeitlich invariante (unabhängige) Programm
ein Satz von Daten, mit dem der Prozess initialisiert wird
ein zeitlich varianter Zustand
sowie verschiedene Informationen über diesen Prozess.
Die Koordination zwischen den Prozessen eines Systems erfolgt einerseits dadurch, dass sie von sich aus bestimmte Zustände einnehmen. Andererseits kann ein spezieller Prozess hoher Priorität - ein Dispatcher - sie in definierte Zustände versetzen.
Prozesse sind mit einer Vielzahl von Zustandsinformationen verknüpft, die im Process Control Block (PCB) abgelegt werden. Deswegen eignen sie sich kaum, um nebenläufige Vorgänge innerhalb einer Anwendung zu realisieren. Ein Konzept, das hier Abhilfe schafft [2], ist der "leichtgewichtige" Prozess oder Handlungsfaden (Thread of Control). Einem solchen Thread ist nur ein minimaler Umfang von Kontextinformationen zugeordnet, die für den Ablauf wichtig sind: Prozessor-Register und -Zustand sowie Stack. Software, die solche Threads implementiert und verwaltet, bietet oft eine weitere Abstraktionsebene an. Auf ihr werden mehrere dieser Threads gruppiert und ihnen gemeinschaftlich die fehlenden Kontextelemente zugewiesen.
Ein Betriebssystem, das zu dieser Abstraktion beigetragen hat, ist der an der Carnegie-Mellon University entwickelte Mach-Kernel. Hier sind mehrere Threads zu einem Task zusammengefasst, der einen eigenen Adressraum besitzt. Bei Mach entspricht ein Task mit einem Thread einem Prozess bei Unix. Ein Thread ist somit ein sequenzieller Kontrollfluss, der nebenläufig zu weiteren im selben Adressbereich eines Tasks ausgeführt wird.
Threads
Der Einsatz von Threads bietet zwei wesentliche Vorteile:
Nebenläufige Vorgänge lassen sich innerhalb von Anwendungsprogrammen elegant umsetzen. Viele moderne Programmiersprachen haben das Konzept der Nebenläufigkeit implementiert. Beispiele sind Java oder die Microsoft Foundation Classes (MFC) für C++.
Die Abhängigkeit zwischen den Befehlen in den unterschiedlichen Threads ist meist sehr gering, so dass die Thread-Ebene eine hervorragende Plattform für die Verteilung der Befehle auf unterschiedliche Ausführungseinheiten darstellt.
In modernen Anwendungen ist die Aufteilung in unabhängige Prozesse und Threads zu beobachten:
Leistungsfähige Server müssen unabhängige Anfragen von zahlreichen Clients beantworten.
Auf Workstations wird mittlerweile vielfach Software eingesetzt, die bereits in mehrere Prozesse und Threads aufgeteilt ist. 3D-Programme sind hierfür ein Beispiel.
Aber auch im Bereich normaler Desktop-PCs laufen in zunehmendem Maße mehrere unabhängige Anwendungen, die ebenso parallelisiert werden könnten.
Multi-Threading
Multi-Threaded-Prozessoren bieten eine Hardware-basierte Unterstützung für den effizienten Einsatz von Threads. Sie erlauben vor allem einen schnellen Wechsel zwischen den Threads desselben Tasks. Dafür besitzen Multi-Threaded-Prozessoren mehrere Registersätze, die jeweils den unterschiedlichen Threads zugeteilt werden. Ein Verteiler (Dispatch Unit) veranlasst das Umschalten zwischen den verschiedenen Threads. Insbesondere sind zu nennen:
Switch-on-Event Multi-Threading stößt den Wechsel von einem auf einen anderen Thread nach einem Ereignis (Event) an. Das kann ein Cache-Miss sein, der einen Zugriff auf den Speicher zur Folge hätte.
Feingranulares, zeitschlitzorientiertes Multi-Threading gibt die einzelnen Bestandteile der verschiedenen Threads in die unterschiedlichen Zeitschlitze einer Pipeline. Zwar werden die Pipelines auf diese Weise am besten genutzt, dies geht allerdings zu Lasten eines größeren Verwaltungsaufwands auf der Hardware-Seite.
Auf dem Intel Developer Forum im August 2001 (IDF Fall 2001) wurde die Hyper-Threading-Technologie als Multi-Threaded-Erweiterung der IA-32-Architektur vorgestellt. Der ursprüngliche Codename lautete Jackson-Technologie, auch die Bezeichnung Simultaneous Multi-Threading (SMT) ist verbreitet. Die Intel-Entwickler setzen bei der Hyper-Threading-Technologie auf ein Konzept, welches
den Thread-Wechsel vom Betriebssystem auslösen lässt und damit eine größtmögliche Flexibilität erreicht.
grobgranular in dem Sinne ist, dass die Thread-Wechsel nur nach größeren Zeiteinheiten erfolgen.
Das Ziel von Hyper-Threading ist, dass die unterschiedlichen Pipelines eines Mikroprozessors durch unterschiedliche Threads zeitgleich - und damit wie getrennte Prozessoren - effizient genutzt werden.
Die auf mehreren Threads aufbauenden Programme und Betriebssysteme sehen den einen physischen Prozessor als mehrere logische Prozessoren (LP).
Hyper-Threading
Intel startet mit Hyper-Threading zunächst auf dem Xeon-Prozessor, der mit zwei logischen CPUs arbeitet.
Auf der Hardware-Seite lassen sich vier Gruppen in Abhängigkeit von der Nutzung durch die Threads unterscheiden:
1. Ein Teil der Hardware liegt nur einfach vor. Hierzu zählen insbesondere die meisten Cache-Speicher und die einfach vorhandenen Ausführungseinheiten wie die Gleitkomma-Pipeline. Zugriffskonflikte auf diese Teile müssen auf Hardware-Ebene abgefangen werden. Der gemeinsame Zugriff auf den Daten-Cache erlaubt sowohl eine effiziente Nutzung der Hardware-Ressourcen als auch den schnellen Datenaustausch zwischen zwei Threads, wenn diese auf die gleichen Speicheradressen (Shared Memory) zugreifen.
2. Ein Teil der Hardware liegt mehrfach vor und wird von den Threads parallel genutzt. Dies gilt vor allem für Ausführungseinheiten wie die Integer-Pipelines.
3. Ein dritter Teil wird weiterhin nur einfach zur Verfügung gestellt und jeweils hälftig von den beiden aktiven Threads genutzt. Hierzu zählen die Warteschlangen (Decoupling Queue) zwischen Instruction Trace Cache und Renaming-Logik sowie zwischen Renaming-Logik und Scheduler. Weiterhin sind das die Load-and-Store-Buffer am L1-Cache sowie die Reorder-/Retire-Buffer.
4. Und schließlich liegt ein Teil der Prozessor-Ressourcen in doppelter Ausführung vor. Dabei handelt es sich im Wesentlichen um die zentralen Zustands- und Datenregister (Architectural State - AS) und um den Interrupt Controller, der im Pentium 4 als Advanced Programmable Interrupt Controller (xAPIC) bezeichnet wird. Zum AS zählen insbesondere die für den Programmierer sichtbaren Daten-, Steuer-, Status-, Segment- und Debug-Register sowie Flags, Stack-Pointer und der Programmzähler (Instruction Pointer - IP).
Interner Ablauf
Das Betriebssystem verwaltet den einen Multi-Threaded-Prozessor mit den beiden logischen Prozessoren im Wesentlichen so wie zwei getrennte physische Prozessoren. Die nächsten auszuführenden Befehle der zwei Threads werden in die beiden Instruction Pointer (IP) gelegt und gelangen von dort zur Ausführung in die nächsten Stufen. Nach dem Abarbeiten der vorbereitenden Ausführungsschritte verteilt der Scheduler die unabhängigen Instruktionen auf die verfügbaren Ausführungseinheiten.
Die Arbeitsweise eines Hyper-Threading-Prozessors sieht beispielsweise so aus:
1. Zu Beginn sind beide logischen Prozessoren im Idle-Zustand.
2. Das Betriebssystem startet einen Thread 1 auf dem logischen Prozessor LP0. Während der Zeit, in der nur ein Thread aktiv ist, kann dieser allein über die Ressourcen des Prozessors verfügen. Von den doppelt vorhandenen Komponenten wird nur eine Ausführung betrieben.
3. Das Betriebssystem startet einen zweiten Thread auf dem logischen Prozessor LP1. Die beiden Threads teilen sich nun die vorhandenen Ressourcen. Dabei konkurrieren sie um die einfach und um die doppelt vorhandenen Hardware-Einheiten, die sie parallel nutzen. Einfach vorhandene Komponenten, die hälftig verwendet werden, sind entsprechend zu partitionieren. Für diese Aufteilung wird die Pipeline mit dem Thread 1 geleert (flushen) und anschließend mit beiden Threads neu gefüllt.
4. Wenn Thread 1 beendet ist, stehen die im vorherigen Punkt genannten Ressourcen Thread 2 vollständig zur Verfügung. Hierzu sollte Thread 1 explizit mit einem HALT beendet werden. Das Flushen und erneute Auffüllen der Pipeline kostet Zeit, weshalb ein zu häufiger Thread-Wechsel nicht empfehlenswert ist. Der erzielbare Leistungsgewinn würde sonst den Aufwand nicht rechtfertigen. Neben dem Leeren der Ausführungs-Pipelines ist auch eine besondere Behandlung von Sperrvariablen (Spin Locks) zu beachten. Sperrvariablen werden in zeitlich beschränkten kritischen Bereichen von Prozessen und Threads eingesetzt, um deren Unterbrechung zu vermeiden. Der neu eintretende Prozess muss entsprechend warten, bis die Sperrvariable aufgehoben wurde. Dies geschieht in der Regel durch eine zyklische Abfrage (Spin Wait Loops). Um dabei die Verzögerung beim Verlassen der Schleife so gering wie möglich zu halten, ist der neu eintretende Task in einem PAUSE-Status zu halten. Wenn ihm eine längere Wartezeit bevorsteht, ist diese zyklische Abfrage nicht mehr effizient. Er muss sich dann an das Betriebssystem wenden, damit der sperrende Task unterbrochen (suspend) wird. Diese Vorkehrungen sind abwärtskompatibel auch auf IA-32-Prozessoren ausführbar, die kein Hyper-Threading unterstützen.
Systemaufbau
Intel legt größten Wert auf die Feststellung, dass die Hyper-Threading-Technologie lediglich geringe Veränderungen des Systemaufbaus zur Folge hat. So sind beim Mainboard-Layout keine besonderen Anforderungen zu verzeichnen, da der Footprint der Hyper-Threading-Prozessoren gegenüber den bisherigen CPUs unverändert geblieben ist.
Nur in der Systemverwaltung müssen einige Modifikationen vorgenommen werden. Diese betreffen die beiden Tabellen zum Verwalten der Prozessoren (MP Table) und der Interrupt-Controller (APIC Table). Dabei enthält die MP Table einen Eintrag für jeden physischen Prozessor, während die APIC Table jeweils einen Eintrag pro Interrupt Controller und somit pro logischem Prozessor aufweist. Diese Einträge werden unter Anleitung eines Bootstrap Processor (BSP) gesetzt, der den Startvorgang abwickelt. Lediglich bei einer richtigen und vollständigen Zuordnung der CPU-IDs zu den logischen und physischen Prozessoren ist ein zielgerichtetes Load Balancing und eine korrekte Verwaltung von ID-gebundenen Lizenzen möglich.
Zum Kennzeichnen der APICs hat Intel seit dem Xeon die Länge der IDs von 4 auf 8 Bit erhöht, wobei jeweils die letzten Bitstellen die logischen IDs darstellen. So lassen sich auch mehr als zwei logische Prozessoren in einem Gehäuse unterbringen.
Derzeit empfiehlt Intel den Einsatz von Hyper-Threading nur für Serveranwendungen. Deshalb ist die neue Technik bei Workstations werkseitig abgeschaltet. Im Idealfall lässt sich das durch die Änderung einer Option im Mainboard-BIOS ändern. Die Sperre ist im Zuge der Power-on-Configuration vergleichsweise einfach realisiert: Wenn während der Reset-Phase der Prozessor-Pin A31# gesetzt ist, wird Hyper-Threading abgeschaltet. Ein späteres Umschalten während des Betriebs per Software ist nicht möglich.
Ausblick
Das Augenmerk der Intel-Entwickler lag bei der Implementierung von Hyper-Threading auf einem möglichst geringen zusätzlichen Die-Flächenaufwand. Dieser beträgt weniger als fünf Prozent. Damit lässt sich ein Kostenvorteil gegenüber Multiprozessor-Systemen erreichen. Damit ließe sich mit der Hyper-Threading-Technologie ein wesentlicher Kostenvorteil gegenüber Multiprozessor-Systemen erreichen. Ob das jedoch auch gegenüber monolithischen Multiprozessor-Chips ausreicht, darf man bezweifeln.
Ein Großteil der Chipfläche moderner Mikroprozessoren ist für die Caches zu veranschlagen. Bei monolithischen Multiprozessor-Chips teilen sich zwei Prozessor-Kerne einen gemeinsamen Cache. Dadurch bleibt der zusätzliche Flächenaufwand dort im Rahmen von schätzungsweise 20 bis 30 Prozent. Hierfür erhält man aber zwei Mikroprozessoren mit komplett verdoppelten Ausführungseinheiten und ebenso gemeinsam genutzten Caches.
Allerdings sollte man die beiden Ansätze nicht zu stark als Konkurrenten betrachten. Mit den weiteren Fortschritten der Halbleitertechnologie erscheinen mittelfristig Systeme realisierbar, die mehrere Multi-Threading-Prozessoren monolithisch integrieren und beide Vorteile nutzen.
Hyper-Threading ist eher als Zugabe zu sehen denn als Ersatz für ein MP-System. Was die neue Technologie wirklich leistet, haben wir in der CeBIT-Ausgabe des tecCHANNEL-Magazins ausführlich getestet.
Das Intel Developer Forum Spring 2002 nahm Intel zum Anlass, einen Ausblick auf die zukünfige Hyper-Threading-Strategie zu geben. Im Desktop-Segment hält Hyper-Threading demnach in der zweiten Jahreshälfte 2003 Einzug. Dann stellt Intel den Pentium-4-Nachfolger Prescott vor. (mec)
Literatur
[1] Giloi, Wolfgang K., Rechnerarchitektur, Springer Verlag, 2. Auflage, 1993, ISBN 3-540-56355-5
[2] Rechenberg, P., Pomberger. G., Informatik-Handbuch, Carl Hanser Verlag, 2. Auflage 1999, ISBN 3-446-19601-3, Seite 633 bis 674