Architektur der Convex C3800
Die C3800-Rechner der Firma Convex sind eine Familie von
Multiprozessor-Rechnern mit ein bis acht Prozessoren; die C3840 der TUHH
ist ein Vier-Prozessor-Modell. Jeder Prozessor ist mit Hardware zur
Vektorverarbeitung ausgestattet, d.h. er hat spezielle Vektorregister und
Vektorkommandos. Im einzelnen sind dies folgende Register:
- Vektorregister V0, V1, .. V7
128 x 64 bit
Ein Vektorregister enthält einen Satz von 128 64bit-Operanden. Es dient
zum Zwischenspeichern und als Akkumulator von Ergebnissen von
Vektor-Operationen.
- Vektor-Länge-Register VL
7 bit
VL enthält die Anzahl (0 .. 127) der Elemente eines Vektors.
- Vektor-''Stride''-Register VS
32 bit
VS gibt in Byte den Abstand im Speicher zwischen aufeinanderfolgenden
Elementen eines Vektors an.
- Vektor-''Mask''-Register VM
128 bit
- VM enthält für jedes Element eines Vektors ein Bit, das angibt, ob das
Element an einer Vektoroperation teilnehmen soll.
In der Assembler-Sprache der C3800 gibt es u.a. folgende Arten von
Vektor-Operationen:
- Lade-/Speicher-Operationen,
arithmetische und logische Verknüpfungen ( Quadratwurzel,
UND-, ODER- und XOR-Verknüpfung),
- Reduktionen, d.h. Bildung eines Skalars aus einem Vektor, durch
Addition, Multiplikation, Maximum, Minimum oder eine der drei logischen
Operationen,
- Vergleichsoperationen (das Ergebnis steht im VM-Register),
- Masken-Operationen (alle obigen, aber unter Verwendung des VM-Registers)
- Gather/Scatter-Operationen, d.h. Verteilen der Elemente eines Vektors
über einen Indexvektor
Die ersten vier Gruppen gab es in ähnlicher Weise schon auf den ersten
Vektorrechnern der Firma Cray, andere kamen hinzu, als deutlich wurde, daß
sich damit große Klassen von Anwendungen beschleunigen lassen würden. So
erlauben es die Masken-Operationen, gewisse Schleifen auch dann zu
vektorisieren, wenn sie IF-Bedingungen enthalten. Die
Gather/Scatter-Operationen bieten eine schnelle Methode zur indirekten
Adressierung ganzer Vektoren, wie sie vor allem bei Berechnungen mit dünn
besetzten Matrizen auftreten.
Als Beispiel, wie diese Operationen benutzt werden, betrachten wir die
Bildung des Skalarprodukts:
In Fortran:
INTEGER A(100), B(100), SUMME, I
SUMME = 0
DO I=1, 100
SUMME = SUMME + A(I)*B(I)
ENDDO
Im Assembler der C3800:
ld.w #0, s0 ; lade s0 mit 0
ld.w #100, VL ; Vektorlaenge = 100
ld.w #4, VS ; Abstand der Elemente = 4 Byte
ld.w A, v0 ; lade Vektor A nach v0
ld.w B, v1 ; und B nach v1
mul.w v0, v1, v2 ; multipliziere, Ergebnis nach v2
sum.w v2 ; addiere Elemente aus v2, Ergebnis ist in s2
st.w s2, SUMME ; speichere s2 nach SUMME
Auch wenn die Addition von bis zu 128 Elementen nur ein einziger
Assemblerbefehl ist, werden doch nicht alle Elemente gleichzeitig addiert,
sondern es wird sogenanntes ''Pipelining'' verwendet, d.h. es wird mit der
Verarbeitung der nächsten Elemente schon begonnen, bevor das letzte fertig
ist. Benötigt eine Addition z.B. 8 Prozessortakte (Zahl willkürlich), so
kann man durch Pipelining erreichen, daß mit jedem Takt eine weitere
Addition beginnt, und so schließlich pro Takt doch eine Addition
durchgeführt wird, zuzüglich der ''Startup''-Zeit von 7 Takten. Da
außerdem noch Zeit für das Setzen von VL- und VS-Register gebraucht wird,
rentiert sich das Verfahren erst ab einer gewissen Vektorlänge, d.h.
Gesamtzahl von Additionen.
Eine weiteres Verfahren, um noch mehr Geschwindigkeit herauszuholen, ist
das ''Chaining'', d.h. das direkte Weiterleiten von Ergebnissen einer
Pipeline in die nächste, ohne daß auf die komplette Beendigung der ersten
Operation gewartet werden müßte. Damit dies funktioniert, werden die
arithmetischen Einheiten in Funktionsgruppen eingeteilt, zwischen denen ein
solches Weiterleiten möglich ist. So ist es beispielsweise auf der C3800
möglich, eine Vektor-Multiplikation und eine Addition zu ''chainen'', nicht
aber zwei Additionen.
Falls alle diese Möglichkeiten optimal ausgenutzt werden und auch der
Speicherzugriff die Verarbeitung nicht hemmt (was ebenfalls einiger
Voraussetzungen bedarf, s.u.), kann man mit einem C38-Prozessor eine
maximale Verarbeitungsgeschwindigkeit von 120 MFLOPS (Millionen
Fließpunkt-Operationen pro Sekunde) erzielen, bei der C3840 also bei
optimaler Parallelisierung (s.u.) 480 MFLOPS. Zum Vergleich: die C240
schafft 200 MFLOPS, die C1 20 MFLOPS.12
Eine Besonderheit der C38 ist noch, daß bei Verwendung von einfacher
Genauigkeit die Geschwindigkeit der Grundoperationen sich verdoppelt, so
daß dann Spitzenwerte von 960 MFLOPS zu erzielen sind (jedenfalls
theoretisch).
Neben der Ausnutzung der Prozessor-Eigenschaften ist auch die Organisation
der Daten im Speicher wichtig, wenn man hohe Geschwindigkeiten erzielen
will. Da das Übertragen von Daten aus dem Hauptspeicher in Register der
CPU länger als einen Takt dauert, muß man wieder auf Pipelining
zurückgreifen, wenn man Vektoren verarbeiten will. Dazu ist der Speicher
in eine Anzahl von Bänken aufgeteilt, aus denen man gleichzeitig Daten
laden kann, ohne daß sie sich gegenseitig behindern. Aufeinanderfolgende
Speicherelemente werden dabei auf verschiedene Bänke verteilt. Dieses
Verfahren heißt ''Interleaving'', die Anzahl der Bänke ''Interleave-Faktor''.
Auf der C3840 der TUHH beträgt er 64. An einem Beispiel wollen wir uns das
genauer ansehen:
Nehmen wir an, ein einzelner Speicherzugriff brauche 8 Takte und der
Speicher sei in 8 Bänke eingeteilt. Wir wollen ein Array A von 16
Elementen aus dem Hauptspeicher in Register laden, das sich dann
folgendermaßen auf die Bänke verteilt:
Bank |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
|
A(0) |
A(1) |
A(2) |
A(3) |
A(4) |
A(5) |
A(6) |
A(7) |
|
A(8) |
A(9) |
A(10) |
A(11) |
A(12) |
A(13) |
A(14) |
A(15) |
Mit jedem Takt kann mit dem Laden eines neuen Elementes begonnen werden,
nach 7 Takten beginnt das Laden von A(7), nach 8 Takten ist der Ladevorgang
von A(0) abgeschlossen und das Laden von A(8) aus der gleichen Bank kann
sich nahtlos anschliessen.
Im Beispiel kann also tatsächlich mit jedem Takt ein Element geladen
werden, so daß die Vektorverarbeitung an dieser Stelle nicht gebremst
wird. Allerdings können verschiedene Faktoren dieses schöne Bild
verändern:
Angenommen, wir wollten nur jedes zweite Element laden, dann würde nach
vier Takten wieder auf Bank 0 zugegriffen. Diese ist aber noch 4 Takte lang
mit A(0) beschäftigt, so daß der Ladevorgang so lange unterbrochen wird.
Noch schlimmer kommt es, wenn wir etwa nur jedes 64. Element laden wollen,
denn dann wird immer auf die gleiche Bank zugegriffen. Solche großen
Abstände treten aber in der Praxis auf, etwa wenn man Spalten und
Zeilen von Arrays braucht. Was dies für die Programmierung bedeutet,
wollen wir uns in Abschnitt 4.3.4 ansehen.
Ein weiteres Problem ist der Zugriff auf kleine Daten-Einheiten: Die
Größe eines Speicherelements beträgt 64 bit (8 Byte). Zwar kann man auf
der C38 auch auf kleinere Einheiten bis herunter zum Byte direkt zugreifen,
aber das Schreiben dauert für Bytes und Halbworte (2 Byte) dann 20 Takte
statt 8. Allerdings geschieht der Zugriff auf Fließkomma-Zahlen einfacher
Genauigkeit (4 Byte) mit maximaler Geschwindigkeit.
Um noch mehr Rechenleistung für ein Programm zu bekommen, ist es
möglich, es auf mehreren CPUs parallel abarbeiten zu lassen. Wie man in
Fortran oder C diese Möglichkeit nutzen kann, ist Gegenstand des
Abschnitts 4.4. Hier wollen wir kurz die zugrundeliegenden
Architektur-Merkmale vorstellen:
Die CPUs einer C3 greifen unabhängig voneinander auf den gesamten Speicher
zu, so daß ein Austausch von Daten durch das Verschicken von Nachrichten,
wie es z.B. auf einem Transputer-Cluster geschieht, auf dieser Ebene nicht
nötig ist (''Shared Memory''-Maschine im Unterschied zur ''Local
Memory''-Architektur). Darüberhinaus gibt es zur Synchronisation und zum
schnelleren Austausch von Daten 8 Sätze von je 128 Kommunikationsregistern
zu 64 Bit und einem zusätzlichen ''Lock''-Bit sowie zwei Index-Register, von
denen eines, das ''Communication Index Register'' CIR, die Zuordnung einer
CPU zu einem Registersatz steuert und das andere, ''Thread Identifier'' TID
genannt, Speicherbereiche für einzelne CPUs aufteilt, die an einem
Programm arbeiten.
Ein Ausführungsstrang eines Programms, der zu einer CPU gehört, wird
''Thread'' genannt. Es können maximal so viele Threads wie CPUs
gemeinsam an einem Programm arbeiten. Sie gehören zu einem Prozeß und
teilen sich dessen Resourcen, meistens auch den Speicher, können aber auch
private Speicherbereiche benutzen. Ein Programm beginnt zunächst mit einem
einzelnen Thread auf einer CPU und kann sich dann in mehrere parallele
Threads aufteilen. Dazu gibt es zwei verschiedene, von der Architektur
unterstützte Möglichkeiten:
- Das Programm setzt die Anforderung ab, so viele Threads zu erzeugen wie
Prozessoren zur Verfügung stehen. Diese arbeiten dann ihre Anteile am
Problem ab und verabschieden sich mit einem ''join''. Dadurch wird die CPU
wieder für andere Prozesse freigegeben, außer beim letzten Thread, der
nach seinem ''join'' automatisch mit dem Programm weitermacht
(''symmetrische Parallelverarbeitung'').
- Das Programm gibt an, wieviele Threads erzeugt werden sollen. Die weitere
Aufsplittung oder das Zusammenkommen mehrerer Stränge kann und muß von
den einzelnen Threads gesteuert werden. Dazu gibt es verschiedene
Möglichkeiten der Kommunikation und Synchronisation zwischen Threads
(''asymmetrische Parallelverarbeitung'').
Bei beiden Verfahren kann nicht angegeben werden, welcher Thread auf
welcher CPU abläuft, sondern wenn eine CPU Zeit hat, sucht sie sich den
nächsten, bisher unbearbeiteten Thread. Dies kann auch dazu führen, daß
etwa ein Programm von vier Threads nur auf drei Prozessoren läuft, von
denen einer zwei nacheinander bearbeitet, weil der vierte Prozessor zu
beschäftigt ist. Dieses Verfahren nennt Convex ASAP (''Automatic
Self-Allocating Processors''), es hat den Vorteil, daß bei unterschiedlich
langer Dauer einzelner Threads kein Prozessor auf die anderen warten muß,
und erhöht bei genügender Last den Gesamt-Durchsatz der Maschine.
Allerdings ist dadurch das Laufzeit-Verhalten und der aktuelle
Parallelisierungsgrad stark lastabhängig und damit unvorhersagbar. Daher
gibt es für Prozesse die Möglichkeit des ''fixed Scheduling'': Wann immer
ein solcher Prozeß läuft, werden alle CPUs für ihn freigehalten, so daß
die mögliche Parallelität auch verwirklicht wird. Dies bedeutet auf der
andern Seite natürlich, daß einige Prozessoren gelegentlich - in der
Praxis eher: des öfteren - leer stehen. Daher wird man es selten, vor
allem bei Messungen des Laufzeit-Verhaltens mit Profilern, einsetzen.
Peter Junglas 18.10.1993