Funktionsweise von simultanem Multithreading
(blog.codingconfessions.com)- SMT ist eine Technik, bei der ein CPU-Kern im selben Taktzyklus Befehle aus mehreren Threads ausgibt, um Ausführungseinheiten besser auszulasten, die allein durch Parallelität auf Befehlsebene ungenutzt blieben.
- Intels Hyper-Threading implementiert zwei Threads pro Kern und dupliziert den Architekturzustand, sodass dem Betriebssystem ein physischer Kern wie zwei logische Prozessoren erscheint.
- In der tatsächlichen Implementierung werden nicht einfach alle Ressourcen verdoppelt: Instruction Pointer, ITLB und RAT werden dupliziert, Trace Cache und TLB werden geteilt, während uop queue, physische Register und reorder buffer gleichmäßig aufgeteilt werden.
- Der Leistungsgewinn hängt von der Workload ab: Wenn zwei Threads um den Cache konkurrieren, kann es langsamer werden; kooperierende Threads, die Daten austauschen, können dank gemeinsam genutztem Cache schneller werden.
- Gemeinsam genutzte Ressourcen und spekulative Ausführung können zu Sicherheitslücken führen; in sicherheitskritischen Umgebungen oder bei Workloads, die maximale Performance und minimale Latenz benötigen, ist SMT zu deaktivieren eine realistische Option.
Warum SMT nötig ist
- Moderne Prozessoren verfügen über Hunderte von Registern, mehrere Load/Store-Einheiten und arithmetische Einheiten und nutzen Techniken der Parallelität auf Befehlsebene wie Pipelining, superskalare Architekturen und Out-of-Order-Ausführung, um diese auszulasten.
- Eine Pipeline teilt die Befehlsausführung in mehrere Stufen auf und reicht in jedem Taktzyklus Befehle an die nächste Stufe weiter; bei einer Pipeline-Tiefe von 5 können ab dem fünften Zyklus bis zu 5 Befehle gleichzeitig in Bearbeitung sein.
- Superskalare Prozessoren können in einem Taktzyklus mehrere Befehle ausgeben; aktuelle Intel-Core-i7-Prozessoren können 4 Befehle pro Zyklus ausgeben.
- In echten Programmen ist es schwierig, genügend unabhängige Befehle zu finden, sodass Ausführungseinheiten zeitweise ungenutzt bleiben.
- Horizontale Verschwendung entsteht, wenn innerhalb eines Threads nicht genügend unabhängige Befehle gefunden werden, um die issue width zu füllen.
- Vertikale Verschwendung entsteht, wenn die nächsten Befehle vom aktuell ausgeführten Befehl abhängen und in einem Taktzyklus gar kein Befehl ausgegeben werden kann.
- Traditionelles Multithreading kann vertikale Verschwendung reduzieren, indem pro Taktzyklus nur Befehle eines Threads ausgegeben werden und im nächsten Zyklus auf einen anderen Thread gewechselt wird; horizontale Verschwendung und der Overhead des Kontextwechsels bleiben jedoch bestehen.
- SMT gibt ohne Kontextwechsel im selben Taktzyklus Befehle aus mehreren Threads aus und füllt die Ausführungseinheiten dadurch zu einem höheren Anteil.
- Intels SMT-Implementierung Hyper-Threading ist auf zwei Threads pro Kern beschränkt.
Grundstruktur von SMT nach Intel-Art
- Ein gewöhnlicher Nicht-SMT-Prozessor kann jeweils nur Befehle eines Threads ausführen.
- Jeder Thread besitzt einen Architekturzustand, der Registerwerte, Programmzähler, Steuerregister usw. umfasst.
- Um Befehle aus zwei Threads gleichzeitig auszuführen, müssen die Zustände beider Threads gleichzeitig repräsentiert werden; SMT-Implementierungen duplizieren daher den Architekturzustand des Prozessors.
- Durch diese Duplizierung erscheint ein physischer Prozessor dem Betriebssystem als zwei logische Prozessoren, und das Betriebssystem kann auf beiden Threads schedulen.
- Puffer und Ausführungsressourcen auf Mikroarchitekturebene werden je nach Faktoren wie Kosten, Leistungsaufnahme und Chipfläche dupliziert, geteilt oder partitioniert.
- Im Mittelpunkt steht hier vor allem Intels SMT-Implementierung, basierend auf Intels Whitepaper von 2002.
Die drei Teile der CPU-Mikroarchitektur
- Ein Prozessor stellt Programmierern die ISA als öffentliche Schnittstelle bereit; die ISA umfasst den Befehlssatz und die Register, die Befehle verwenden können.
- Die Mikroarchitektur ist die interne Implementierung, die sich auch zwischen Prozessormodellen unterscheiden kann, die dieselbe ISA unterstützen.
- Die Mikroarchitektur moderner Prozessoren lässt sich grob in drei Teile gliedern:
- Frontend: Enthält die Befehlssteuereinheit, die die als Nächstes auszuführenden Programmbefehle holt und dekodiert.
- Backend: Enthält Ausführungsressourcen wie physische Register, arithmetische Einheiten und Load/Store-Einheiten und weist dekodierten Befehlen Ressourcen zu, um ihre Ausführung zu planen.
- retirement unit: Übernimmt die Ergebnisse ausgeführter Befehle endgültig in den Architekturzustand des Prozessors.
SMT im Frontend
- Der Instruction Pointer verfolgt die Adresse des nächsten zu holenden Befehls.
- SMT-fähige Prozessoren besitzen zwei Sätze von Instruction Pointern, um die nächsten Befehle zweier Programme unabhängig zu verfolgen.
- Der Trace Cache speichert kürzlich dekodierte Befehls-Traces, um die Dekodierkosten und Ausführungslatenz wiederholt ausgeführter Befehle zu reduzieren.
- Er wird von zwei logischen Prozessoren je nach Bedarf dynamisch gemeinsam genutzt.
- Wenn ein Thread mehr Befehle ausführt, kann er mehr Einträge im Trace Cache belegen.
- Jeder Eintrag wird mit Thread-Informationen markiert, um Befehle der beiden Threads zu unterscheiden.
- Der Zugriff auf den Trace Cache wird in jedem Zyklus zwischen den beiden logischen Prozessoren arbitriert.
- Bei einem Trace-Cache-Miss sucht das Frontend im L1 Instruction Cache nach dem Befehl an der betreffenden Adresse; bei einem L1-Instruction-Cache-Miss muss er aus der nächsten Cache-Ebene oder dem Hauptspeicher geholt werden.
- Der L1 Instruction Cache cached Daten anhand virtueller Adressen, für den Zugriff auf den Hauptspeicher werden jedoch physische Adressen benötigt.
- Die ITLB enthält kürzlich übersetzte virtuelle Adressen und übersetzt virtuelle in physische Adressen.
- In SMT-fähigen Prozessoren besitzt jeder logische Prozessor seinen eigenen ITLB-Cache.
- Die Logik zum Holen von Befehlen aus dem Hauptspeicher arbeitet nach dem First-come-first-served-Prinzip, reserviert aber mindestens einen Request-Slot pro logischem Prozessor, damit beide logischen Prozessoren vorankommen können.
- Aus dem Hauptspeicher eintreffende Befehle werden vor dem Dekodieren in einem kleinen Streaming Buffer gespeichert; in SMT-fähigen Prozessoren wird auch dieser Puffer pro logischem Prozessor dupliziert.
- Nach dem Holen werden Befehle in kleinere, einfachere uops dekodiert.
- uops gelangen in die uop queue, die als Grenze zwischen CPU-Frontend und -Backend dient.
- Die uop queue wird gleichmäßig zwischen den beiden logischen Prozessoren geteilt; diese statische Partitionierung ermöglicht den beiden logischen Prozessoren unabhängigen Fortschritt.
SMT im Backend
- Das Backend holt Mikrobefehle aus der uop queue und führt sie aus, ist dabei aber nicht ausschließlich an die ursprüngliche Programmreihenfolge gebunden, sondern nutzt Out-of-Order-Ausführung.
- Nahe beieinanderliegende Programmbefehle hängen häufig voneinander ab; gibt es eine Operation mit langer Latenz wie einen Hauptspeicherzugriff, müssen auch abhängige Befehle warten.
- Die Out-of-Order-Engine führt weiter hinten liegende Befehle vor ihrer ursprünglichen Reihenfolge aus und reduziert so Ressourcenverschwendung.
- Der allocator identifiziert die für Mikrobefehle benötigten Ressourcen und weist sie je nach Verfügbarkeit zu.
- In einem Zyklus weist er Ressourcen für Mikrobefehle eines logischen Prozessors zu und wechselt im nächsten Zyklus zum anderen logischen Prozessor.
- Wenn in der uop queue nur Mikrobefehle eines logischen Prozessors liegen oder ein logischer Prozessor seinen Ressourcenanteil vollständig ausgeschöpft hat, nutzt der allocator alle Zyklen für den anderen logischen Prozessor.
- Die wichtigsten Ressourcen im Backend sind teils dupliziert, teils gemeinsam genutzt, teils partitioniert.
- Auf ISA-Ebene besitzt X86-64 nur 16 allgemeine Integer-Register, auf Mikroarchitekturebene gibt es jedoch Hunderte physischer Integer-Register und eine ähnliche Zahl von Gleitkommaregistern.
- In SMT-fähigen Prozessoren werden physische Register gleichmäßig auf die beiden logischen Prozessoren aufgeteilt.
- Auch load buffer und store buffer für Speicherlese- und Schreiboperationen werden gleichmäßig auf die beiden logischen Prozessoren aufgeteilt.
Register-Renaming, Scheduling und Commit
- Für Out-of-Order-Ausführung führt das Backend Register-Renaming durch.
- Da es auf ISA-Ebene nur wenige Architekturregister gibt, verwenden Programmbefehle dieselben Register in mehreren unabhängigen Befehlen wieder.
- Die Out-of-Order-Engine ersetzt das ursprüngliche logische Register durch eines der physischen Register und ermöglicht so frühere sowie parallele Ausführung.
- Diese Zuordnung wird in der register alias table, kurz RAT, verwaltet.
- Da die beiden logischen Prozessoren jeweils eigene Sätze von Architekturregistern besitzen, haben sie auch jeweils eine eigene Kopie der RAT.
- Befehle, die Register-Renaming und allocator-Stufen durchlaufen haben, gelangen in Bereitschaftswarteschlangen.
- Eine Queue ist für Speicherlese- und Schreibbefehle vorgesehen, die andere für allgemeine Befehle.
- In SMT-fähigen Kernen werden diese Queues gleichmäßig auf die beiden logischen Prozessoren aufgeteilt.
- Der Prozessor besitzt mehrere instruction scheduler, die parallel arbeiten.
- In jedem CPU-Zyklus wird ein Teil der Befehle aus den Bereitschaftsqueues an Scheduler weitergegeben.
- Die Queue sendet in einem Zyklus Befehle eines logischen Prozessors und wechselt im nächsten Zyklus zum anderen logischen Prozessor.
- Der Scheduler kümmert sich nicht um den logischen Prozessor, sondern schickt Mikrobefehle, deren benötigte Operanden und Ausführungseinheiten bereit sind, sofort zur Ausführung.
- Aus Fairness-Gründen ist die Zahl der active entries begrenzt, die ein logischer Prozessor innerhalb der scheduler queue besitzen darf.
- Die Ergebnisse fertig ausgeführter Befehle gelangen in den reorder buffer.
- Auch wenn Befehle out of order ausgeführt werden, müssen sie in der ursprünglichen Programmreihenfolge in den Architekturzustand des Prozessors übernommen werden.
- In SMT-fähigen Kernen wird der reorder buffer gleichmäßig auf die beiden logischen Prozessoren aufgeteilt.
- Die retirement unit verfolgt, ob Befehle bereit sind, in den Architekturzustand übernommen zu werden, und retired sie in der korrekten Programmreihenfolge.
- In SMT-fähigen Kernen verarbeitet sie abwechselnd Mikrobefehle der einzelnen logischen Prozessoren.
- Wenn ein logischer Prozessor keine Mikrobefehle zum Retiren hat, wird die gesamte Bandbreite für den anderen logischen Prozessor genutzt.
- Nach dem Retiren eines Befehls muss unter Umständen in den L1 Cache geschrieben werden; auch diese Schreibauswahl-Logik wechselt in jedem Zyklus zwischen den beiden logischen Prozessoren.
Speichersubsystem und Cache-Effekte
- Der TLB, der virtuelle Adressen von Datenanforderungen in physische Adressen übersetzt, wird von den beiden logischen Prozessoren je nach Bedarf dynamisch gemeinsam genutzt.
- TLB-Einträge werden mit einer logical processor id markiert, um Einträge der beiden logischen Prozessoren zu unterscheiden.
- Jeder CPU-Kern besitzt seinen eigenen privaten L1 Cache.
- Der L2 Cache kann je nach Mikroarchitektur privat sein oder zwischen Kernen geteilt werden.
- Falls ein L3 Cache vorhanden ist, wird er zwischen Kernen geteilt.
- Caches sind sich der Existenz logischer Prozessoren nicht bewusst.
- Da der L1 Cache und je nach Fall auch der L2 Cache kernprivat sind, enthalten sie bei Bedarf Daten beider logischer Prozessoren gemeinsam.
- Wenn zwei Threads den Cache aggressiv nutzen, können Datenkonflikte und Evictions auftreten und die Performance sinken.
- Wenn zwei Threads mit demselben Datensatz arbeiten, kann der gemeinsam genutzte Cache die Performance erhöhen.
Entscheidungskriterien bei Performance und Sicherheit
- Selbst wenn auf einem SMT-fähigen Kern nur ein Thread läuft, bleiben viele Puffer und Ausführungsressourcen zwischen den beiden logischen Prozessoren geteilt oder partitioniert, was die potenzielle Single-Thread-Performance verringern kann.
- Auf dem nicht genutzten logischen Prozessor führt das Betriebssystem eine idle loop aus; auch diese Schleife kann Ressourcen verbrauchen, die der andere logische Prozessor für maximale Performance nutzen könnte.
- Bei Intel-Core-Prozessoren scheint es keine Ressourcenteilung oder -partitionierung zu geben, wenn auf einem Kern nur ein Thread läuft; Intel behandelt dies als Verbesserung, die in dieser Generation eingeführt wurde.
- Wenn zwei Threads auf den beiden logischen Prozessoren eines SMT-fähigen Kerns laufen, bestimmt das Cache-Zugriffsmuster die Performance.
- Wenn zwei Threads um den Cache konkurrieren, evicten sie gegenseitig ihre Daten und die Performance sinkt.
- Wenn sie kooperieren, etwa indem ein Thread Daten produziert, die der andere konsumiert, kann die gemeinsame Nutzung von Cachedaten die Performance verbessern.
- Wenn zwei Threads den Cache nicht konkurrierend nutzen, können sie die Auslastung der CPU-Kernressourcen erhöhen, ohne die Performance des jeweils anderen zu beeinträchtigen.
- Viele Fachleute sind der Ansicht, dass Programme, die absolute Maximalleistung benötigen, SMT besser deaktivieren sollten, damit ein einzelner Thread alle Ressourcen nutzen kann.
- SMT bringt auch Sicherheitsprobleme mit sich.
- Gemeinsam genutzte Ressourcen und spekulative Ausführung können die Möglichkeit eröffnen, dass sensible Daten an Angreifer durchsickern.
- Dokumente von Oracle Linux und Red Hat werden als Beispiele für SMT-bezogene Sicherheitsprobleme verlinkt.
- Die allgemeine Empfehlung geht dahin, SMT auf dem System zu deaktivieren.
- Es gibt auch Gerüchte, dass Intel Hyper-Threading in der nächsten Prozessorgeneration Arrow Lake entfernen könnte.
1 Kommentare
Meinungen auf Hacker News
Wenn man SMT stark vereinfacht versteht, leuchtet ein, dass es die wertvollen ALUs weiter beschäftigen kann, während ein Thread wegen eines Cache Miss angehalten ist.
LPDDR in älteren Laptops war langsamer und es gab weniger Kerne, daher dürfte der Nutzen damals größer gewesen sein; heute hat man aber oft mehr Kerne als skalierbare Arbeit, sodass der Wert weniger spürbar ist.
Man vermeidet auch Cache-Konkurrenz, indem man Aufgaben nicht auf denselben Kern wie einen wichtigen Thread legt, weil man weiß, dass die Single-Thread-Leistung der Flaschenhals ist.
Früher habe ich beim Multithread-Rendering von DirectX 12 Efficient-/Performance-Cores und SMT-Cores getestet: Auf einem i7-12700K waren die Renderzeiten komplexer Szenen mit nur P-Cores, P+SMT sowie P+E+SMT fast gleich. Auf der Xbox Series X war derselbe Test jedoch etwas schneller, wenn auch SMT mit Arbeit belegt wurde.
SMT glänzt, wenn auf Ein-/Ausgabe gewartet wird oder einfache Integer-Arbeit anfällt. Wenn beide Threads die FPU auslasten können, wird SMT meist langsamer, weil zusätzliches Tagging nötig ist, um interne CPU-Daten zuzuordnen.
Der Kernpunkt sind weniger Cache Misses, sondern dass der Core andere Dinge ausführen kann, während Schreibvorgänge abgeschlossen werden.
Deshalb skaliert mancher Code schlecht, während anderer Code nahezu lineare Geschwindigkeitsgewinne erzielt.
Vielleicht ist es einfach besser, solche Prozessoren zwischendurch kurz ein Nickerchen machen zu lassen.
Busbreite, Transfers pro Sekunde, Queuing und die Energie pro übertragenem bzw. gespeichertem Bit wurden verbessert, aber wenn ein Programm Daten braucht, die nicht im Cache liegen, und auch die Vorhersage danebenliegt, ist am Ende die RAM-Latenz das Problem.
Intels nächste Arrow-Lake-CPUs sollen Hyper-Threading, also SMT, vollständig entfernen.
Da der Leistungsgewinn schon immer stark von der Anwendung abhing, könnte Vereinfachung besser sein.
Eine aktuelle Diskussion dazu, wann und wo es sinnvoll ist, gibt es hier: https://news.ycombinator.com/item?id=39097124
Ich bin noch nicht sicher, dass wir diesen Punkt schon erreicht haben, aber Intels P-/E-Cores sind eine Alternative mit ähnlichem Ziel und wirken auf Desktops mit vielen Single- und Low-Thread-Workloads ziemlich plausibel. Es scheint auch wertvoll zu sein, bei der Anwendungsoptimierung nicht zwischen SMT und E-Cores unterscheiden zu müssen.
AMD hingegen will vorerst weitgehend homogene Kerne beibehalten und SMT weiter nutzen. Welche Strategie tatsächlich besser ist, dürfte je nach Anwendung stark variieren und schwer pauschal zu beurteilen sein.
Deshalb habe ich beschlossen, die Thread-Zahl auf
std::thread::hardware_concurrency() / 2 - 1, also Anzahl der Kerne - 1, zu begrenzen. Ich arbeite mitstd::vector.Selbst wenn es gut funktionierte, lag der Zugewinn nur im niedrigen zweistelligen Prozentbereich, und es gab aufeinanderfolgende Generationen, in denen es schlechter war; ich weiß nicht, warum man es weiter versucht.
Ich bin jedes Mal erstaunt, wenn ich lese, wie solche Low-Level-CPU-Funktionen arbeiten.
An der Uni habe ich eine Vorlesung gehört, die etwa „Einführung in Computerhardware“ hieß, tatsächlich aber „Einführung in CPU-Design“ hätte heißen sollen. Wir bauten aus Logikgattern Addierer, Latches, Flipflops und Ähnliches, und am Ende des Semesters konnten wir auf Gatterebene einen sehr einfachen Prozessor entwerfen.
Aber sich Dinge wie Register Renaming oder Out-of-Order Execution auszudenken, kann ich mir kaum vorstellen. Entwirft man so etwas auch auf Gatterebene? Oder gibt es eine Sprache dafür und eine Art „Compiler“, der Gatter und Transistoren platziert?
Alle Aufgaben haben wir in einer Hardwarebeschreibungssprache namens Verilog gemacht, wodurch man mehrere Elemente abstrahiert beschreiben konnte.
Ein großes Missverständnis, das Nutzer bei SMT häufig haben, ist das mentale Modell, es gebe einen „echten Core“ und daneben einen minderwertigen Core.
In allen beobachtbaren Aspekten sind die beiden Threads gleichwertig.
Wenn man nur auf die Leistung schaut, entspricht es konzeptionell eher etwa 1,25 Single-Thread-Cores oder, je nach Anwendung, einem entsprechenden Anteil davon.
Meiner Ansicht nach wurden E-Cores vor allem wegen Wärmeentwicklung und Die-Fläche eingeführt, nicht primär wegen Stromverbrauch oder Performance. Deshalb kaufe ich immer Chips ohne E-Cores; ich halte die für besser.
Ich frage mich, wie man nach solchen detaillierten technischen Artikeln suchen sollte.
Ich habe genau zu diesem Thema gesucht, aber wie zu erwarten war, handelt es sich um Endnutzer-Technik, und die Suchergebnisse bestehen nur aus Anwenderartikeln, die nichts wirklich erklären.
Die angegebenen URLs sind allerdings meist Halluzinationen.
Ich habe es in einem neuen privaten Firefox-Tab auf einem anderen Gerät geprüft; Tracking oder Caching lässt sich damit zwar nicht vollständig ausschließen, aber ich halte es für eine ziemlich vernünftige Annäherung.
Die Erklärung, dass „bei einem CPU-Kern mit aktiviertem SMT viele Puffer und Ausführungsressourcen von zwei logischen Prozessoren geteilt werden müssen, sodass diese Ressourcen selbst dann, wenn auf einem SMT-Kern nur ein Thread läuft, für diesen Thread nicht nutzbar sind und die potenzielle Leistung sinkt“, stimmt heute nicht mehr.
Im SMT-Modus werden zwar ROB, Fetch-/Decode-Bandbreite usw. aufgeteilt, aber ich habe verschiedene SMT-Kerne gesehen, die im Nicht-SMT-Betrieb alles vollständig nutzbar machen.
Der zentrale Zweck von SMT ist es, die Auslastung der superskalaren Execution Engine zu maximieren.
Ich frage mich, ob diese Entwicklung bedeutet, dass die Leute Superskalarität nicht mehr für so wichtig halten wie früher.
Insgesamt eine gute Zusammenfassung, aber stellenweise wirkte sie etwas vermischt.
Ich würde gern mehr Praxis-Kniffe kennen, die Insider verwenden, selbst wenn sie nichts mit Sicherheit zu tun haben.
Die arme Bulldozer-Architektur von AMD wurde so sehr dafür kritisiert, kein SMT zu haben, und jetzt wenden sich alle von SMT ab.
Natürlich weiß ich, dass Bulldozer weit mehr Probleme hatte als nur das fehlende SMT. Tatsächlich war es eher fast das genaue Gegenteil: mehrere Kerne teilten sich Dinge wie dieselbe ALU. Trotzdem: Wenn man nur etwas mehr Performance herausgeholt hätte, hätte man vielleicht in gewisser Weise etwas vorausgesehen.
Power10 unterstützt derzeit effektiv SMT8 mit 8 Threads pro Kern, und angesichts der jahrelangen Weiterentwicklung SMT-zentrierter Designs glaube ich nicht, dass sie das aufgeben werden.
Bemerkenswert ist, dass auch die Recheneinheiten von GPUs typischerweise SMT in einer Größenordnung von 7 bis 10 Threads pro CU verwenden.
Das hilft dabei, Latenzen zu verbergen.
In jedem Taktzyklus wird aus mehreren verfügbaren Threads eine Instruktion ausgewählt und gestartet, die nicht ausgelastete Ressourcen benötigt. Die meisten GPUs starten nicht mehrere Instruktionen pro Takt; mehrere Instruktionen können nach dem Start aber gleichzeitig weiterlaufen. Selbst wenn mehrere Instruktionen pro Takt gestartet werden, müssen sie möglicherweise zu getrennten Instruktionsklassen gehören, die unterschiedliche Ausführungsressourcen nutzen, etwa skalare und Vektor-Instruktionen.
SMT, also simultanes Multithreading, bedeutet, dass in jedem Taktzyklus aus allen Threads viele Instruktionen gleichzeitig gestartet werden und diese Instruktionen um die mehreren Ausführungseinheiten einer superskalaren CPU konkurrieren, um möglichst viele Ausführungseinheiten beschäftigt zu halten. Für jede parallele Ausführungseinheit, etwa jede der sechs Integer-Addierer einer modernen CPU, wird separat entschieden, welche Instruktion aus einer Queue mit Instruktionen aller gleichzeitigen Threads ausgeführt wird.