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:

In der Assembler-Sprache der C3800 gibt es u.a. folgende Arten von Vektor-Operationen:

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:
  1. 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'').
  2. 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.

previous    contents     next

Peter Junglas 18.10.1993