Bernd Leitenbergers Blog

Funktionelle Einheiten, Vektorinstruktionen und Mehrkernprozessoren

Da wir alle drei Konzepte heute in den Intelprozessoren und auch einigen AMD-Typen finden, mal eine historische Betrachtung was die Unterschiede sind und was wo nützt. Das älteste sind mehrere funktionelle Einheiten. Dazu hole ich erst mal aus und erkläre wie eine CPU allgemein aufgebaut ist. Man kann sie selbst bei einfachen Exemplaren, also z.B. einem 8-Bit Mikroprozessor in mehrere Untereinheiten zerlegen die jeweils eine Aufgabe haben. Da ist zum einen der Befehlsdekoder. Er stellt fest welcher Befehl hinter dem Bitmuster steckt, das gerade geladen wird und welche Register er nutzt. Die Daten selbst bekommt er von einer Einheit die mit dem Speicher kommuniziert, je nach Architektur Load/Store Einheit, Bus-Interface etc. genannt. Dann gibt es noch mindestens eine Ausführungseinheit welche die Befehle auch durchführt, bei einfachen Prozessoren meistens als Arithmetisch-logische Einheit (ALU) bezeichnet. Das ist nur die Grundmenge. Es können durchaus mehrere dieser Einheiten sein.

Schon bald kam man drauf diese Einheiten zumindest zeitweise parallel arbeiten zu lassen. Ein Befehl wird z.B. erst von der Load/Store Einheit aus dem Speicher geholt, durchläuft dann den Befehlsdekoder und dann die Ausführungseinheit. Schon bei der 8086 CPU war die Bus-Interface Unit schon fähig während Decoder und ALU arbeiteten die nächsten Bytes vorrausschauend aus dem Speicher zu laden, das nennt man übrigens Prefetch. Der rapide Geschwindigkeitseinbruch der 8088 CPU kam dadurch zustande, dass neben den Befehlen auch noch Daten von dieser Einheit geholt und geschrieben wurden und durch die halbierte Busbreite kam sie kaum noch dazu den Prefetch durchzuführen, da jeder Transfer nun doppelt so lange dauerte. Auch bei der IBM 7030 Stretch gab es schon Parallelität von Laden/Speichern und ausführen/Dekodieren.

Allerdings brauchen viele Befehle die meisten Taktzyklen beim Ausführen. Das brachte Seymour Cray auf die Idee die Ausführungseinheiten mehrmals vorzusehen. Die CDC 6600 hatte nicht weniger als zehn davon. Sie waren allerdings relativ spezialisiert so gab es eigene Einheiten für Fließkommaoperationen und Ganzzahloperationen und eine Fließkommaeinheit für Addition/Subtraktion, eine für Multiplikation und eine für Division. Der Lohn die theoretische Spitzenleistung war etwa um den Faktor 3 höher als ohne diese mehrfach vorhandenen Einheiten.

Intel führte das beim Pentium ein, der zwei Integerrecheneinheiten hatte (die Fleißkommaeinheit war nur einmal vorhanden). Bei der Ausführung gibt es nun ein Problem, das schon bei der CDC 6600 zuschlug: der Code enthält Instruktionen, die sind unabhängig voneinander und andere bauen aufeinander auf. Also in einer Operation wird ein Ergebnis errechnet und in der nächsten wird mit diesem weiter gerechnet. Das ergibt sich schon daraus das nur eine Rechenoperation pro Befehl möglich ist. Eine einfache quadratische Gleichung wie

Y = x * x + 2 * x + 4

enthält so also vier Operationen von denen immerhin x * x und 2 * x gleichzeitig ablaufen können. Das Addieren aller drei Therme geht aber nicht parallel. Beim Pentium war dies spürbar. Mit bestehendem Code für das Vorgängermodell war er 1,6 mal schneller als dieses. Achtete ein Compiler darauf, Befehle möglichst so anzuordnen das es keine Abhängigkeiten gab, so erreichte er die 2,3-fache Geschwindigkeit. Später integrierte man dies im Prozessor und nannte das Umsortieren der Befehle „Out-of-Order Execution“. Ein Haswell-EP Prozessor hat heute vier Integerrecheneinheiten und drei Fließkommarecheneinheiten. Die Zahl ist über 20 Jahre kaum angestiegen, das liegt daran, dass es in Code schwer fällt so viele nicht voneinander abhängige Rechnungen kurz hintereinander zu finden. Mehr Einheiten würden dann nicht ausgelastet werden. Auch Seymour Crays Rechner hatten maximal zwei Einheiten desselben Typs bzw. für dieselben Operanden (er trennte die Register für Ganzzahlen, Fließkommazahlen und Adressen in drei Registersätze).

Historisch als zweites kam dann das Konzept mehrere CPU einzusetzen. Der Unterschied zu mehreren funktionellen Einheiten ist das eine CPU komplett ist, also auch Befehlsdekoder und Anbindung an den Speicher umfasst. Der wesentliche Vorteil einer Mehrkern CPU liegt vor allem bei Multitasking Systemen, die schon in den Sechzigern entwickelt wurden. Dabei liefen mehrere Programme gleichzeitig. oder ein Rechner bediente mehrere Benutzer. Doch auch bei nur einem Programm bringen mehrere CPUs einen Vorteile, wenn große Teile unabhängig von anderen sind. Bei vielen naturwissenschaftlichen Problemstellungen ist dies gegeben. Da wird eine Rechenvorschrift (Programm) mit unterschiedlichen Daten durchgerechnet, die dann ein zwei oder mehrdimensionales Modell ergeben. Dann läuft ein Teil dieser Berechnungen auf einer CPU, der andere auf der anderen. Sie müssen aber miteinander kommunizieren, weil es natürlich am Rand beider Mengen Daten gibt die zwischen den beiden Teilen ausgetauscht werden müssen.

Ein weiteres Problem, das wohl dazu führte das Seymour Cray relativ spät auf diese Karte setzte, ist das der Speicher meist gemeinsam genutzt wird. Dann können sich mehrere CPU gegenseitig blockieren oder zumindest ausbremsen und auch die Bandbreite, also Transferrate wird meist aufgeteilt. Dabei ist ein Grundübel von Speicher, seit er eingeführt wurde das, das er immer langsamer als die CPUs ist.

Der letzte Schritt bei Intel sind Vektorinstruktionen. Bei Crays Rechner waren sie der zweite und das liegt daran dass diese die oben beschriebenen naturwissenschaftlichen Probleme durchrechneten. Wie erwähnt ist die Rechenvorschrift immer dieselbe aber die Daten sind immer andere. Was liegt näher einen Befehl zu entwerfen, der dann nicht zwei Zahlen verarbeitet sondern viel mehr. Bei jedem Takt wird ein Paar verarbeitet bis bei einer Cray 1 maximal 64 Ergebnisse (andere Vektorrechner bis zu 1024!) vorliegen. Der Vorteil ist das die Dekodierung aber auch das Holen der Daten und Scheiben nur einmal erfolgte. Auch die Rechenwerke waren so ausgelegt das sie jeden Takt eine neue Rechnung anstießen, auch wenn diese einige Takte brauchte bis sie das Rechenwerk durchlaufen hatte. Es konnte sogar parallelisiert werden, man musste also nicht warten, bis 64 Werte in den Registern waren sondern konnte nach zwei Werten anfangen während gleichzeitig das Holen der Daten in die anderen Register weiterging.

Bei Intel ist die Konzeption eine andere. In der Form wie Cray sie implementierte bringt sie keinen Performancevorteil, da mittlerweile auch komplexe Rechnungen in einem Takt durchgeführt werden. Stattdessen hat bei AVX, bzw. SSE ein Befehl mehrere Datenworte (Single Instruction, Multiple Data) die in überlangen Registern (bis zu 256 Bit obwohl Fließkommazahlen nur 64 oder 32 Bit breit sind) parallel verarbeitet werden. Anstatt zwei Fließkommazahlen werden so zweimal vier oder zweimal acht auf einaml verarbeitet.

Doch das geht meist nur wenn der Code stimmt. Selbst heute sind Compiler sehr dumm. Das ist ein Ausschnitt aus der Berechnung der Gravitationswirkung von Körpern gegeneinander:

for I:=low(SSystem) to high(SSystem) do
begin
  SSystem[I].Ax:=0;
  SSystem[I].Ay:=0;
  SSystem[I].Az:=0;
  J:=0;
  while J<=high(SSystem) do
  begin
    if I<>J then
    begin
      Dx:=SSystem[I].Pos.Px-SSystem[J].Pos.Px;
      Dy:=SSystem[I].Pos.Py-SSystem[J].Pos.Py;
      Dz:=SSystem[I].Pos.Pz-SSystem[J].Pos.Pz;
      D2:=Sqr(Dx)+Sqr(Dy)+Sqr(Dz);
      D:=Sqrt(D2);
      G:=(SSystem[J].Masse*Grav)/D2;
      SSystem[I].Ax:=SSystem[I].Ax+(G*Dx/D);
      SSystem[I].Ay:=SSystem[I].Ay+(G*Dy/D);
      SSystem[I].Az:=SSystem[I].Az+(G*Dz/D);
    end;
  end;
end;

Dieser Code ist eigentlich super geeignet sowohl für mehrere Funktionseinheiten (unabhängige Berechnung von DX,DY,DZ und AX,AY,AZ). Wie auch mehrere Kerne (so kann man einen Lauf von 1 bis n/2 und einen von n/2+1 bis n aufteilen). Auch Vektoroperationen sind möglich, denn es sind in allen Durchgängen die selben 9 Berechnungen. Compiler sind aber noch heute so dämlich, dass man, damit sie die Instruktionen nutzen, dann jede Berechnung in eine Schleife packen muss also so umschreiben:

      while J<=high(SSystem) do
      Dx[j]:=SSystem[I].Pos.Px-SSystem[J].Pos.Px;
      J:=0;
      while J<=high(SSystem) do
      Dy[j]:=SSystem[I].Pos.Py-SSystem[J].Pos.Py;
      J:=0;
      while J<=high(SSystem) do
      Dz[j]:=SSystem[I].Pos.Pz-SSystem[J].Pos.Pz;

Das bedeutet man muss auch die eigentlich nur kurzzeitig als Zwischenwerte benötigten Variablen DX, DY und DZ in einem Array speichern. Es verwundert nicht, dass von dem Performanceboost von AVX, der bei dem achtfachen der normalen Geschwindigkeit liegen sollte wenig in realen Anwendungen übrig bleibt. Vor allem aber dominieren diese Anwendungen nicht bei den PC-Usern, sie profitieren eher von mehr Kernen, noch mehr aber von mehr Takt da typisch ein Thread sehr viel Performance braucht und andere im Hintergrund mit weniger Leistung auskommen. Daher führte Intel noch vor AVX den Turbo-boost ein. Das ist das „Übertakten“ eines Kerns während die anderen wenig zu tun haben. Er nutzt damit das Thermalbudget aller Kerne aus. Wird ein Kern höher getaktet so steigt die Wärmeabgabe meist stark an. Bei Supercomputern finden diese Erweiterungen dagegen ihren Einsatz. Allerdings haben sie dort Konkurrenz von GPUs bekommen die das Problem anders lösen: sie setzen einige Hunder bis 1000 einfache Recheneinheiten auf genauso viele Zahlenpaare an.  Ihre Leistung ist daher erheblich höher, auch wenn sie schwerer zu programmieren sind und der Speicher noch mehr zum Flaschenhlas wird.

Die mobile Version verlassen