it-swarm.com.de

Wie läuft ein einzelner Thread auf mehreren Kernen?

Ich versuche auf hoher Ebene zu verstehen, wie einzelne Threads über mehrere Kerne laufen. Unten ist mein bestes Verständnis. Ich glaube jedoch nicht, dass es richtig ist.

Basierend auf meiner Lektüre von Hyper-Threading scheint das Betriebssystem die Anweisungen aller Threads so zu organisieren, dass sie nicht aufeinander warten. Dann organisiert das Front-End der CPU diese Anweisungen weiter, indem es einen Thread auf jeden Kern verteilt und unabhängige Anweisungen von jedem Thread auf alle offenen Zyklen verteilt.

Wenn es also nur einen einzigen Thread gibt, führt das Betriebssystem keine Optimierung durch. Das Front-End der CPU verteilt jedoch unabhängige Befehlssätze auf jeden Kern.

Laut https://stackoverflow.com/a/1593627 kann eine bestimmte Programmiersprache mehr oder weniger Threads erstellen, dies ist jedoch irrelevant, wenn festgelegt wird, was mit diesen Threads geschehen soll. Das Betriebssystem und die CPU übernehmen dies, sodass dies unabhängig von der verwendeten Programmiersprache geschieht.

(enter image description here

Zur Verdeutlichung frage ich nach einem einzelnen Thread, der über mehrere Kerne ausgeführt wird, und nicht nach der Ausführung mehrerer Threads auf einem einzelnen Kern.

Was ist los mit meiner Zusammenfassung? Wo und wie werden die Anweisungen eines Threads auf mehrere Kerne aufgeteilt? Ist die Programmiersprache wichtig? Ich weiß, dass dies ein weites Thema ist. Ich hoffe auf ein umfassendes Verständnis davon.

65
Evorlor

Das Betriebssystem bietet Zeitscheibe s CPU für Threads an, die zur Ausführung berechtigt sind.

Wenn nur ein Kern vorhanden ist, plant das Betriebssystem den am besten geeigneten Thread für die Ausführung auf diesem Kern für eine Zeitscheibe. Nach Abschluss einer Zeitscheibe oder wenn der laufende Thread auf E/A blockiert oder wenn der Prozessor durch externe Ereignisse unterbrochen wird, bewertet das Betriebssystem neu, welcher Thread als Nächstes ausgeführt werden soll (und es könnte denselben oder einen anderen Thread erneut auswählen).

Die Berechtigung zum Ausführen besteht aus Variationen in Bezug auf Fairness, Priorität und Bereitschaft, und durch diese Methode erhalten verschiedene Threads Zeitscheiben, einige mehr als andere.

Wenn mehrere Kerne vorhanden sind, N, plant das Betriebssystem die am besten geeigneten N Threads, die auf den Kernen ausgeführt werden sollen.

Prozessoraffinität ist eine Effizienzüberlegung. Jedes Mal, wenn eine CPU einen anderen Thread als zuvor ausführt, wird sie etwas langsamer, da der Cache für den vorherigen Thread warm, für den neuen jedoch kalt ist. Das Ausführen desselben Threads auf demselben Prozessor über mehrere Zeitscheiben hinweg ist daher ein Effizienzvorteil.

Es steht dem Betriebssystem jedoch frei, Zeitscheiben für einen Thread auf verschiedenen CPUs anzubieten, und es kann sich durch alle CPUs auf verschiedenen Zeitscheiben drehen. Es kann jedoch nicht, wie @ gnasher729 sagt , einen Thread auf mehreren CPUs gleichzeitig ausführen.

Hyperthreading ist eine Methode in der Hardware, mit der ein einzelner erweitert CPU-Kern die Ausführung von zwei oder mehr nterstützen kann. -different Threads gleichzeitig. (Eine solche CPU kann zusätzliche Threads zu geringeren Kosten in Siliziumimmobilien anbieten als zusätzliche Vollkerne.) Dieser erweiterte CPU-Kern muss zusätzlichen Status für die anderen Threads unterstützen, z. B. CPU-Registerwerte, und weist auch einen Koordinationsstatus und ein Koordinationsverhalten auf Ermöglicht die gemeinsame Nutzung von Funktionseinheiten innerhalb dieser CPU, ohne die Threads zusammenzuführen.

Hyperthreading ist zwar aus Hardware-Sicht technisch herausfordernd, aus Sicht des Programmierers jedoch eher das Modell zusätzlicher CPU-Kerne als etwas Komplexeres. Das Betriebssystem sieht also zusätzliche CPU-Kerne, obwohl es einige neue Probleme mit der Prozessoraffinität gibt, da mehrere Threads mit Hyperthread die Cache-Architektur eines CPU-Kerns gemeinsam nutzen.


Wir könnten naiv denken, dass zwei Threads, die auf einem Hyperthread-Kern laufen, jeweils halb so schnell laufen wie jeder mit seinem eigenen vollen Kern. Dies ist jedoch nicht unbedingt der Fall, da die Ausführung eines einzelnen Threads voller Durchhangzyklen ist und einige davon vom anderen Thread mit Hyperthread verwendet werden können. Selbst während nicht lockerer Zyklen kann ein Thread andere Funktionseinheiten als der andere verwenden, so dass eine gleichzeitige Ausführung erfolgen kann. Die erweiterte CPU für Hyperthreading verfügt möglicherweise über einige weitere häufig verwendete Funktionseinheiten, die speziell dafür unterstützt werden.

84
Erik Eidt

Es gibt keinen einzelnen Thread, der gleichzeitig auf mehreren Kernen ausgeführt wird.

Dies bedeutet jedoch nicht, dass Anweisungen von einem Thread nicht parallel ausgeführt werden können. Es gibt Mechanismen, die als Anweisungs-Pipelining und Ausführung außerhalb der Reihenfolge bezeichnet werden erlaube es. Jeder Kern verfügt über viele redundante Ressourcen, die nicht von einfachen Anweisungen verwendet werden, sodass mehrere solcher Anweisungen zusammen ausgeführt werden können (solange die nächste nicht vom vorherigen Ergebnis abhängt). Dies geschieht jedoch immer noch innerhalb eines einzelnen Kerns.

Hyper-Threading ist eine Art extreme Variante dieser Idee, bei der ein Kern nicht nur Anweisungen von einem Thread parallel ausführt, sondern auch Anweisungen von zwei verschiedenen Threads mischt, um die Ressourcennutzung noch weiter zu optimieren.

Verwandte Wikipedia-Einträge: Instruction Pipelining , Ausführung außerhalb der Reihenfolge .

24
Frax

zusammenfassung: Das Finden und Ausnutzen der Parallelität (Befehlsebene) in einem Single-Thread-Programm erfolgt ausschließlich in Hardware durch den CPU-Kern, auf dem es ausgeführt wird. Und nur über ein Fenster von ein paar hundert Anweisungen, keine groß angelegte Nachbestellung.

Single-Thread-Programme profitieren nicht von Multi-Core-CPUs, außer dass other Dinge auf den anderen Kernen ausgeführt werden können, anstatt sich Zeit für die Single-Thread-Aufgabe zu nehmen.


das Betriebssystem organisiert die Anweisungen aller Threads so, dass sie nicht aufeinander warten.

Das Betriebssystem schaut NICHT in die Anweisungsströme von Threads. Es werden nur Threads für Kerne geplant.

Tatsächlich führt jeder Kern die Scheduler-Funktion des Betriebssystems aus, wenn er herausfinden muss, was als nächstes zu tun ist. Scheduling ist ein verteilter Algorithmus. Stellen Sie sich zum besseren Verständnis von Multi-Core-Maschinen vor, dass jeder Core den Kernel separat ausführt. Genau wie bei einem Multithread-Programm ist der Kernel so geschrieben, dass sein Code auf einem Kern sicher mit seinem Code auf anderen Kernen interagieren kann, um gemeinsam genutzte Datenstrukturen (wie die Liste der ausführbaren Threads) zu aktualisieren.

Auf jeden Fall ist das Betriebssystem daran beteiligt, Multithread-Prozesse dabei zu unterstützen, Parallelität auf Thread-Ebene auszunutzen, die durch manuelles Schreiben eines Multithread-Programms explizit verfügbar gemacht werden muss. (Oder von einem automatisch parallelisierenden Compiler mit OpenMP oder so).

Dann organisiert das Front-End der CPU diese Anweisungen weiter, indem es einen Thread auf jeden Kern verteilt und unabhängige Anweisungen von jedem Thread auf alle offenen Zyklen verteilt.

Ein CPU-Kern führt nur einen Befehlsstrom aus, wenn er nicht angehalten wird (bis zum nächsten Interrupt, z. B. Timer-Interrupt, eingeschlafen). Oft ist das ein Thread, aber es kann auch ein Kernel-Interrupt-Handler oder ein anderer Kernel-Code sein, wenn der Kernel nach der Behandlung und Unterbrechung oder dem Systemaufruf beschlossen hat, etwas anderes zu tun, als nur zum vorherigen Thread zurückzukehren.

Bei HyperThreading oder anderen SMT-Designs verhält sich ein physischer CPU-Kern wie mehrere "logische" Kerne. Der einzige Unterschied aus Sicht des Betriebssystems zwischen einer Quad-Core-CPU mit Hyperthreading (4c8t) und einer einfachen 8-Core-Maschine (8c8t) besteht darin, dass ein HT-fähiges Betriebssystem versucht, Threads so zu planen, dass sie physische Kerne trennen, damit sie nicht. nicht miteinander konkurrieren. Ein Betriebssystem, das nichts über Hyperthreading wusste, würde nur 8 Kerne sehen (es sei denn, Sie deaktivieren HT im BIOS, dann würde es nur 4 erkennen).


Der Begriff " Front-End" bezieht sich auf den Teil eines CPU-Kerns, der Maschinencode abruft, die Anweisungen decodiert und sie in den nicht ordnungsgemäßen Teil des Kerns ausgibt. . Jeder Kern hat sein eigenes Front-End und ist Teil des gesamten Kerns. Anweisungen, die es abruft are was die CPU gerade läuft.

Innerhalb des nicht ordnungsgemäßen Teils des Kerns werden Anweisungen (oder Uops) an Ausführungsports gesendet, wenn ihre Eingabeoperanden bereit sind und ein freier Ausführungsport vorhanden ist. Dies muss nicht in der Programmreihenfolge geschehen, daher kann eine OOO-CPU auf diese Weise die Parallelität auf Befehlsebene innerhalb eines einzelnen Threads ausnutzen.

Wenn Sie in Ihrer Idee "Kern" durch "Ausführungseinheit" ersetzen, sind Sie fast richtig. Ja, die CPU verteilt unabhängige Anweisungen/Uops parallel an Ausführungseinheiten. (Aber es gibt eine Terminologie-Verwechslung, da Sie "Front-End" gesagt haben, wenn es wirklich der Befehlsplaner der CPU, auch Reservierungsstation genannt, ist, der Befehle auswählt, die zur Ausführung bereit sind).

Die Ausführung außerhalb der Reihenfolge kann ILP nur auf einer sehr lokalen Ebene finden, nur bis zu ein paar hundert Anweisungen, nicht zwischen zwei unabhängigen Schleifen (es sei denn, sie sind kurz).


Zum Beispiel das asm-Äquivalent dazu

int i=0,j=0;
do {
    i++;
    j++;
} while(42);

läuft ungefähr so ​​schnell wie dieselbe Schleife und erhöht nur einen Zähler auf Intel Haswell. i++ hängt nur vom vorherigen Wert von i ab, während j++ hängt nur vom vorherigen Wert von j ab, sodass die beiden Abhängigkeitsketten parallel ausgeführt werden können, ohne die Illusion zu zerstören, dass alles in Programmreihenfolge ausgeführt wird.

Auf x86 würde die Schleife ungefähr so ​​aussehen:

top_of_loop:
    inc eax
    inc edx
    jmp .loop

Haswell verfügt über 4 ganzzahlige Ausführungsports, und alle verfügen über Addierereinheiten, sodass ein Durchsatz von bis zu 4 inc Anweisungen pro Takt aufrechterhalten werden kann, wenn alle unabhängig sind. (Mit Latenz = 1 benötigen Sie also nur 4 Register, um den Durchsatz zu maximieren, indem Sie 4 inc Anweisungen im Flug behalten. Vergleichen Sie dies mit Vektor-FP MUL oder FMA: Latenz = 5 Durchsatz = 0,5 benötigt 10 Vektorakkumulatoren 10 FMAs im Flug zu halten, um den Durchsatz zu maximieren. Und jeder Vektor kann 256b groß sein und 8 Floats mit einfacher Genauigkeit enthalten.

Der genommene Zweig ist auch ein Engpass: Eine Schleife benötigt immer mindestens einen ganzen Takt pro Iteration, da der Durchsatz des genommenen Zweigs auf 1 pro Takt begrenzt ist. Ich könnte einen weiteren Befehl in die Schleife einfügen, ohne die Leistung zu verringern, es sei denn, er liest/schreibt auch eax oder edx. In diesem Fall würde er diese Abhängigkeitskette verlängern. Das Einfügen von 2 weiteren Befehlen in die Schleife (oder eines komplexen Multi-Uop-Befehls) würde einen Engpass im Front-End verursachen, da nur 4 Uops pro Takt in den Kern außerhalb der Reihenfolge ausgegeben werden können. (Siehe this SO Q & A für einige Details darüber, was bei Schleifen passiert, die kein Vielfaches von 4 Uops sind: Der Schleifenpuffer und der UOP-Cache machen die Dinge interessant. )


In komplexeren Fällen erfordert das Finden der Parallelität das Betrachten eines größeren Anweisungsfensters . (z. B. gibt es möglicherweise eine Folge von 10 Anweisungen, die alle voneinander abhängen, dann einige unabhängige).

Die Kapazität des Nachbestellungspuffers ist einer der Faktoren, die die Fenstergröße außerhalb der Reihenfolge begrenzen. Bei Intel Haswell sind es 192 Uops. (Und Sie können sogar experimentell messen zusammen mit der Umbenennungskapazität des Registers (Größe der Registerdatei).) CPU-Kerne mit geringem Stromverbrauch wie ARM haben einen viel kleineren ROB) Größen, wenn sie überhaupt nicht in der richtigen Reihenfolge ausgeführt werden.

Beachten Sie auch, dass CPUs sowohl per Pipeline als auch außer Betrieb sein müssen. Daher müssen Anweisungen weit vor den ausgeführten Anweisungen abgerufen und dekodiert werden, vorzugsweise mit einem ausreichenden Durchsatz, um Puffer nach dem Fehlen von Abrufzyklen wieder aufzufüllen. Zweige sind schwierig, weil wir nicht einmal wissen, woher wir sie holen sollen, wenn wir nicht wissen, in welche Richtung ein Zweig gegangen ist. Deshalb ist die Verzweigungsvorhersage so wichtig. (Und warum moderne CPUs spekulative Ausführung verwenden: Sie raten, in welche Richtung ein Zweig gehen wird, und beginnen mit dem Abrufen/Dekodieren/Ausführen dieses Befehlsstroms. Wenn eine Fehlvorhersage erkannt wird, rollen sie in den letzten bekanntermaßen guten Zustand zurück und führen von dort aus.)

Wenn Sie mehr über CPU-Interna erfahren möchten, finden Sie im Stackoverflow x86-Tag-Wiki einige Links, einschließlich Agner Fogs Microarch-Handbuch und zu David Kanters detaillierten Beschreibungen mit Diagrammen von Intel- und AMD-CPUs. Aus seiner Intel Haswell Microarchitecture Writeup ist dies das endgültige Diagramm der gesamten Pipeline eines Haswell-Kerns (nicht des gesamten Chips).

Dies ist ein Blockdiagramm eines single CPU-Kerns . Eine Quad-Core-CPU verfügt über 4 davon auf einem Chip mit jeweils eigenen L1/L2-Caches (gemeinsame Nutzung eines L3-Caches, von Speichercontrollern und PCIe-Verbindungen zu den Systemgeräten).

(Haswell full pipeline

Ich weiß, dass dies überwältigend kompliziert ist. Kanters Artikel zeigt auch Teile davon, um beispielsweise getrennt von den Ausführungseinheiten oder den Caches über das Frontend zu sprechen.

23
Peter Cordes