Referat Vektorisierung auf pipeline-Rechnern am Beispiel der CRAY Y-MP von Markus Wegner und Michael Stange Hochschule Bremen RST-Labor Semester I7I1 Samstag, 14. Mai 2016 Markus Wegner, Michael Stange, I7I1 14.05.2016 Inhaltsverzeichnis Warum Vektorisierung? __________________________________________3 Geschichtlicher Überblick ________________________________________4 CRAY-1 ______________________________________________________________________ 4 CRAY-2 ______________________________________________________________________ 5 CRAY-XMP ___________________________________________________________________ 5 CRAY-YMP ___________________________________________________________________ 6 Architektur der CRAY-YMP 816 __________________________________6 Probleme bei der Vektorisierung ___________________________________7 Dependency Tests _______________________________________________9 Dependencies beheben __________________________________________10 Umordnen von Anweisungen ____________________________________________________ 10 Recurrence Threshold __________________________________________________________ 10 Save Vector Length ____________________________________________________________ 11 Loop Unrolling ________________________________________________________________ 11 Partielle Vektorisierung ________________________________________________________ 12 Bedingte Vektorisierung ________________________________________________________ 12 Loop Switching________________________________________________________________ 12 Matrix Multiplikation __________________________________________________________ 13 Performance Gewinn durch Vektorisierung (Amdahls Gesetz) _________14 Speichersysteme________________________________________________15 Realer Speicher _______________________________________________________________ 15 Memory Banks ________________________________________________________________ 17 Quellen _______________________________________________________18 __________________________________________________________________________________________ Seite 2 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Warum Vektorisierung? Bisher sind wir davon ausgegangen, daß wenige Eingabegrößen (Operanden) vorliegen, die schon zu Beginn des Programmablaufs abrufbereit in Registern bereitstehen. Wir hatten also das Laden der Operanden aus dem Speicher vollständig außer acht gelassen. Die Wichtigkeit dieses Problems illustriert das folgende Programmfragment: 10 DO 10 I = 1, 63 A(I) = B(I) + C(I) CONTINUE Wollte man hier für die Addition Pipelining ausnutzen, müßten vorher alle 126 B() und C() –Werte in Register geladen werden. Die CRAY besitzt aber nicht so viele Einzelregister zum Speichern von jeweils einer Zahl. Dafür besitzt sie Vektorregister, denen sie letztendlich den Namen Vektorrechner verdankt. In einem Vektorregister können mehrere Zahlen gleichzeitig gespeichert und direkt verarbeitet werden. Sie wurden gerade für die Verarbeitung solcher Schleifen konstruiert. Die genaue Funktionsweise wird am besten bei Betrachtung eines Beispiels deutlich (Abb. 1): Abb.1: Zeitablauf für die Addition zweier Vektoren Zuerst wird das gesamte Feld A() aus dem Speicher in ein Vektorregister geladen. Ein Vektorregister der CRAY kann 64 Elemente (Zahlen der Länge 8Byte) aufnehmen. Hierfür benötigt sie 17 Zyklen. Parallel dazu kann sie auch Feld B() laden, allerdings kann sie erst 3 Zyklen später beginnen, da diese Zeit benötigt wird, um die Startadresse von B() zu laden. Nach 20 Zyklen stehen die Zahlen bereit und werden an die „+“- Pipeline übergeben, die ja 6-stufig ist. Pipelining wird hier optimal genutzt, da die Vektorregister in jedem weiteren Taktzyklus je einen Operanden übertragen. Nach 26 Zyklen steht das erste Ergebnis bereit; zum Speichern werden weitere 3 Zyklen benötigt. Das erste Ergebnis verläßt also nach 3 + 17 + 6 + 3 = 29 Taktzyklen die CPU. Diese Überlegung stellt die Zusammenhänge vereinfacht dar, wie wir später noch sehen werden. In Abb.1 sind die Lade- und Speicherbefehle in der selben Weise gezeichnet wie Functional Units, was sie ja eigentlich nicht sind. Die Graphik soll nur den Zeitablauf verdeutlichen. Die Ladezeit von 17 Zyklen bedeutet, daß nach 17 Zyklen der erste Operand im Vektorregister zur Verfügung steht. In jedem weiteren Zyklus wird dann ein weiterer Operand geladen. Das Speichern geht schneller, weil es- aus der Sicht des Vektorregisters- reicht zu warten, bis der Operand das Register in Richtung Speicher verlassen hat. Zu diesem Zeitpunkt ist das Vektorregister wieder frei, obwohl der Operand noch nicht wieder im Speicher ist. Erst nach 17 Zyklen liegt dann das Ergebnis im Speicher vor. Wenn die gesamte Maschinerie, bestehend aus Speicherzugriffslogik, Vektorregistern und Functional Unit einmal in Gang gesetzt ist, „fließen“ die Daten so durch das System, daß in jedem Zyklus ein Schleifendurchlauf abgearbeitet wird. Erst durch die Vektorregister wird es möglich, die Functional Units so schnell mit Eingabedaten zu beliefern, daß Pipelining genutzt werden kann. Dies erfordert __________________________________________________________________________________________ Seite 3 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 wiederum, daß auch der Speicher in der Lage ist, die Operanden schnell genug an die Vektorregister weiterzugeben und die Ergebnisse entsprechend entgegen zu nehmen. In unserem Beispiel müssen in jedem Taktzyklus 3*8=24 Bytes bewegt werden. Geschichtlicher Überblick CRAY-1 Der CRAY Vektorrechner wurde Ende der 70er Jahre von Seymour CRAY konzipiert und verwirklicht. Abb.2: CRAY-1 Vektorrechner Als erste Vektor- Architektur mit Vektorregistern erschien die CRAY-1 auf dem Markt. Das vereinfachte Blockschaltbild der CRAY-1 ist in der Abb.2 dargestellt. Der Hauptspeicher ist in 16 Speicherbänke aufgeteilt mit einem Speicherzyklus von 50 ns (Maschienenzyklus 12 ns). Dieser Vektorrechner enthält 8 Vektorregister und 16 Mehrzweckregister, von denen jeweils 8 für die Abspeicherung von Daten (Skalare) und 8 für die von Adressen vorgesehen sind. Anstelle von Caches sind Puffer zwischen Mehrzweckregister und Hauptspeicher eingesetzt. Die Puffer sorgen für eine schnelle Nachlieferung der Adressen und Daten zu den Funktionseinheiten. Der Befehlspuffer realisiert das Prinzip Prefetch- Buffers. Ein schwieriges Problem der CRAY-1 liegt in der Verarbeitung von Vektoren mit einer Länge größer als 64 Elemente. Um Operationen mit größeren Vektoren von z.B. 4096 Elementen auszuführen, müssen diese in Teilstücke von 64 Elementen zerlegt werden. Diese Fragmente müssen dann stückweise verarbeitet werden. Eine __________________________________________________________________________________________ Seite 4 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 solche Aufteilung eines Vektors in Teilstücke, die in ein Vektorregister passen, heißt Sektionierung 1. Im Normalfall besitzen aber die zu verarbeitenden Vektoren keine Längen mit einem Vielfachen von 64 Byte, so daß ein Reststück der Vektoren nach der Sektionierung übrig bleibt und erfahrungsgemäß nicht am Ende, sondern am Anfang verarbeitet werden muß. Nachdem die erste Sektion der zu verarbeitenden überlangen Vektoren ein Ergebnis generiert hat und dieses im Hauptspeicher abgespeichert wurde, sollte aus Performance- Gründen die Möglichkeit bestehen, die nächste Sektion der Quell-Operanden in die betreffenden Vektorregister zu laden. Diese drei Zugriffe können teilweise zeitlich überlappend vorgenommen werden. Die CRAY-1 kann aber innerhalb eines Maschinenzyklus nicht die notwendigen drei Zugriffe, sondern nur einen einzigen realisieren. Infolge dieses Nachteils sinkt die theoretische Gleitkomma- Verarbeitungsleistung von 80 MFLOPS auf 80/3 MFLOPS. Dieser Nachteil der CRAY-1 Architektur ist in der CRAY-2 und später in den CRAY Nachfolgearchitekturen beseitigt worden. CRAY-2 Abb.3: Speicher-Organisation der CRAY-2-Architektur (4Speicher-Quadranten zu je 32Bänken und 1Gbyte) Der CRAY-2 Vektorrechner wurde zum Beispiel mit einem Hauptspeicher von 512 M Worten (Wort=64Bit) ausgeliefert. Der Hauptspeicher ist in der Form organisiert, daß die 4 Gbyte in 4 Quadranten zu je 32 Bänken aufgeteilt werden (Abb.3). Bezüglich des ‘Stride‘ existieren unterschiedliche Möglichkeiten. Wenn die Vektorelemente aufeinanderfolgend in der linken oberen Ecke des 1., 2., 3. und 4. Quadranten beginnend (schraffiert) angeordnet sind, hat der ‘Stride‘ den Wert 1. Werden die Elemente in 4 benachbarten Bänken desselben Quadranten angeordnet, beträgt der ‘Stride‘=4, während beim Abspeichern in einer Speicherbank mit einem ‘Stride‘=128 gerechnet werden muß. Der Hauptspeicher der CRAY-2 kann optimal mit statischen (55ns Zyklus-Zeit) oder dynamischen (80ns Zyklus-Zeit) RAM-Bausteinen ausgerüstet werden. CRAY-XMP Als Nachfolgearchitektur der CRAY-2 wurde die CRAY-XMP EA entwickelt. Dieser Vektorrechner enthält insgesamt 13 separate Pipelines (4 Vektor-Festkomma-Pipes, 3 Gleitkomma-Pipes mit je 4 Stufen, 4 SkalarFestkomma-Pipes, 2 Adressen-Pipes). Der Hauptspeicher ist in 64 Speicherbänke aufgeteilt, auf die unabhängig voneinander zugegriffen werden kann (68ns Zyklus-Zeit). Die CRAY-XMP arbeitet ohne virtuelle Speichertechnik. Die Vektor-Sektionierung wird im Angelsächsischen Sprachgebrauch als „strip mining“ bezeichnet, in Anlehnung an das Abtragen von Schichten im Minen- Tagebau. __________________________________________________________________________________________ 1 Seite 5 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 CRAY-YMP Einen leistungsfähigenVektorrechner stellt die CRAY-YMP dar. Die wichtigste Implementierung dieser Architektur stellen die drei sogenannten Adressen-Pipes dar, die dem Rechner ermöglichen, gleichzeitig drei Hauptspeicher-Zugriffe (2 Lese- und 1Schreiboperation) vorzunehmen. Eine Adressen-Pipe ist eine Verbindung zwischen zwei Crosspoint-Matrixschaltern. Für eine optimale Performance ist es jedoch notwendig, daß keine Zugriffskonflikte zu den Hauptspeicherbänken auftreten. Die CRAY-YMP verfügt bei maximaler Ausrüstung über 8 CPU’s (6ns Zyklus-Zeit, 4 parallele Daten-Pfade, 8*32 Bit Adressen-Register, 8*64 Bit Skalar-Register, 8*64 Bit Vektorregister zu je 64 Elementen) und 256 MByte Hauptspeicher (256 Bänke, 30ns Zyklus-Zeit). Die maximale Gleitkommaleistung beträgt 2.67 GFLOPS (maximal 2 Operationen pro Zyklus). Die YMP C90 Architektur verfügt über 16 CPU’s mit einer Leistung von maximal 13.7 GFLOPS. Architektur der CRAY-YMP 816 Abb.5: CRAY-YMP 816 __________________________________________________________________________________________ Seite 6 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Das schematische Blockschaltbild der YMP 816 wird in der Abb.5 gezeigt. Das System kann für 1, 2, 4 und 8 Prozessoren konfiguriert werden. Die 8 CPU’s der YMP teilen sich den Hauptspeicher, die I/O Einheit, Interprozessor Kommunikation und die Echtzeituhr. Der Hauptspeicher ist in 256 überlappende Bänke unterteilt. Ein 6 ns Taktzyklus wird in der CPU verwendet. Der Hauptspeicher wird mit 16M-, 32M, 64M, und 128 M- Wort und einer maximalen Größe von 1 Gbyte angeboten. Die SSD Optionen gibt es von 32 M bis 512 M Worte oder bis zu 4 Gbytes. Die 4 Speicherzugriffskanäle erlauben jeder CPU, gleichzeitig 2 Skalar- und Vektor Aufrufe, einen Speicher und einen unabhängigen I/O Zugriff auszuführen. Diese parallelen Speicherzugriffe erfolgen auch über eine Pipeline, um Vektor-READ und Vektor-WRITE zu ermöglichen. Die Architektur wurde so konzipiert, daß die durch Speicherkonflikte verursachten Pausen minimiert wurden. Um die Daten zu schützen, wird die SECDED (Single Error Correction and Double Error Detection) Logik im Hauptspeicher und an den Datenkanälen vom und zum Hauptspeicher verwendet. Die Recheneinheit verfügt über 14 Functional Units, die unterteilt sind in Vektor, Skalar, Adress und Control Sektionen. Beide Skalar- und Vektorinstruktionen können parallel ausgeführt werden. 8 von den 14 Functional Units können von den Vektorinstruktionen verwendet werden. Es werden große Zahlen von Adress, Skalar, Vektor, Intermediate und temporäre Register verwendet. Durch das benutzen von Registern, mehrfachen Speicherzugriffen und Arithmetic Logic Pipelines wird das flexible Chaining von Functional Pipelines ermöglicht. 64 Bit Floting Point und 64 Bit Integer Arithmetic sind ausgeführt. Große Instruction–Buffers werden benutzt, um 512 16 Bit Instruktionspakete zur gleichen Zeit zu halten. Die Interprocessor Kommunikations- Sektion vom Großrechner verfügt über Cluster von geteilten Registern für schnelle Synchronisationszwecke. Jedes Cluster verfügt über geteilte Adress-, geteilte Skalar und Semaphor- Register. Die Vektor Datenkommunikation unter den CPU’s wird durch den geteilten Speicher realisiert. Die Echtzeituhr verfügt über einen 64 Bit Zähler, der bei jedem Taktzyklus hochgezählt wird. Weil der Takt synchron mit der Programmausführung hochgezählt wird, kann er während der Ausführung als exakter Zähler verwendet werden. Die I/O Sektion unterstützt 3 Kanaltypen mit Transferraten von 6 Mbyte/s, 100 Mbyte/s und 1 Gbyte/s. Die IOS und SSD sind Hochgeschwindigkeits-Module, die entwickelt wurden, um Großrechner Prozesse für 8 Caches zu unterstützen. Probleme bei der Vektorisierung Die Probleme, die bei der Vektorisierung auftreten können, wollen wir an einem kleinen Beispiel illustrieren. Hierfür betrachten wir folgendes Programm: 10 INTEGER J(3), K(3), L(3), M(3), N(3) DATA J/ 2, -4, 7 / K/ 5, 3, 8 / M/ 4, 6, -2/ DO 10 I = 1, 3 L(I) = J(I) + K(I) N(I) = L(I) + M(I) CONTINUE END Ohne Vektorisierung: L(1) = N(1) = L(2) = N(2) = L(3) = N(3) = J(1) + L(1) + J(2) + L(2) + J(3) + L(3) + K(1) M(1) K(2) M(2) K(3) M(3) 7 = 2 +5 11 = 7 + 4 -1 = -4 + 3 5 = -1 + 6 15 = 7 + 8 13 = 15 + ( -2 ) __________________________________________________________________________________________ Seite 7 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Mit Vektorisierung: Wenn das Programm vektorisiert abläuft, wird zuerst die gesamte Schleife für die Anweisung L(I) = J(I) + K(I) ausgeführt, dann für die Anweisung N(I) = L(I) + M(I). L(1) = L(2) = L(3) = N(1) = N(2) = N(3) = J(1) + J(2) + J(3) + L(1) + L(2) + L(3) + K(1) K(2) K(3) M(1) M(2) M(3) 7 = 2 +5 -1 = -4 + 3 15 = 7 + 8 11 = 7 + 4 5 = -1 + 6 13 = 15 + ( -2 ) Die Reihenfolge der Anweisungen wird durch die Vektorisierung verändert. Auf das Ergebnis hatte die Umgruppierung in diesem Beispiel keinen Einfluß, L() und N() haben nach dem Schleifendurchlauf in beiden Fällen den gleichen Inhalt. Das ist aber nicht immer der Fall, wie folgendes Beispiel zeigt: 10 INTEGER A(3), B(3), C(3) DATA A/ 1, 2, 3 / B/ 5, 7, 9 / DO 10 I = 3, 2, -1 A(I) = B(I) C(I) = A(I-1) CONTINUE Ohne Vektorisierung: A(3) = B(3) C(3) = A(2) A(2) = B(2) C(2) = A(1) A(3) = 9 C(3) = 2 A(2) = 7 C(2) = 1 Mit Vektorisierung: A(3) = B(3) A(2) = B(2) C(3) = A(2) C(2) = A(1) A(3) = 9 A(2) = 7 C(3) = 7 C(2) = 1 Es hängt also von der Konstruktion des Programms ab, ob man durch Vektorisierung zu falschen Ergebnissen kommen kann, weil durch die Vektorisierung die Reihenfolge der Anweisungen geändert wird. Die falschen Ergebnisse kommen durch Abhänigkeit der Daten innerhalb der Schleife zustande. Diese Abhänigkeit wird Dependency genannt. Sie kann auftreten, wenn in einer Schleife ein Feldelement definiert und benutzt (referenziert) wird. Der Compiler erkennt die Gefahr von Dependencies und erzeugt in diesem Fall keinen vektorisierten Code, d.h., das Problem, daß durch Vektorisierung des Compilers falsche Ergebnisse erzeugt werden, besteht nicht. Das Problem beim Programmieren von Vektorrechnern besteht meist darin, Programmteile, die vom Compiler nicht vektorisiert werden können, durch Umstrukturieren des Programms doch zu vektorisieren. Das kann duch eine einfache Umstellung einiger Anweisungen erfolgen, durch die Verwendung eines anderen Algorithmus (wenn der ursprüngliche Algorithmus prinzipiell keine Vektorisierung zuläßt), es kann aber auch ganz unmöglich sein, wenn das Problem entsprechend geartet ist. __________________________________________________________________________________________ Seite 8 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Dependency Tests Wir werden im folgenden einige Kriterien entwickeln, mit denen entschieden werden kann, ob in einer Schleife die Gefahr einer Dependency vorliegt. Wir interessieren uns hierbei nur für Dependencies in Schleifen, weil nur dort die Möglichkeit der Vektorisierung besteht. Diese Kriterien werden wir anwenden, um in einigen, häufig auftretenden Situationen Dependencies aufzuspüren und Methoden kennenzulernen, Dependencies zu umgehen. Abb.6: Dependency-Analyse Es gibt unterschiedliche Möglichkeiten für die Abhänigkeit zwischen verschiedenen Anweisungen, die sich wie folgt klassifizieren lassen: Wir betrachten hierfür ein Programmfragment, in dem die Anweisung A(I) = ... bezüglich einer Dependency untersucht werden soll. Eine Dependency kann nur dann vorliegen, wenn A() in einem anderen Teil der Schleife auftritt. Man unterscheidet, ob (siehe Abb.6) weitere Bezug auf A() im Bereich vor (P) oder nach (S) der Anweisung A(I) =..... (wobei die rechte Seite der Anweisung zum Bereich von “vorher“ gehört) liegt, der Index des weiteren Bezugs größer (G) oder kleiner (L) als I ist, der Index aufsteigend (I) oder absteigend (D) ist. Es gibt 3 Kategorien mit jeweils 2 Möglichkeiten die Dependencies lassen sich also in 8 Gruppen aufteilen: PGI, PLI, PGD, PLD, SGI, SLI, SLD, SGD. Nicht jeder dieser Gruppen hat eine Dependency zur Folge. Die Gefahr einer Dependency besteht bei den Gruppen SLD, SGI, PLI und PGD. __________________________________________________________________________________________ Seite 9 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Dependencies beheben Umordnen von Anweisungen Das folgende Programm besitzt eine Dependency vom Typ PLI 10 SUBROUTINE S(A, B, C, N) DIMENSION A(100), B(100), C(100) DO 10 I= 2, N B(I)= A(I-1) * B(I) + C(I) A(I)= C(I) CONTINUE Es darf nicht vektorisiert ablaufen, weil in diesem Fall zuerst die Anweisung B(I)= A(I- 1) * B(I) + C(I) für alle I ausgeführt würde und somit immer die alten Werte von A() benutzt würden. Bei skalarer Ausführung wird durch A(I)= C(I) das in der nächsten Iteration verwendete A() verändert. Der Compiler erzeugt also einen sklararen Code. Wie muß das Programm verändert werden, um einen vektorisierten Ablauf zu ermöglichen? Es müssen lediglich die beiden Anweisungen vertauscht werden. 10 SUBROUTINE S(A, B, C, N) DIMENSION A(100), B(100), C(100) DO 10 I= 2, N A(I)= C(I) B(I)= A(I- 1) * B(I) + C(I) CONTINUE Diese Version des Programms ist vom Typ SLI; sie besitzt keine Dependency mehr. Natürlich muß man sich bei dem Umordnen der Anweisungen davon überzeugen, daß das veränderte Programm gleiche Ergebnisse liefert. Das so optimierte Programm benötigt bei N=100 nur 26% der Rechenzeit des ursprünglichen Programms. Recurrence Threshold Das folgende Programm enthält zwar eine Dependency, trotzdem kann es teilweise vektorisiert ablaufen: 10 SUBROUTINE S(A) DIMENSION A(100) DO 10 I= 6, 100 A(I)= A(I-5)+1 CONTINUE END __________________________________________________________________________________________ Seite 10 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 A() wird zwar in der Schleife geändert, allerdings hat die Änderung erst nach 5 Iterationen einen Einfluß: A(6) = A(1) A(6) wird definiert A(7) = A(2) A(8) = A(3) A(9) = A(4) A(10)= A(5) A(11)= A(6) nach 5 Iterationen wird A(6) benutzt A(12)= A(7) Jeweils 5 Iterationen können also auch vektorisiert ablaufen. Diese Tatsache wird als Recurrence Threshold von 5 bezeichnet. Der vom Compiler erzeugte Code wird die ersten 5 Elemente von A() in ein Vektorregister laden und verarbeiten, dann die nächsten 5 u.s.w.. Ist die Recurrence Threshold größer als 63 (der Länge eines Vektorregisters), kann der Code immer vektorisiert ablaufen. Save Vector Length Der Compiler kann sogar eine Variable Recurrence Threshold verarbeiten wie im folgenden Beispiel: 10 SUBROUTINE S(A, B, K, N) DIMENSION A(-1000:1000), B(-1000:1000) DO 10 I= K, N A(I)= A(I-K) CONTINUE END Hier ist die Recurrence Threshold zur Übersetzungszeit nicht bekannt. Der Compiler erzeugt einen Code, der zur Laufzeit die maximal zulässige Vektorlänge ermittelt (save vector length). Loop Unrolling Das folgende Programm enthält eine Dependency, es kann also nicht vektorisiert werden. 10 SUBROUTINE S(A, B) DIMENSION A(100), B(100) DO 10 I= 2, 100 A(I) = A(I-1) * B(I) CONTINUE END Beim Übersetzen dieses Programms wird die Schleife teilweise „aufgerollt“, d.h., einige Iterationen explizit programmiert. Hierdurch wird eine hohe skalare Optimierung erreicht. Es ist sinnvoll, soviel Iterationen aufzurollen, daß die Skalarregister möglichst alle genutzt werden. 10 SUBROUTINE S(A, B) DIMENSION A(100), B(100) DO 10 I= 2, 100-3,4 A(I) = A(I-1) * B(I) A(I+1) = A(I) * B(I+1) A(I+2) = A(I+1) * B(I+2) A(I+3) = A(I+2) * B(I+3) CONTINUE END __________________________________________________________________________________________ Seite 11 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Partielle Vektorisierung Viele umfangreiche Schleifen können wegen wenigen Anweisungen nicht vektorisiert werden. In diesem Fall empfiehlt es sich, die Schleife in mehrere separate Schleifen aufzuteilen, von denen wenigstens einige vektorisiert ablaufen können. 10 DO 10 I= 2, 10 A(I)= A(I-1)*5 B(I)=SIN(A(I)) CONTINUE Diese Schleife ist wegen der ersten Anweisung nicht vektorisierbar, in der zweiten wird aber die meiste Rechenzeit verbraucht, da die Sin-Berechnung aufwendig ist. Partielle Vektorisierung lohnt sich hier besonders, da eine hochoptimierte Vektorversion der Sinusfunktion existiert, die durch folgende Programmänderung genutzt werden kann: 10 DO 10 I= 2, 10 A(I)= A(I-1)*5 Skalare Ausführung CONTINUE 20 DO 20 I= 2, 10 B(I)=SIN(A(I))Vektorisierte Ausführung CONTINUE Bedingte Vektorisierung Das folgende Programm kann nicht vektorisiert werden, ohne den Wert IProblem zu kennen. 10 SUBROUTINE S(A, B, IProblem) DIMENSION A(*), B(*) DO 10 I= 1, 100 A(I+IProblem)= A(I)+B(I) CONTINUE END Da zur Zeit der Übersetzung der Wert von IProblem unbekannt ist, kann die Schleife nicht unbedingt vektorisiert werden. Sie ist vektorisierbar, wenn IProblem<1 oder IProblem>64 ist. Das Problem kann gelöst werden, indem Code für skalare und vektorielle Ausführung erzeugt wird und per IF-Abfrage zur Laufzeit entschieden wird, welcher Teil durchlaufen wird. Loop Switching Hier ein Standard-Beispiel für eine Dependency in der innersten Schleife: 10 SUBROUTINE S(A, B, N, X) DIMENSION A(*), B(*) DO 10 J= 1, N DO 10 I= 1, N A(I,J)=X*A(I-1,J) CONTINUE END __________________________________________________________________________________________ Seite 12 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Hier sind beide Schleifen voneinander unabhäng, und die Dependency besteht nur in der Schleife über I. Deshalb können die Schleifen einfach vertauscht werden (“Loop Switching“). 10 SUBROUTINE S(A, B, N, X) DIMENSION A(*), B(*) DO 10 I= 1, N DO 10 J= 1, N A(I,J)=X*A(I-1,J) CONTINUE END Matrix Multiplikation Folgendes Programm führt eine Matrix-Multiplikation (A() = B() x C()) durch. Die Verschachtelung der 3 Schleifen ist so ausgeführt, daß die Anweisungen in dieser Reihenfolge auch ein Mensch ausführen würde: DO 10 I = 1, N DO 10 J = 1, N A(I,J) = 0. DO 10 K = 1, N A(I,J)= A(I,J) + B(I,K) * C(K,J) 10 CONTINUE Bei der Ausführung auf einem Vektorrechner kann zwar die Multiplikation B(I,K) * C(K,J) vektorisiert durchgeführt werden, aber A(I,J) ist in der innersten Schleife ein Skalar; die Schleife hat also die Form: Skalar = Skalar + Vektor * Vektor, die nicht vektorisiert werden kann. Durch folgende Umordnung der Schleifen erhält die innnerste Schleife die Form: Vektor = Vektor + Vektor * Skalar, die sehr gut vektorisiert werden kann: 10 20 DO 10 I = 1, N DO 10 J = 1, N A(J,I) = 0. CONTINUE DO 20 K = 1, N DO 20 J = 1, N DO 20 I = 1, N A(I,J)= A(I,J) + B(I,K) * C(K,J) CONTINUE Durch diese Form wird die Geschwindigkeit fast verdoppelt, obwohl jedes Element von A jetzt N-mal häufiger aus dem Speicher gelesen und geschrieben werden muß. Dies ist auf der CRAY kein Problem, weil sie 3 Pfade zum Memory besitzt, die dann voll genutzt werden. Weitere Verfahren sind: Indirekte Adressierung Dependencies in Mehrdimensionalen Felden Reduction Loops __________________________________________________________________________________________ Seite 13 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Performance Gewinn durch Vektorisierung (Amdahls Gesetz) Jedes Programm besteht aus einem vektorisierbaren und einem skalar ablaufendem Anteil. Amdahls Gesetz ist ein Maß für den Performancegewinn durch Vektorisierung (oder auch Parallelisierung) in Abhängigkeit des vektorisierbaren Anteils an dem Programm. Hierzu betrachten wir folgende Größen: R = Geschwindigkeitsgewinn durch Vektorisierung im Vergleich zur skalaren Ausführung (bei der CRAY typischerweise 10-20) V = Anteil der Operationen, die vektorisiert ablaufen S = 1-V = Anteil der Operationen, die skalar ablaufen müssen Der Speedup A durch Vektorisierung ist dann A = skalare Ausführungszeit / gemischte Ausführungszeit = T S / TG In der Zeit TG wird der vektorisierte Anteil des Programmms auch vektorisiert verarbeitet (mit R-facher Geschwindigkeit), der Rest wird skalar verarbeitet, d.h.: TG = S * TS + V * TS / R Dieser Zusammenhang wir Amdahls Gesetz genannt. Er zeigt, daß der skalare Anteil eines Programms dominierend für die Performance ist. In der Realität kann R über 10 liegen, häufig ist R aber kleiner 10 wegen zusätzlichen Overhead oder zu kurzen Vektorlängen. Ein spürbarer Performancegewinn kann nur dann erzielt werden, wenn fast das gesamte Programm vektorisiert ist. Ist dies nicht der Fall, ist die Geschwindigkeit des Skalarteils dominierend für die Performance. Ein Vektorrechner ist nur dann schnell, wenn er auch einen schnellen Skalarteil besitzt, der die Grundperformance angibt. Es ist zu beachten, daß in vielen Programmen der größte Teil der CPU-Zeit in einem kleinen Teil des Programms verbraucht wird (häufig werden in 20% des Programms 80% der CPU-Zeit verbraucht). Deshalb bringt die Vektorisierung in einem kleinen Teil des Programms oft einen spürbaren Performancegewinn, weil in Amdahls Gesetz der zeitliche Anteil des Programms zählt. __________________________________________________________________________________________ Seite 14 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Speichersysteme Realer Speicher Bei der CRAY und anderen Supercomputern wird aus Performancegründen kein virtueller Speicher verwendet. Das führt dazu, daß jeder Prozeß an einer bestimmten realen Speicherstelle beginnt, und sein gesamter benutzter Speicher in den folgenden Speicherstellen liegt. Möchte der Prozeß seine Größe ändern, kann er dies nur bis zum Beginn des nächsten Jobs. Wird ein Prozeß beendet, hinterläßt er eine freie Lücke, die von anderen Prozessen genutzt werden kann. Da jeder Prozeß einen zusammenhängenden Speicherbereich benötigt, kann es sein, daß zwar insgesamt genug Speicherplatz vorhanden ist, aber die größte freie Lücke gerade zu klein für einen neuen Prozeß ist. Das Memory ist zu stark fragmentiert. In diesem Fall ordnet das BS die Prozesse um (SHUFFLE) und erzeugt damit eine große Lücke. Ein weiterer Nachteil vieler Realisierungen von realem Speicher liegt darin, daß ein Prozeß immer in seiner Gesamtheit im Speicher oder außerhalb des Speichers (SWAP) ist, während bei einem Rechner mit virtuellem Speicher auch Teile eines Programms ausgelagert weren können, da hier mit Seiten oder Segmenten gearbeitet wird. Bei folgender Konstellation, die leider in der Praxis häufiger auftritt, wird dieser Nachteil besonders deutlich: Nur 2 Prozesse sind aktiv, von denen jeder 55% des verfügbaren Speichers benötigt. Es kann also nur jeweils einer der beiden Prozesse im Speicher sein. Wenn dieser die CPU nicht nutzen kann (weil er z.B. auf ein Peripherie-Gerät wartet), bleibt die CPU ungenutzt. Das Auslagern des aktiven Jobs und ins Memory Holen des wartenden Jobs dauert sehr lange (wenn z.B. jeder Job 30MB groß ist, wären das mindestens 6s). In dieser Zeit ist keiner der beiden Prozesse aktiv. Wenn bei einer solchen Konstellation beide Prozesse viele I/O-Operationen durchführen, kann es sein, daß die CPU die meiste Zeit ungenutzt ist. Hier kann man nur auf einen weiteren Prozeß hoffen, der weniger als 40% des Speichers benötigt und wenige I/O-Operationen durchführt. Dieser könnte dann die freie CPU-Zeit nutzen. Der große Vorteil des realen Speichers ist seine hohe Geschwindigkeit. Während bei einem virtuellen System für einen Speicherzugriff zunächst aus der virtuellen Adresse die reale Adresse berechnet und für den Zugriff möglicherweise eine zusätzliche I/O-Operation durchgeführt werden muß, kann bei einem System mit realem Speicher der Zugriff immer direkt erfolgen. __________________________________________________________________________________________ Seite 15 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Abb.7: Memory-Bandbreite einer CRAY-YMP für verschiedene Strides. Durch das reale Speicherkonzept besitzt diese Kurve keinen Trend nach oben wie bei der Convex (Abb.8) Abb.8: Memory-Bandbreite einer Convex C3800 für verschiedene Strides __________________________________________________________________________________________ Seite 16 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Memory Banks Jedes Register ist in der Lage, in jedem Taktzyklus ein Wort zu lesen bzw. zu schreiben. Da die Kosten für solche Register sehr hoch sind, ist es nicht praktikabel, mehr als einige hundert von ihnen in einem Rechner zu haben. Der Hauptspeicher besteht aus Millionen von Speicherzellen, die billiger und damit auch langsamer sind. Bei der CRAY-YMP ist das Memory für einen Zugriff 8 Zyklen blockiert. Durch das Konzept der MemoryBanks ist es trotzdem möglich, ein Speichersystem zu besitzen, das in jedem Taktzyklus ein Speicherzugriff ermöglicht. Der gesamt Speicher ist in mehrere (CRAY-YMP: je nach Anzahl der CPU´s 64 oder mehr) Bänke unterteilt, die in gewisser Weise voneinander unabhängig arbeiten können: Wenn eine Bank durch einen Zugriff blockiert ist, können die anderen weitere Zugriffe verarbeiten. Der Speicher ist so organisiert, daß aufeinanderfolgende Speicherstellen in unterschiedlichen Bänken liegen. __________________________________________________________________________________________ Seite 17 von 18 Markus Wegner, Michael Stange, I7I1 14.05.2016 Quellen Klaus Schmidt „Programmierung von Vektorrechnern und Parallelrechnern“, Verlag Harry Deutsch, Frankfurt am Main, Thun, 1994, ISBN: 3-8171-1360-9 Wilhelm Gehrke „Fortran 90 Referenz-Handbuch“, Carl Hanser Verlag München, Wien, 1991, ISBN: 3-446-16321-2 Brigitte und Rainer Wojciescynski „Fortran 90 Programmieren mit dem neuen Standard“ Addison Wesley, Bonn, 1993, ISBN: 3-89319-600-5 Rudolf Herschel „Fortran Systematische Darstellung für den Anwender“ R.Oldenburg Verlag, München, 1978, ISBN: 3-486-22431-X __________________________________________________________________________________________ Seite 18 von 18