Funktionale Programmierkonzepte für die verteilte numerische Simulation Ralf Ebner Institut für Informatik der Technischen Universität München Lehrstuhl für numerische Programmierung und Ingenieuranwendungen in der Informatik Funktionale Programmierkonzepte für die verteilte numerische Simulation Ralf Ebner Vollst¨andiger Abdruck der von der Fakult¨at für Informatik der Technischen Universit¨at München zur Erlangung des akademischen Grades eines Doktors der Naturwissenschaften (Dr. rer. nat.) genehmigten Dissertation. Vorsitzender: Univ.-Prof. Dr. Heinz-Gerd Hegering Prüfer der Dissertation: 1. Univ.-Prof. Dr. Christoph Zenger 2. Univ.-Prof. Dr. Manfred Broy Die Dissertation wurde am 1.7.1999 bei der Technischen Universit¨at München eingereicht und durch die Fakult¨at für Informatik am 6.12.1999 angenommen. Funktionale Programmierkonzepte für die verteilte numerische Simulation Zusammenfassung Die Arbeit zeigt, wie die g¨angigen funktionalen Sprachkonzepte der Einfach-Zuweisung, der Rekursion, der hierarchischen Datentypen und der Funktionen höherer Ordnung für die automatisierte Parallelisierung und Verteilung von Anwendungen im Bereich der adaptiven numerischen Simulation einsetzbar sind. Hierzu wird die im Rahmen dieser Arbeit weiterentwickelte funktionale Sprache FASAN beschrieben. FASAN-Programme bilden eine Koordinationsschicht, deren parallele Semantik mit Hilfe von Datenflussgraphen spezifiziert wird. Sie schließen typische Fehler des parallelen Programmierens wie Verklemmungen, Laufbedingungen und falsche Kommunikation aus. Weiterhin werden für die Komponenten hierarchischer Datenstrukturen direkte Kommunikationsverbindungen erzeugt und eine zu starke Synchronisation vermieden. Schließlich verbindet die Übersetzung von FASAN in Java die Forderungen nach Portabilit¨at und nach paralleler Effizienz. Eingesetzt wird das FASAN-System für die rekursive Substrukturierung, ein hierarchisches Gebietszerlegungs- und numerisches Lösungsverfahren für Aufgaben aus dem Bereich der Strukturmechanik, die auf elliptische lineare partielle Differentialgleichungen führen. Hierzu werden für die iterative Lösungsmethode drei neue Varianten vorgeschlagen, die sich besonders für die verteilte Bearbeitung eignen, sowie eine Möglichkeit zur dynamischen Lastverteilung der entstehenden adaptiven Baumstruktur. Beispielrechnungen belegen die praktische Einsetzbarkeit des FASAN-Systems, das parallele Effizienz mit einfacher Programmierung verbindet. An dieser Stelle möchte ich mich bei meinem Doktorvater Prof. Dr. Christoph Zenger bedanken, der mir die Aufgabenstellung überlassen, durch seine unerschöpflichen Ideen und Anregungen diese Arbeit gelenkt und mir trotzdem viel Freiraum für eigene Konzepte gelassen hat. Bei Prof. Dr. Manfred Broy bedanke ich mich für dieÜbernahme des Zweitgutachtens. Mein herzlicher Dank gilt außerdem folgenden Leuten, die zum Gelingen dieser Arbeit beigetragen haben: Martin Backschat hat durch Diskussionen über die Verteilungsaspekte des Substrukturierungsprogramms und durch Testen von Programmteilen viel beim Anwendungsteil für diese Arbeit mitgewirkt. Er war immer über die neuesten Entwicklungsumgebungen, gerade im Java-Bereich, bestens informiert und hat mir wertvolle Hinweise gegeben. Seine gründliche und kritische Durchsicht dieser Arbeit hat wesentlich zu einer klareren Darstellung beigetragen. Eifrige Korrekturleser waren auch meine Ehefrau Elfriede und mein Vater. Alexander Pfaffinger hat mit mir viele der Konzepte von FASAN erörtert, mit seinem Dünngitter-Programm eine wertvolle Testanwendung bereitgestellt und mich immer wieder zur Weiterentwicklung von FASAN ermuntert. Der intensiven Zusammenarbeit mit Stefan Bischof und Thomas Erlebach im Rahmen des SFB 342 verdanke ich die Entwicklung der Lastverteilungskonzepte für die adaptive rekursive Substrukturierung. Dank meiner Kolleginnen und Kollegen, die zu dem ¨außerst guten Arbeitsklima am Lehrstuhl beigetragen haben, konnte ich auch so manche Durststrecke in den vergangenen vier Jahren überstehen. Schließlich hat mir meine Frau die notwendige moralische Unterstützung w¨ahrend meiner T¨atigkeit am Lehrstuhl und vor allem in der Endphase der Arbeit gegeben. Inhaltsverzeichnis i Inhaltsverzeichnis 1 Einführung 1 1.1 Schwierigkeiten verteilter Programmierung . . . . . . . . . . . . . . . . 2 1.2 Verwandte Arbeiten im Parallelisierungsbereich . . . . . . . . . . . . . . 7 1.3 Aufgabenstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.4 Überblick über die Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2 Grundlagen funktionaler Programmierung und Semantik 2.1 Kennzeichen funktionaler Sprachen . . . . . . . . . . . . . . . . . . . . 2.2 Formale Beschreibung und operationelle Semantik funktionaler Sprachen 2.3 Parallele Semantik mit Prozessnetzen . . . . . . . . . . . . . . . . . . . 15 15 18 22 3 Die funktionale Koordinationssprache FASAN 3.1 Sprachbeschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Externe Funktionen und externe Typen . . . . . . . . . . . . . . 3.1.2 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . 3.1.3 Hierarchische Datentypen . . . . . . . . . . . . . . . . . . . . . 3.1.4 Endrekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.5 Lokationen zur verteilten Berechnung . . . . . . . . . . . . . . . 3.1.6 Funktionen höherer Ordnung . . . . . . . . . . . . . . . . . . . . 3.2 Eine abstrakte Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Hierarchische Datentypen . . . . . . . . . . . . . . . . . . . . . 3.2.3 Externe Funktionen, externe Typen und Funktionstypen . . . . . 27 27 29 31 32 33 34 36 39 41 44 45 4 Operationelle Semantik von FASAN 4.1 Datenflussgraphen . . . . . . . . . . . . . . . . . . . . . 4.2 Abbildung der abstrakten Syntax auf Datenflussgraphen . 4.3 Datenflussgraphen als Berechnungsmodell . . . . . . . . 4.3.1 Auswertung von Funktionsknoten . . . . . . . . 4.3.2 Entfaltung von -Knoten . . . . . . . . . . 47 47 49 54 55 57 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inhaltsverzeichnis ii . . . . 58 59 63 65 5 Übersetzung von FASAN in Java 5.1 Erfahrungen aus vorhandenen Implementierungen . . . . . . . . . . . . . 5.2 Auswahl der Zielsprache . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2.1 Auswahl der Parallelisierungsplattform . . . . . . . . . . . . . . 5.3 Übersetzung in ein sequentielles Grundprogramm . . . . . . . . . . . . . 5.3.1 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . 5.3.2 Hierarchische Datentypen . . . . . . . . . . . . . . . . . . . . . 5.3.3 Externe Funktionen und Typen . . . . . . . . . . . . . . . . . . . 5.3.4 Funktionen höherer Ordnung . . . . . . . . . . . . . . . . . . . . 5.4 Erweiterungen für gemeinsamen Speicher mit Threads . . . . . . . . . . 5.4.1 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . 5.4.2 Hierarchische Datentypem . . . . . . . . . . . . . . . . . . . . . 5.5 Erweiterungen für verteilten Speicher mit RMI . . . . . . . . . . . . . . 5.5.1 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . 5.5.2 Hierarchische Datentypen . . . . . . . . . . . . . . . . . . . . . 5.6 Praktische Verwendung von FASAN-Programmen . . . . . . . . . . . . . 71 71 73 75 78 78 81 82 83 85 85 86 88 89 91 97 6 Anwendung von FASAN: Die rekursive Substrukturierung in der numerischen Simulation 99 6.1 Gebietszerlegung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 6.1.1 Beschreibung der Substrukturierung . . . . . . . . . . . . . . . . 102 6.1.2 Klassifikation der Punkte . . . . . . . . . . . . . . . . . . . . . . 103 6.2 Aufbau der Gleichungssysteme . . . . . . . . . . . . . . . . . . . . . . . 105 6.2.1 Assemblierung der Gleichungssysteme aus den Sohnknoten . . . 106 6.2.2 Hierarchisierung des Gleichungssystems . . . . . . . . . . . . . 106 6.2.3 H¨angende Punkte und Dirichlet-Randwerte . . . . . . . . . . . . 110 6.2.4 Algebraische Vorkonditionierung . . . . . . . . . . . . . . . . . 112 6.2.5 Auswirkung auf die Lösungsiterationen . . . . . . . . . . . . . . 112 6.3 Bisherige Lösungsvarianten . . . . . . . . . . . . . . . . . . . . . . . . . 112 6.3.1 Ein Gauß-Seidel-Löser auf hierarchischer Basis . . . . . . . . . . 114 6.4 Neue Lösungsvarianten . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 6.4.1 Ein Gauß-Seidel-Löser mit teilweiser Elimination von starken Kopplungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 6.4.2 Ein Jacobi-Löser auf hierarchischer Basis . . . . . . . . . . . . . 118 4.4 4.3.3 Entfaltung von Funktionsknoten . . . . . . . . . 4.3.4 Entfaltung von Konstruktor- und Selektorknoten 4.3.5 Lokationen . . . . . . . . . . . . . . . . . . . . Eigenschaften der Datenfluss-Semantik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inhaltsverzeichnis 6.5 6.6 iii 6.4.3 Ein CG-Löser auf hierarchischer Basis . . . . . . . . . . . . . . . 120 Das Koordinationsprogramm in FASAN . . . . . . . . . . . . . . . . . . 123 Lastverteilung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 6.6.1 Ein statisches Verfahren . . . . . . . . . . . . . . . . . . . . . . 127 6.6.2 Kostenabsch¨atzung der Lösungsverfahren . . . . . . . . . . . . . 128 6.6.3 Ein dynamisches Verfahren . . . . . . . . . . . . . . . . . . . . . 131 7 Beispielrechnungen und Leistungsmessungen 135 7.1 Testbeispiele für die Simulation . . . . . . . . . . . . . . . . . . . . . . 135 7.2 Numerische Ergebnisse der rekursiven Substrukturierung . . . . . . . . . 139 7.2.1 Gleichm¨aßige Verfeinerung . . . . . . . . . . . . . . . . . . . . 140 7.2.2 Adaptive Verfeinerung . . . . . . . . . . . . . . . . . . . . . . . 143 7.3 Paralleler Ablauf mit FASAN . . . . . . . . . . . . . . . . . . . . . . . . 144 7.3.1 Einsatz von Threads . . . . . . . . . . . . . . . . . . . . . . . . 145 7.3.2 Verteilte Berechnung bei gleichm¨aßiger Verfeinerung . . . . . . . 146 7.3.3 Verteilte Berechnung bei adaptiver Verfeinerung . . . . . . . . . 147 8 Schlussbetrachtungen und Ausblick 149 Literaturverzeichnis 153 Index 163 iv Inhaltsverzeichnis 1 Kapitel 1 Einführung Rechnernetze sind heutzutage sehr weit verbreitet. Aus Sicht des Programmierers handelt es dabei fast immer um Systeme mit verteiltem Speicher, im Gegensatz zu parallelen Programmierumgebungen im engeren Sinn mit einem gemeinsamen Adressraum. Mit der Nutzung von Rechnernetzen für den Bereich der numerischen Simulation zur Verteilung von Anwendungen werden folgende Ziele verfolgt: 1. W¨ahrend nicht-interaktiven Arbeitszeiten können die vorhandenen Rechnerkapazit¨aten für langlaufende Berechnungen sinnvoll genutzt werden. 2. Langdauernde Rechnungen können bei verteilter Ausführung beschleunigt werden. 3. Um der physikalischen Realit¨at möglichst nahe zu kommen, ist man an einer möglichst feinen diskreten Auflösung (Gitterweite) interessiert, der aber vom Hauptspeicher des Rechners Grenzen gesetzt sind. Eine kleinere Gitterweite ist möglich, wenn die Anwendung die Speicher-Ressourcen mehrerer Rechner gleichzeitig nutzt und das Gitter auf die Rechner verteilt, beispielsweise durch Gebietszerlegung. W¨ahrend Ziel 1 noch leicht zu erreichen ist, wenn voneinander unabh¨angige Berechnungen auszuführen sind, erhöht sich die Schwierigkeit der Aufgabe, sobald die Berechnungen (wie bei Ziel 2 und 3) kooperieren, also zur richtigen Zeit Daten austauschen müssen. Zun¨achst stellt sich die Frage, welche Programmiersprache für solche verteilten numerischen Anwendungen eingesetzt werden soll. Programmiersprachen (ob für sequentielle, parallele oder verteilte Anwendungen) lassen sich grob in zwei Kategorien einteilen: W¨ahrend prozedurale Sprachen stark am Maschinenmodell des von-Neumann-Rechners orientiert sind und feste Berechnungsabl¨aufe und Speichermanipulationen vorschreiben, legen deklarative Sprachen nur die Ergebnisse einer Berechnung fest, nicht aber den genauen Rechenweg. Zur deklarativen Sprachfamilie gehören funktionale Sprachen, auch applikative Sprachen genannt. Berechnungen in funktionalen Programmen sind Auswertungen von Ausdrücken. Logikprogrammierung, ebenfalls eine Art deklarativen Programmierens, basiert 2 Einführung auf dem Berechnungsprinzip der Deduktion. Der Vorteil von funktionaler Programmierung gegenüber Logik-Programmen ist darin zu sehen, dass das Ablaufverhalten in operationellen Semantiken leichter erfassbar und damit auch leichter optimierbar ist. Konzepte der Objektorientierung wie abstrakte Datentypen und polymorphe Datenstrukturen sind bei Sprachen aus beiden Familien zu finden und stellen somit ein eigentlich orthogonales Paradigma dar. Dieser Umstand wird sich auch bei dem in dieser Arbeit entwickelten System darstellen, indem die funktionale Koordinationsschicht von FASAN auf die objektorientierte Sprache Java aufgesetzt wird. 1.1 Schwierigkeiten verteilter Programmierung Viele Schwierigkeiten bei der Entwicklung sowohl sequentieller als auch paralleler Programme sind darauf zurückzuführen, dass prozedurale Sprachen immer mit einer Semantik von Zustands¨anderungen versehen sind, wobei der Zustand“ sehr groß ist und im Prin” zip den ganzen Speicherbereich umfasst, den das Programm beansprucht. Diese Problematik wurde schon frühzeitig unter anderem von Backus erkannt, der als möglichen Ausweg die funktionale Programmierung vorgeschlagen hat [Bac78]. Ein Konzept für funktionale Programmierung in technisch-wissenschaftlichen Anwendung wird in [Zen92] vorgestellt. Mit dem Einsatz funktionaler Sprachen werden einige Ursachen dafür, dass parallele und verteilte Programmierung schwieriger und aufwendiger ist als sequentielle, vermieden. Insbesondere müssen nicht mehr bei jedem Berechnungsschritt der globale Speicherzustand und die implizit in einem prozeduralen Programm enthaltenen Seiteneffekte berücksichtigt werden. Doch stellen sich noch weitere Probleme, wie das folgende Beispiel zeigen soll. Beispiel 1.1 Um ein -dimensionales Problem zu lösen, kann man es oft auf ein dimensionales Problem sowie kleinere Probleme der Dimension reduzieren. In der numerischen Simulation wird h¨aufig auf ein geometrisch vorgegebenes Gebiet ein Gitter gelegt, an dessen Kreuzungspunkten die (diskrete) N¨aherungslösung für ein (kontinuierliches) physikalisches oder technisches Problem zu ermitteln ist. So können die Punkte eines zweidimensionalen Gitters, wie es schematisch in Abb. 1.1 zu sehen ist, als eine Liste von Listen gespeichert werden mit Hilfe einer Datenstruktur der Form type Area = empty2()| cons2(first:Line; rest:Area) type Line = empty() | cons (head:double; tail:Line). In Abb. 1.2 ist diese Datenstruktur über dem Gitter dargestellt. Die cons-Knoten bauen die eindimensionalen Listen und die cons2-Knoten die zweidimensionalen Listen auf. Dies ist auch für viele Algorithmen vorteilhaft, die unidirektional auf diesem Gitter arbeiten. Sie zerlegen für die Berechnung die zweidimensionale Liste 1.1 Schwierigkeiten verteilter Programmierung 3 1 2 3 4 5 n 2 3 4 5 6 n+1 m m+1 m+2 m+3 m+4 m+n -1 Abbildung 1.1 Ein zweidimensionales Gitter mit Zeilen und Spalten über einem rechteckigen Gebiet. In den Punkten ist der von Datenabh¨angigkeiten bestimmte frühestmögliche Zeitschritt eingetragen, zu dem sein Wert von der Funktion wavefront aus Beispiel 1.1 berechnet werden kann. in ihre eindimensionalen Teillisten, die dann jede für sich bearbeitet werden, in etwa wie folgende Funktion: function wavefront (prec:Line; ar:Area) -> ar’:Area { if not isempty2(ar) then line = step(prec, ar.first); // (1) ar’ = wavefront(line, ar.rest); // (2) else ar’ = empty(); fi } Wie die Formulierung der Funktion wavefront schon andeutet, treten allerdings Datenabh¨angigkeiten in vertikaler Richtung auf, nicht nur in Richtung der cons-Listen: Zeile (2) h¨angt über die Variable line von Zeile (1) ab. Es sieht also so aus, als ob eine (horizontale) Liste erst berechnet werden kann, wenn die darüberliegende Liste fertiggestellt ist. Wünschenswert ist aber eine Berechnungsreihenfolge, die Werte einer Liste möglichst unmittelbar der benachbarten unteren Liste zur Verfügung stellt, auch wenn im Programmtext syntaktisch die untere Liste von der komplett aufgebauten oberen Liste abh¨angt. Enth¨alt das Gitter Zeilen mit je Punkten, so führt die Auswertung auf einen Zeitbedarf von . Dies sehen wir an der Nummerierung in den Gitterpunkte in Abb. 1.2, die angibt, in welchem Zeitschritt die Berechnung für den betreffenden Punkt erfolgen kann. Bei ausreichend Prozessoren (mindestens ) ist aber die optimale Auswertungsstrategie diejenige von Abb. 1.1, die mit Zeitschritten auskommt. cons2 first cons cons tail Einführung tail 4 cons cons cons cons 2 3 4 5 n cons cons cons cons cons n+2 n+3 n+4 n+5 2n cons cons cons cons cons head head 1 cons2 first cons tail rest head rest n+1 Cons2 first cons tail ... head n(m-1) n(m-1) n(m-1) n(m-1) n(m-1) +1 +2 +3 +4 +5 mn Abbildung 1.2 Eine Liste von Listen als Datenstruktur für die Punkte zur Diskretisierung eines zweidimensionalen Gebiets. Auch bei Problemen aus dem nicht-numerischen Bereich kann ein solcher Konflikt zwischen bequemer Aufschreibung und optimaler paralleler Berechnung auftreten. Nehmen wir einen Algorithmus, der aus mehreren Einzelschritten besteht, von denen jeder für seine Argumente und Ergebnisse Listen verwendet. Es ist dann nicht notwendig, jeden Schritt streng sequentiell nach dem anderen auszuführen. Besser ist es, genau wie oben bei der zweidimensionalen Liste die Schritte in einer Pipeline anzuordnen, bei der jedes Teilergebnis des vorausgehenden Schritts dem folgenden Schritt sofort zur Verfügung steht. Statt Listen finden sich bei rekursiven numerischen Algorithmen meist B¨aume, also hierarchische Datenstrukturen, so z. B. in vielen Gebietszerlegungsalgorithmen für die numerische Simulation, die nach dem Divide&Conquer-Prinzip verfahren. Beispiel 1.2 Ein typisches Programm-Schema für derartige Algorithmen unterteilt etwa ein zweidimensionales Gebiet in zwei 2D-H¨alften und eine 1D-Trennlinie mit folgender Datenstruktur: Tree1 = nil1() | tree1(left:Tree1; val:double; right:Tree1) Tree2 = nil2() | tree2(top: Tree2; sep:Tree1; bottom:Tree2) Damit l¨asst sich ein dünnes Gitter wie in Abb. 1.3 aufbauen. Ein Algorithmus, der die Baumstruktur von oben herab durchl¨auft, verwendet die Ergebnisse der Mittellinie mid für Berechnungen auf oberem und unterem Teilgebiet: topdown2D (border:Tree1; area:Tree2) -> ... { { mid = topdown1D (area.sep); topdown2D (mid, area.top); topdown2D (mid, area.bottom); ... } 1.1 Schwierigkeiten verteilter Programmierung 5 top mid bottom Abbildung 1.3 Eine dünnes Gitter, dessen Punkte in einem Baum von B¨aumen gehalten werden. Auch hier stellt sich wieder das Problem der überm¨aßigen Synchronisation, da Datenabh¨angigkeiten in vertikaler Richtung und damit orthogonal zur Richtung der Datenstrukturen bestehen. Benötigt wird also ein System, das die strenge Auswertungsreihenfolge, die sich aus der Aufschreibung der Datenabh¨angigkeiten ergibt, aufbricht und die voneinander abh¨angigen Funktionsaufrufe verzahnt ausführt. Nicht immer sind die Datenstrukturen regul¨ar. Wenn bestimmte Punkte eines Gitters weggelassen werden oder in Gebieten mit größerem Diskretisierungsfehler eine feinere Auflösung durch mehr Punkte notwendig ist, sind nicht alle Unterb¨aume eines Knotens der Datenstruktur gleich tief wie im obigen Beispiel. Es entsteht dann eine weitere Schwierigkeit bei der verteilten parallelen Berechnung: Der Programmieraufwand wird bedeutend größer, weil zus¨atzlich zu jeder Datenstruktur angegeben werden muss, wie diese zu verpacken und wann und wohin diese Daten zu senden sind. Um uns klar zu werden, welche Anforderungen wir an ein System stellen, das diese Probleme angehen kann, ist es vorteilhaft, die Konstruktion des zu erstellenden verteilten Programms in verschiedene Teilaspekte zu gliedern [AG94, Qui94]: der (in der Regel sequentielle) Algorithmus, die Zerteilung des Algorithmus in parallel ausführbare Teile, die Konsequenzen der Zerteilung, insbesondere die notwendige Synchronisation der entstehenden Programmteile und ihre wechselseitige Kommunikation, und schließlich die Lastverteilung, also die Angabe, auf welchem Rechner welche Teilberechnung auszuführen ist. 6 Einführung Der sequentielle Algorithmus muss notwendigerweise explizit erstellt oder vorgegeben werden. Bei der Zerteilung des Algorithmus in parallele Teilberechnungen stehen wir oft vor dem Problem, dass ein automatisches Vorgehen zu viele kleine Teile produziert und damit die Granularit¨at zu fein und der Overhead der Parallelisierung zu groß wird. Hier erscheint es sinnvoll, dem Programmierer die Möglichkeit zur Angabe großer Teilprobleme offenzuhalten. In verschiedenen Arbeiten (z. B. [Ham94, Pfa97]) wird dargestellt, dass es in lose gekoppelten Rechnernetzen nicht sinnvoll ist, feine Granularit¨at auszuschöpfen. Ist die Zerteilung festgelegt, folgt die Organisation der Kommunikation und Synchronisation, die in einem komfortablen System voll automatisch erledigt werden muss. Denn gerade in diesem Bereich sind die meisten Fehlerquellen angesiedelt, die die Entwicklung verteilter Programme so aufwendig und zeitraubend machen. Um typische Schwierigkeiten der parallelen Programmierung wie Verklemmungen, race conditions (deutsch auch Laufbedingungen genannt) und Kommunikationsfehler soll sich der Programmierer nicht kümmern müssen. Auch die Speicherverwaltung muss idealerweise im verteilten System automatisch erfolgen. Bei der Lastverteilung h¨angt es vom Algorithmus ab, ob sie automatisch vorgenommen oder besser vom Programmierer festgelegt wird. In jedem Fall sollte eine Möglichkeit bestehen, den Lastverteilungsmechanismus leicht auszuwechseln. Für manche Anwendungen kann es sinnvoll sein, zum Beispiel ein Verfahren einzusetzen, das eine neu entstehende Teilaufgabe an einen Rechner zuweist, der aus einer Menge von momentan nicht oder wenig besch¨aftigten Rechnern ausgew¨ahlt wird. Oft kann aber speziell bei der Lastverteilung auf die Erfahrung, Intuition und die Expertise des Programmierers nicht verzichtet werden. Dies erkennen wir beispielsweise in der ausgefeilten Lastverteilung für Algorithmen auf dünnen Gittern in der Arbeit [Pfa97], die einerseits ein automatisierter Lastverteiler nicht finden kann und die andererseits zu anwendungsspezifisch ist, als dass sie vorteilhaft als grundlegendes Verteilungsmuster in ein parallelisierendes System eingebaut werden könnte. Es gibt schon eine ganze Reihe von Ans¨atzen auf höherer Ebene. Diese unterscheiden sich zun¨achst darin, ob der Programmierer auf die Parallelisierung Einfluss nehmen kann. Implizite Parallelisierung scheint zwar komfortabel, l¨asst aber kaum Optimierungsspielraum, wenn die gewünschte oder erwartete Effizienz nicht erreicht wird. Wenn wir uns also für eine teilweise explizite Parallelisierung in dem zu entwickelnden Werkzeug entscheiden, stellt sich die Frage, ob eine vorhandene Sprache durch neue Schlüsselworte und Konstrukte erweitert oder eine neue Sprache zur Koordination definiert werden soll. In eng gekoppelten Systemen mit geringen Kommunikationskosten kann auch feingranulare Parallelit¨at ausgenutzt werden. Dies ist ein Grund, weshalb in diesem Bereich fast ausschließlich mit Spracherweiterungen gearbeitet wird, die eine enge Verflechtung von parallelem Kontrollfluss und eigentlicher Berechnung erlauben. Für unsere Aufgabenstellung der verteilten Simulation haben wir es aber mit Plattformen zu tun, in denen Kommunikation um viele Größenordnungen teurer als die Berechnung ist. Hier ist es ganz natürlich, große sequentielle Blöcke auf einer programmtechnisch hohen Ebene zu ko- 1.2 Verwandte Arbeiten im Parallelisierungsbereich 7 ordinieren. Koordinationssprachen haben in der Regel einen sehr kleinen Umfang und erleichtern damit die Formalisierung der durch sie beschriebenen Konzepte. Die Anzahl der syntaktischen Konstrukte, ihre auf die Aspekte der Parallelisierung eingeschr¨ankte Semantik und ihre abgegrenzte Wechselwirkung mit dem Berechnungsteil ist überschaubarer als bei Spracherweiterungen. 1.2 Verwandte Arbeiten im Parallelisierungsbereich Zur Parallelisierung in funktionalen Programmen gibt es bereits eine große Zahl von Vorschl¨agen, Methoden und Systemen. Wir wollen hier die wichtigsten nennen und darstellen, inwieweit sie sich für unser Ziel der verteilten numerischen Simulation mit rekursiven Algorithmen eignen und welche zus¨atzlichen Anforderungen wir haben. Implizite Parallelisierung: Von den weit verbreiteten funktionalen Sprachen SML [Pau91] und Haskell [HF93, Tho96, PH97] existieren schon seit einiger Zeit parallele Varianten, mit denen Parallelit¨at automatisch erkannt wird, z. B. das parallele Haskell pH [Hil93, JH93, Mae96]. Es handelt sich im Grunde um Datenfluss-Sprachen, in denen Datenabh¨angigkeiten ermittelt werden und daraus die parallel bearbeitbaren Teilberechnungen gebildet werden. Als Auswertungsstrategie wird meist lenient evaluation verwendet, eine Form der strikten Auswertung mit nicht-strikten Fallunterscheidungen. Diese Sprachen sind aber zu unterscheiden von Datenfluss-Sprachen im engeren Sinn, die Programme auf Maschinenebene und meist für spezielle Datenfluss-Architekturen beschreiben [PT94]. Die Datenfluss-Sprachen Id [ANP89, Nik90] und SISAL [CFBO92, FMS 95] sind ebenfalls funktionale Sprachen. Ihre Kontrollstrukturen und Parallelisierungsstrategien basieren auf Iterationen und Array-Strukturen, w¨ahrend rekursiver Parallelismus nicht ausgenützt werden kann. Für unseren Anwendungsbereich benötigen wir aber in erster Linie rekursive Funktionen und baumartige dynamische verteilte Datenstrukturen, deren Komponenten sich gegenseitig nicht synchronisieren. Dies wird von diesen Sprachen nicht direkt unterstützt. Noch ein anderer Ansatz zur impliziten Parallelisierung funktionaler Programme ist die Transformation in einen Graph von Kombinatoren, also von variablenfreien Ausdrücken [Tur79]. Die Berechnung erfolgt dann mittels Graphreduktion, einem Verfahren, das viel inh¨arente Parallelit¨at bietet. Allerdings gibt es keine effizienten Implementierungen für verteilte Umgebungen [Pey89]. Problematisch ist bei impliziter Parallelisierung außerdem der Umstand, dass meist zu viele und zu feingranulare parallele Kontrollflüsse erzeugt werden, die nicht nur in verteilten Umgebungen nicht verwaltet werden können, sondern auch g¨angige Laufzeitsysteme für Threads überfordern. Ein Ausweg für die Implementierung w¨aren Laufzeitsysteme, 8 Einführung die die Erzeugung extrem leichtgewichtiger Threads erlauben. Eine solche Plattform bietet z. B. Cilk [BJK 96, Joe96, Sup98], das dann nicht als Programmier-, sondern als Zielsprache verwendet werden kann. Allerdings ist auch Cilk nur für gemeinsamen Speicher einsetzbar. In jüngster Zeit gibt es zwar Bestrebungen für Implementierungen von Cilk auf verteiltem Speicher, doch sind diese auf eine sehr leistungsf¨ahige Netz-Hardware angewiesen [Leb98]. Meist sind Systeme zur impliziten Parallelisierung nur für gemeinsamen Speicher konzipiert. Sie leisten nur die Zerteilung und Parallelisierung der Berechnung. Die Zuordnung von parallelen Teilaufgaben zu Prozessoren oder Rechnern kann nicht spezifiziert werden. Es ist dann Aufgabe des Zielsystems, etwa des Schedulers des Betriebssystems oder einer Thread-Bibliothek, neue parallele Berechnungen zu verteilen. Damit kommen sie für unseren Anwendungsbereich nicht in Frage, weil sie sich nicht für Rechnernetze eignen, wo sich zus¨atzlich das Problem der Verteilung und Lokalit¨at von Datenstrukturen stellt. Concurrent Constraint-Programmierung ist eine besondere Variante der impliziten Parallelisierung mit Logikprogrammen, bei der auch mit unvollst¨andigen Informationen Deduktionsschritte möglich sind, ¨ahnlich wie bei den hierarchischen Datenstrukturen in unseren einleitenden Beispielen. Vertreter sind Oz [MS96, SHW93] sowie Janus [SKL90], dessen semantische Fundierung in [SRP90] zu finden ist. Für Aufgaben aus dem Bereich des technisch-wissenschaftlichen Rechnens sind Logik- oder Constraint-Sprachen meist zu komplex und abstrahieren zu stark von der Berechnungsreihenfolge, da für einen numerischen Algorithmus meist schon ein Grobkonzept für die Ausführung und das Verhalten vorhanden ist. Datenparallelismus: Datenparallele Sprachen dienen dazu, Daten zu partitionieren und dann Funktionen (parallel) auf diese Datenblöcke anzuwenden [HL92, KPS94]. Dies ist das duale Paradigma zum Task- oder Prozess-Parallelismus, wo verteilt aufgerufene Funktionen mit den nötigen Daten versorgt werden. Die datenparallele Sprache NESL [BCH 94, BG96, Ble96] bietet als Grundstrukturen geschachtelte Arrays und Lastbalancierungsstrategien. Allerdings stoßen datenparallele Ans¨atze, sobald Datenabh¨angigkeiten auftreten, schnell an ihre Grenzen, werden ineffizient oder erlauben es nicht, solche Abh¨angigkeiten überhaupt auszudrücken. So fehlen auch in NESL die sprachlichen Möglichkeiten, Datenabh¨angigkeiten wie aus Beispiel 1.1 auf Seite 2 zu beschreiben. Spracherweiterungen: GpH [THM 96, LT97, HLPT98] ist ein Beispiel für eine minimale Spracherweiterung, aufbauend auf Haskell. Es werden nur die beiden zus¨atzlichen Operatoren und eingeführt, die angeben, ob strenge Datenabh¨angigkeit oder ein potentiell parallel auszuwertender Ausdruck vorliegt. Die explizite Verwendung von ist wichtig, weil Haskell einerseits eine Sprache mit verzögerter Auswertung ist, aber andererseits Teilberechnungen schon parallel angestoßen werden sollen, auch wenn 1.2 Verwandte Arbeiten im Parallelisierungsbereich 9 ihr Ergebnis noch von keinem anderen Ausdruck gebraucht wird (spekulative Auswertung). Eine komplexere Spracherweiterung für Haskell stellt Caliban [Kel89] dar, w¨ahrend Concurrent Clean [NSvP93] ein Mittelweg zwischen großer Ausdrucksmöglichkeit und Ein fachheit ist. In Concurrent Clean gibt es außer reinen Partitionierungsannotationen (für echt parallele Berechnung), und (für quasiparallele Berechnungen), wie sie im Prinzip auch GpH bietet, zudem die Möglichkeit, mit explizit zu spezifizieren, wo eine parallele Berechnung ausgeführt werden soll. Unter der Bezeichnung para-funktionale Programmierung finden wir Erweiterungskonzepte, etwa für Haskell [Hud91, Hud88], die ebenfalls explizite Platzierung von Prozessen durch einen Ausdruck der Form "! erlauben. Dabei gibt die Annotation mit der Prozessor-ID #"! an, auf welchem Prozessor der Ausdruck auszuwerten ist. Zus¨atzlich kann das Scheduling von Kontrollflüssen gesteuert werden, indem Ausdrücken Labels zugewiesen werden. In eine andere Richtung orientiert sich die Sprache Eden [BLOMP98], die Haskell um die explizite Beschreibung von Prozessen erweitert. Diese Prozesse werden funktional spezifiziert (Prozess-Abstraktion) und bei ihrer Erzeugung (Prozess-Instantiierung) durch Kan¨ale miteinander verbunden. Auf den Kan¨alen können Datenströme oder auch Kanalnamen ausgetauscht werden. Direkte Kan¨ale, sogenannte Bypasses, können dynamisch erzeugt werden. Allerdings muss diese Erzeugung explizit angegeben werden, im Gegensatz zu den von uns gewünschten impliziten direkten Verbindungen für Komponenten hierarchischer Datenstrukturen. Koordinationssprachen: Das Programm wird in zwei Einheiten aufgeteilt: Das Berechnungsmodell beschreibt den Algorithmus, das Kooridinationsmodell das Verhalten und die Parallelit¨at [GC92]. Diese Zweiteilung bringt Flexibilit¨at in den Entwurf. Beispielsweise können unterschiedliche Paradigmen unterstützt werden. Oft wird der algorithmische Teil in einer imperativen Sprache beschrieben, w¨ahrend die Koordination in einer deklarativen Sprache anzugeben ist. Zudem ist es einfacher und übersichtlicher, die Semantik der Koordinationsschicht zu untersuchen, weil sie von der zugrundeliegenden Hostsprache getrennt ist. Die bekanntesten Vertreter von Koordinationssprachen sind Linda [GC92, Wes92] und PCN [FOT92, FT93, FT94] sowie dessen Vorg¨anger STRAND [FT90]. W¨ahrend Linda auf einem (ggf. virtuell) gemeinsamen Speicherbereich für Objekte, dem Tupelraum, aufbaut, stellt PCN auf niedrigerer Ebene drei Konstrukte zur sequentiellen, parallelen und bedingten Komposition von Tasks bereit. Zu diesen Koordinationssprachen gibt es auch ausgereifte Implementierungen. Algorithmische Skeletons [Col98, DGTY95, Ski95] sind ein Sammelbegriff für effiziente Implementierungen von h¨aufig auftretenden parallelen Operationen auf rekursiven Datenstrukturen (Listen, B¨aume etc.). Genauer handelt es sich um Funktionen höherer Ordnung, die die Anwendung beliebiger Funktionen auf die einzelnen Elemente der Datenstrukturen festlegen. Im Grunde sind Skeletons sprachunabh¨angig und auch in Koordinations- 10 Einführung sprachen formulierbar, sofern die verwendeten Datentypen auf der Ebene der jeweiligen Sprache bekannt und zugreifbar sind. SCL [DGTY95] ist eine Methodik, die die parallele Koordination durch algorithmische Skeletons leistet. SCL stellt drei Arten von Skeletons zur Verfügung: Konfigurationen zur expliziten Datenverteilung, elementare datenparallele Funktionen (map, fold, filter etc.) und kontrollflussbasierte Funktionen. Vom Standpunkt der Koordinationssprachen ist SCL ein Ansatz auf sehr hohem Abstraktionsniveau und großer M¨achtigkeit. Nachteilig ist allerdings, dass es nur experimentelle Implementierungen auf Basis von Arrays gibt, etwa Fortran-S auf Basis von Fortran, aber keine mit komplexeren Datenstrukturen. Für unseren Anwendungsbereich stellt sich bei den vorhandenen Koordinationssprachen wieder das Problem, dass sie nicht darauf achten, dass Daten als Teil von Strukturen so weit wie möglich auf ihrem Ursprungsrechner gehalten werden und beim Versenden den direktesten Weg nehmen. Denn für die meist sehr großen Datenmengen in numerischen Anwendungen ist es nicht ausreichend, nur die funktionalen Abh¨angigkeiten aus einem Programmstück herauszufinden, sondern auch so viel Kommunikation wie möglich zu vermeiden. Je teurer die Kommunikation ist, desto wichtiger wird Datenlokalit¨at und Kommunikationsvermeidung, besonders auf Rechnernetzen. Statische Analyse: Sie hat natürlich den großen Vorteil, dass sie zur Laufzeit keinen Overhead erzeugt. Nachteilig sind aber Voraussetzungen, die oftmals nur die Optimierung stark eingeschr¨ankter Programme oder Anwendungen zulassen. Zur statischen Analyse von Baumstrukturen gehört beispielsweise die sogenannte Deforestation-Technik [Wad90, GLP93, Gil96] zur Elimination tempor¨arer B¨aume. Sie setzt strenge Linearit¨at voraus, schreibt also vor, dass das Ergebnis einer Ausdrucksberechnung nur einmal als Argument eines weiteren Ausdrucks verwendet werden darf. Die mehrfache Verwendung ist aber wichtig z. B. beim Top-down-Durchlauf von B¨aumen, wenn die Berechnungen in den Sohnknoten von den Ergebnissen des Vaterknotens abh¨angen und jeder Sohnknoten den Wert des Vaters verwenden muss. Der Diffusion-Ansatz [HTI99] erlaubt die Optimierung von parallelen Koordinationsfunktionen höherer Ordnung für die datenparallele Ausführung. Die gefundenen Transformationen können in allen parallelen Sprachen verwendet werden, die Funktionen höherer Ordnung unterstützen. Datenparallelismus ist aber, wie schon erw¨ahnt, meist zu einschr¨ankend für rekursive numerische Anwendungen, da oft Abh¨angigkeiten zwischen den Teildaten bestehen. Ein weiteres Problem statischer Analysemethoden ist, dass der Programmierer keine Kontrolle mehr darüber hat, wie das Programm tats¨achlich ausgeführt wird (es sei denn es handelt sich um sehr einfache Optimierungen), und damit auch kaum noch die Möglichkeit, selbst Optimierungen in den Programmtext einfließen zu lassen. 1.3 Aufgabenstellung 11 Explizite Parallelisierung: Dies ist wohl die ¨alteste Form der Parallelisierung funktionaler Sprachen. Zu erw¨ahnen ist das future-Konzept, z. B. in MultiLisp [Hal85, Hal90], das parallel auswertbare Ausdrücke kennzeichnet. Dabei besteht allerdings noch die Möglichkeit für das Laufzeitsystem zu entscheiden, ob tats¨achlich ein neuer Task für ein future-Konstrukt in Abh¨angigkeit von der Granularit¨at erzeugt wird [MH91]. In QLisp [GG98] wird diese Entscheidung durch benutzerdefinierte Pr¨adikate gef¨allt. Doch auch in aktuellen Arbeiten und Systemen hat die explizite Parallelisierung, u. a. aus Gründen der Effizienz, ihre Berechtigung. Auf vergleichsweise niedrigem Abstraktionsgrad muss beispielsweise in CML (Concurrent ML [Rep91, Rep98]) Kommunikation und Synchronisation der parallelen Prozesse von Hand programmiert werden, ebenso bei Distributed ML [Kru93]. Zusammenfassend stellen wir fest, dass zwar schon viele Methoden und Systeme für die Parallelisierung in funktionalen Programmen verfügbar sind. Jedoch werden unsere besonderen Forderungen nach Datenlokalit¨at, direkter Kommunikation und Aufbrechen von Datenabh¨angigkeiten nicht oder nur unzureichend erfüllt. Dennoch können wir aus den vorhandenen Arbeiten verschiedene Ans¨atze wie z. B. die Datenflussanalysen, algorithmische Skeletons und Techniken der Koordination aufgreifen, um mit einer eigenen Koordinationssprache FASAN diese Anforderungen zu erfüllen. 1.3 Aufgabenstellung Ziel der Sprache FASAN (Functions Applied to Substructured Data in Algorithms for Numerical Simulations) und ihrer Implementierung ist es, die Parallelisierung von Algorithmen der numerischen Simulation auf hierarchischen Datenstrukturen für die Ausführung auf Rechnernetzen zu automatisieren. Dabei soll die Effizienz einer von Hand vorgenommen Parallelisierung erreicht oder — bei Datenabh¨angigkeiten wie in den Beispielen 1.1 und 1.2 — sogar übertroffen werden. Rekursive Algorithmen auf hierarchischen Datenstrukturen spielen einerseits in modernen adaptiven numerischen Verfahren zur technisch-wissenschaftlichen Simulation eine zentrale Rolle, andererseits können sie gerade durch die Möglichkeit der Adaptivit¨at und resultierender Unregelm¨aßigkeit in den Datenstrukturen nur sehr mühsam von Hand für die verteilte Berechnung aufbereitet werden. Eine teilweise explizite Parallelisierung ist für FASAN erwünscht, damit der Programmierer seine Ideen und Erfahrungen verwerten kann. Daran scheint in verteilten Umgebungen allgemein auch kein Weg vorbeizuführen [Ham94]. Andererseits sollen Routineund Fleißarbeiten, die nur der Organisation der Parallelisierung, Kommunikation und Synchronisation dienen, auf jeden Fall eingespart werden. Wie oben erw¨ahnt, eignen sich funktionale Sprachen besonders für diesen Anwendungsbereich und zur einfacheren parallelen Beschreibung. Im Grunde soll FASAN eine Koordinationssprache sein, die Funktionen aus einer sequentiellen Hostsprache zur nebenl¨aufigen Ausführung konfiguriert. Als Hostsprachen sind C, C++, FORTRAN sowie 12 Einführung Java nötig. Denn es sollen große Programmteile aus bereits bestehendem Code wiederverwendet werden können, da der Anwender meist nicht bereit ist, auf Unterroutinen, in denen viel Entwicklungsarbeit steckt, zu verzichten. Für numerische Aufgaben stehen zudem hoch-optimierte Bibliotheken (BLAS, LAPACK etc.) zur Verfügung, die auch für neue Anwendung nutzbar sein müssen. Deshalb soll die funktionale Ebene, die ein FASAN-Programm beschreibt, lediglich der parallelen Komposition der sequentiellen Module dienen, obgleich einfache arithmetische Berechnungen auch direkt in FASAN formulierbar sein sollen. Verteilte Bearbeitung erfordert Konzepte zur verteilten Datenhaltung und für verteilte Datenstrukturen. Dabei ist bei den Verfahren in Anbetracht der hohen Kommunikationskosten besonderes Augenmerk auf Reduzierung der Kommunikation zu legen. Die Anforderung an FASAN ist die Bildung hierarchischer Datenstrukturen nach dem sogenannten Kabelbaum-Prinzip: Sie müssen unnötige Kommunikation, die durch eine bequemere textuelle Beschreibung zustande gekommen ist, eliminieren. Wesentliche Idee ist dabei, dass die Daten der Baumkomponenten direkt vom Sender zu den jeweiligen Empf¨angern gelangen, auch wenn es im Programmtext so erscheint, als liefen sie über verschiedene Funktionsaufrufe und u. U. auch verschiedene Rechner als Zwischenstationen. Eine weitere Anforderung ist die Unabh¨angigkeit der Knotenwerte. Ist der Wert in einem Baumknoten berechnet, so muss er entsprechenden Verbraucherfunktionen zur Verfügung stehen, ohne dass der komplette Baum aufgebaut ist. Hier w¨are die Programmierung mit konventionellen, wenig abstrahierenden parallelen Modellen wie Nachrichtenaustausch zu aufwendig und fehleranf¨allig. Gefragt ist also ein Verfahren, das die hierarchischen Datenstrukturen automatisch entsprechend der verteilten Ausführung verschickt, aber wirklich nur dorthin, wo sie für die Berechnung auch gebraucht werden. In numerischen Simulationen werden oft mit gleichen Ablaufstrukturen verschiedene Datens¨atze berechnet, etwa für mehrere aufeinanderfolgende Zeitschritte. Auch beim Lösen eines Gleichungssystems kann es interessant sein, nur die rechten Seiten zu variieren, was etwa in der Strukturmechanik verschiedenen Belastungen des zu untersuchenden Objekts entspricht. Auch innerhalb eines iterativen Lösers sind Ergebnisse einer Iteration als Argumente der folgenden Iteration zu verwenden. In prozeduralen Sprachen wird dies durch Schleifen ausgedrückt. Das funktionale Pendant sind endrekursive Funktionen. Hier ist es möglich, die Ergebnisse einer Iteration als Startwerte für die neue Iteration auf demselben Stack- und Speicherbereich zu verwenden. Die Lastverteilung soll benutzerdefiniert geschehen, weil für komplizierte Abh¨angigkeitsstrukturen die optimale Verteilung nicht automatisch gefunden werden kann. Dabei ist zu berücksichtigen, dass auch dynamische Lastverteilung auf zur Laufzeit ermittelte Zielrechner möglich sein muss und in FASAN entsprechende Schnittstellen vorzusehen sind. Als Ergebnis soll ein portables, wartbares und leicht anzuwendendes Werkzeug entstehen, das FASAN-Programme in eine ausführbare Form übertr¨agt, die diese Aufgaben bew¨altigt. Weiterhin soll die praktische Verwendbarkeit an einer größeren Anwendung aus dem Bereich der numerischen Simulation demonstriert werden. 1.4 Überblick über die Arbeit 13 1.4 Überblick über die Arbeit Im folgenden Kapitel 2 geben wir zun¨achst die formalen Grundlagen funktionaler Programmierung an. Es werden Möglichkeiten für die Beschreibung der operationellen Semantik mit Hilfe von Regelsystemen und — für den parallelen Fall — mit Hilfe von Prozessnetzen dargestellt. Daran schließt sich das Kapitel 3 über die Beschreibung der Syntax von FASAN an. Diese Syntax ist, ganz im Sinn von Koordinationssprachen, wenig umfangreich, erfasst aber die wesentlichen Aspekte funktionaler Sprachen, also Einfach-Zuweisungen, Rekursion und hierarchische Datenstrukturen. Im Vergleich zu den Vorg¨angerarbeiten werden hier außerdem Funktionen höherer Ordnung unterstützt. Als Beispielprogramme dienen Funktionen, die für Teilaufgaben der numerischen Simulation verwendet werden können und damit den unmittelbaren Bezug zum Anwendungsbereich von FASAN zeigen. Als Vorbereitung für die Spezifikation der Semantik enth¨alt das Kapitel die Transformation in eine abstrakte Syntax. Kapitel 4 führt das mathematische Modell der Datenflussgraphen ein. Dieses Modell eignet sich sehr gut als operationelle parallele Semantik von FASAN-Programmen und wurde informell u. a. bereits in [EPZ96, EP96, EP98, Pfa97] vorgestellt. Hier wird Wert auf eine durchg¨angige formale Abbildung von FASAN-Programmen in abstrakter Syntax auf (zun¨achst statische) Datenflussgraphen gelegt. Das dynamische Verhalten der Programme wird dann durch die ebenfalls formal eingeführten Konzepte der Auswertung und der Entfaltung von Funktionsknoten im Datenflussgraphen beschrieben. Das Kapitel schließt mit dem Nachweis von Sicherheitsbedingungen (Verklemmungsfreiheit, Ausschluss von Laufbedingungen) und Effizienzaspekten (direkte Kommunikation, Datenlokalit¨at, höherer Parallelit¨atsgrad). Für den praktische Einsatz beschreibt Kapitel 5 eine Implementierung von FASAN. Wir werden von Erfahrungen aus vorausgegangenen Implementierungen [Rau92, Ebn94] berichten und die Gründe für die Wahl von Java als Implementierungsplattform darstellen. Die nachher vorgeschlagene Übersetzung von FASAN in Java vermeidet die explizite dynamische Generierung von Datenflussgraphen. Die Idee ist, durch eine ausreichende Zahl von Kontrollflüssen dafür zu sorgen, dass die von FASAN garantierte Parallelit¨at ausgenützt wird. Das neue FASAN-System ist dadurch wesentlich flexibler und benutzerfreundlicher als die Vorg¨angerversionen. Eine größere Anwendung, das Verfahren der rekursiven Substrukturierung zur numerischen Lösung elliptischer linearer partieller Differentialgleichungen aus den Gebieten der Strukturmechanik und Strömungsmechanik wird in Kapitel 6 dargestellt. Für dieses Verfahren schlagen wir nach der Beschreibung der Ablauf- und Organisationsstrukturen drei neue Lösungsvarianten vor, die für die verteilte Ausführung gewisse Vorteile gegenüber dem bisherigen Lösungsverfahren bringen. Außerdem geben wir die FASANImplementierung der zugehörigen Koordinationsschicht an. Das Kapitel endet mit der Kostenabsch¨atzung der verschiedenen Lösungsvarianten und einer darauf aufbauenden 14 Einführung und in FASAN übernommenen dynamischen Lastverteilung. Kapitel 7 w¨ahlt konkrete Anwendungsbeispiele für die numerische Simulation aus. Es werden sowohl numerische Ergebnisse der neuen Lösungsvarianten, insbesondere das Konvergenzverhalten und die Rechenkosten, experimentell ermittelt als auch deren Laufzeiten auf Rechnernetzen durch Koordination mit dem FASAN-System aufgeführt. Schließlich geben wir in Kapitel 8 nochmals einen kurzen Überblick über die Methoden und Ergebnisse der Arbeit sowie Möglichkeiten für aufbauende und erg¨anzende Untersuchungen. 15 Kapitel 2 Grundlagen funktionaler Programmierung und Semantik Diese Kapitel fasst formale Grundlagen zusammen, die wir im Weiteren für die Beschreibung von Rekursion, hierarchischen Datentypen und Funktionen höherer Ordnung brauchen. Wir beschreiben zun¨achst genauer die Merkmale funktionaler Sprachen anhand von Beispielen (Abschnitt 2.1). Sodann geben wir die formale Beschreibung funktionaler Programme und ihrer operationellen Semantik an (Abschnitt 2.2). Für die Modellierung verteilter Ausführung werden wir sp¨ater Prozessnetze verwenden. Die nötigen Grundlagen dafür stellen wir in Abschnitt 2.3 zusammen. 2.1 Kennzeichen funktionaler Sprachen In den modernen funktionalen Sprachen stoßen wir fast durchwegs auf Konzepte, die eine gewisse Abgrenzung zu prozeduralen Sprachen erlauben. Darin stecken gleichzeitig die wesentlichen Ideen funktionaler Programmierung, weshalb wir sie hier im Einzelnen erl¨autern. Das Fehlen von Seiteneffekten ist das wichtigste Merkmal funktionaler Sprachen. Es gibt keinen Programmzustand; eine Funktion liefert bei mehrmaligem Aufruf immer das gleiche Ergebnis. Die Seiteneffektfreiheit ist der Grund für die referentielle Transparenz, d. h. der einer Variablen einmal zugewiesene Wert ¨andert sich nicht im Laufe des Berechnungsvorgangs. Somit wird die Bearbeitungsreihenfolge von Berechnungstermen, zwischen denen keine expliziten Datenabh¨angigkeiten bestehen, beliebig und die parallele Bearbeitung möglich. Da sich eine Funktion allein durch die Abbildung ihrer Eingabedaten auf die Ausgabe beschreiben l¨asst, ist der Nachweis von Programmeigenschaften meist leichter durchführbar als in prozeduralen Sprachen. Es gibt funktionale Sprachen wie Lisp mit seinen vielen Dialekten [SG86, KCR98], die zur Übersetzungszeit keine Typisierung vornehmen. Ein polymorphes Typsystem hat 16 Grundlagen funktionaler Programmierung und Semantik sich mittlerweile aber für funktionale Sprachen bew¨ahrt, da Typprüfung zurÜbersetzungszeit schon viele Fehler abfangen kann. Moderne funktionale Sprachen wie SML [Pau91] oder Haskell [HF93, Tho96, PH97] erlauben, Typvariablen einzuführen, um Datentypen zu parametrisieren (parametrischer Polymorphismus). Beispiel 2.1 In Haskell schreiben wir beispielsweise data tree a = Leaf a | Branch (a, tree a, tree a) und haben damit eine generische Baumstruktur mit Knoten eines beliebigen, aber innerhalb eines Baums festen Typs a definiert. Haskell übernimmt ein Konzept aus der objekt-orientierten Welt für Datentypen, n¨amlich das Vererbungskonzept, in dem Datentypen eine Hierarchie der Spezialisierung bilden. In einem Ausdruck darf dann ein Argument immer auch einen spezielleren Typ haben als der formale Parameter. Hier ist anzumerken, dass parametrischer Polymorphismus, also Datentyp-Definitionen mit Typvariablen, in objektorientierten Sprachen nicht selbstverst¨andlich ist, z. B. nicht in Java [Bis98]. Er kann allerdings durch Vorübersetzer erreicht werden; bei Java etwa sorgen Pakete namens Pizza [OW97b] oder GJ [BOSW98] für parametrischen Polymorphismus. Pizza ist eine Obermenge der Java-Sprache und dazu gedacht, Java durch Einführung von Templates (¨ahnlich wie C++) besonders bei der Wiederverwendung von algebraischen Datentypen flexibler zu machen. Automatische Speicherverwaltung von dynamisch erzeugten Datenstrukturen ist zwar Standard in funktionalen Sprachen, aber kein eindeutiges Kennzeichen für sie. Die sogenannten garbage collectors finden sich auch in zahlreichen objektorientierten Sprachen, etwa Java oder Eiffel, da sie den Aufwand beim Programmieren und bei der Fehlersuche entscheidend verringern. Funktionen sind Bürger 1. Klasse“, d. h. Funktionen sind als Werte von Variablen so” wie als Argumente und Ergebnisse anderer Funktionen erlaubt. Dies führt dann zu Funktionen höherer Ordnung, wobei die Ordnung durch den Typkonstruktor für Funktions typen wie folgt festgelegt ist: Die Ordnung eines Typs ist induktiv definiert: falls Grundtyp oder Datentyp ohne Funktionskomponente falls und Wenn eine Funktion wieder Funktionen als Parameter bekommt oder zurückgibt, spiegelt sich dies unmittelbar im Typ wider, der dieser Funktion zugeordnet ist. Betrachten wir das folgende Beispiel. 2.1 Kennzeichen funktionaler Sprachen Beispiel 2.2 17 Mit den Abkürzungen type R = Float type F = (R,R)->R hat eine zweidimensionale reellwertige Funktion u in Haskell den Typ F, geschrieben u::F. Differentialoperatoren bilden Funktionen vom Typ F auf Funktionen gleichen Typs ab. Der zweidimensionale Laplace-Operator (in der Analysis meist als geschrieben) erh¨alt in seiner diskretisierten Form den Typ F->F. Oder anders ausgedrückt: Laplace liefert, angewandt auf eine Funktion (R,R)->R, wieder eine Funktion vom Typ (R,R)->R. Dies entspricht der vollst¨andigen Klammerung ((R,R)->R)->((R,R)->R). Der Typkonstruktor -> ist also rechtsassoziativ. Verwenden wir die weniger abgekürzte Form dieses Typs, Laplace :: F->(R,R)->R kann Laplace auch als Funktion mit zwei Argumenten aufgefasst werden: Dabei ist Laplace eine Funktion, die als erstes Argument eine differenzierbare Funktion vom Typ F erwartet, z. B u, und als n¨achstes Argument eine Auswertungsstelle (Typ (R,R)), an der der Funktionswert berechnet werden soll. Ein generischer numerischer Löser NumSolve, der für mehrere Differentialoperatoren einsetzbar ist, bekommt einen solchen Operator, der vom Typ der Ordnung 2 ist, als Argument und wird damit zu einer Funktion vom Typ der Ordnung 3, also NumSolve:: (F->F)->(R,R)->(R,R) Viele prozedurale Sprachen erlauben keine Funktionen als Ergebnis anderer Funktionen, so auch nicht C, C++ und Java. Für objektorientierte Sprachen existieren Aufs¨atze auf die Grundsprache, die Funktionen erster Klasse“ ermöglichen; für Java leistet dies unter ” anderen das Paket Pizza [OW97b]. Als Auswertungsstrategie gibt es zun¨achst die (in prozeduralen Sprachen durchwegs anzutreffende) eager evaluation durch call by value. SML und Lisp sind solche — als strikt bezeichnete — Sprachen und werten die Argumente vor einem Funktionsaufruf vollst¨andig aus. Manche funktionale Sprachen verwenden dagegen verzögerte Auswertung (lazy evaluation); zu dieser Familie gehören Gofer und Haskell. Die Terme, die nicht weiter ausgewertet werden, sind Anwendungen von Datentypkonstruktoren. Sie stellen die Normalformen dar, genauer schwache Kopfnormalformen, weil sie nur den Kopf“, ” also den Anfang oder die oberste Ebene der Datenstruktur berechnen. Beispiel 2.3 Form In Haskell ist mit dem Listenkonstruktor (:) eine Funktionsdefinition der squares :: Int -> [Int] squares x = (x*x):squares(x+1) nicht fehlerhaft, da Komponenten von Datenstrukturen — hier der Ausdruck squares(x+1) — erst ausgewertet werden, wenn eine andere Funktion darauf zugreift. 18 Grundlagen funktionaler Programmierung und Semantik In parallelen Systemen ist die ausschließlich verzögerte Auswertung allerdings nicht immer sinnvoll [THM 96]. Wenn ein neuer paralleler Kontrollfluss mit einem Teilterm eines zu berechnenden Ausdrucks erzeugt wird, muss durch spezielle Operatoren dafür gesorgt werden, dass der Teilterm spekulativ ausgewertet wird. Die Rekursionstiefe der spekulativen Auswertungen kann man zum Beispiel von den Systemressourcen, speziell der Prozessorenanzahl, abh¨angig machen [HLPT98]. 2.2 Formale Beschreibung und operationelle Semantik funktionaler Sprachen Wie wir bereits gesehen haben, wird der Typ von Funktionen mit dem Typkonstruktor angegeben. Bei Funktionen höherer Ordnung können Terme, die mit aufgebaut sind, auch geschachtelt sein. Formal sind es Typausdrücke: Definition 2.4 Sei eine Menge von Typsymbolen. Dann ist die Menge ausdrücke induktiv definiert: Jedes Typsymbol ist ein Typausdruck: . Sind , so ist auch der Ausdruck der Typ- . Programmiersprachen bieten typischerweise mehrere syntaktische Varianten, um Deklarationen, Kontrollstrukturen, Berechnungsterme etc. auszudrücken. Ziel einer abstrakten Notation ist es, nur den Kern einer Sprache zu repr¨asentieren, ohne syntaktische Erg¨anzungen, die zwar dem Programmierkomfort dienen, aber nicht weiter untersucht werden müssen, weil sie auf diesen Sprachkern zurückgeführt werden können. Neben den Typsymbolen , die wir gerade benutzt haben, sind dazu nur noch Funktionssymbole nötig: besteht aus einer Menge von Typsymbolen (den Definition 2.5 Eine Signatur Grundtypen) sowie einer Menge von Funktionssymbolen. Für die Funktionssymbole ist zus¨atzlich eine Abbildung definiert, die jedem Funktionssymbol einen Typausdruck (oder Funktionstyp) zuordnet. Statt schreiben wir meist kürzer oder . Fast immer wird eine algorithmische Signatur Verwendung finden, die Fallunterscheidungen ermöglicht: "! #%$'&)( &)*+,& .- ./ 10.2 Definition 2.6 Eine Signatur Mit den Symbolen aus dieser Signatur heißt algorithmisch, wenn können wir Terme bilden: 2.2 Formale Beschreibung und operationelle Semantik funktionaler Sprachen 19 Meist verwenden wir für Terme die Bezeichnung Ausdrücke. Die Terme ohne Variablen und Funktionssymbole aus heißen Grundterme. Definition 2.7 Die Menge der Terme mit Variablen über einer Signatur ist induktiv definiert: Alle Variablen sind Terme: . Jedes nullstellige Funktionssymbol aus ist ein Term: , so ist für jedes Sind , aus auch ein Term in , 0 0 1 Definition 2.8 Die Funktion ermittelt die in einem Term vorkommenden Variablen, wobei für für , und für , , und 00 0 Ein technisches Hilfsmittel, von dem wir sp¨ater oft Gebrauch machen werden, sind endliche Sequenzen von Elementen aus . Die Menge dieser Sequenzen bezeichnen wir mit . Namen für Sequenzen versehen wir ebenfalls mit einem Stern, z. B. . Zum Aufbau von Sequenzen verwenden wir einen Konkatenationsoperator sowie einen Operator zur Formierung einer Sequenz aus ihren Elementen. Die leere Sequenz ist . Analog zu Mengen definieren wir noch ! #" !$" &%' !$" / 0. !()"*+( ( -, ( Bisweilen werden wir endliche Sequenzen auch als Tupel für Funktionsargumente ver wenden, also ! " gleichbedeutend mit . Mit Sequenzen lassen sich Operationen auf Listen von Symbolen bequem ausdrücken, zum Beispiel die Typisierung .( ergibt für eine Sequenz von einer Parameterliste. Die Abbildung 0 Variablen die Sequenz der diesen Variablen zugeordneten Typen: 0 0 0 .( !$" !$" ! "* / !" *" 0 / Solche induktive Definitionen mathematischer Abbildungen werden wir in dieser Arbeit sehr h¨aufig verwenden. Durch die Rekursion über den strukturellen Aufbau einer Sequenz, die selbst endlich ist, ist sofort klar, dass die Abbildung auch wohldefiniert ist. 1 Mit 0.13254 bezeichnen wir die Potenzmenge einer Menge 2 . 20 Grundlagen funktionaler Programmierung und Semantik In Programmen können Rekursionen wesentlich allgemeinere Form haben. In funktionalen Sprachen bildet Rekursion die Grundlage der Berechnungsvollst¨andigkeit, deshalb legen wir eine Möglichkeit fest, dies notationell auszudrücken. Dazu erweitern wir eine algorithmische Signatur um die Formeln zu : Definition 2.9 Eine rekursive Programmdefinition ist eine Sequenz von Termpaaren ! " " 0 0 . Jeder Formel aus einer rekursiven Programmdefinition können zuordnen und deshalb selbst als Funktionswir den Funktionstyp wobei für jedes i mit die Form hat und der Ausdruck aus sein muss mit symbol auffassen. Da diese Art von Rekursion nicht mehr notwendig eine einfache strukturelle Rekursion ist, sondern von allgemeineren Abbruchbedingungen abh¨angt, muss die Bedeutung einer rekursiven Programmdefinition festgelegt werden, die fehlerhafte oder nichtterminierende Programme identifiziert. Bei funktionalen Programmen, die ja beschreiben, was berechnet werden soll, aber nicht festlegen, wie genau bei der Berechnung vorgegangen wird, ist in erster Linie das Ergebnis wichtig, das die Auswertung von Ausdrücken liefert. Dies wird durch eine denotationelle Semantik für ein rekursives Programm beschrieben. Eine solche Semantik ist die Fixpunkt-Deutung für die aus den Rekursionsgleichungen gebildeten Funktionalgleichungen, wie sie in [Bro92, Bes95, Thi94] zu finden ist. Damit wird festgelegt, welcher mathematischen Funktion ein rekursives Programm entspricht, also welche Ergebnisse das Programm liefert. Wird allerdings die parallele Abarbeitung funktionaler Programme betrachtet, interessiert auch, wie die Auswertungen vonstatten gehen und wie die Teilberechnungen verteilt werden. Wie der tats¨achliche Berechnungsvorgang l¨auft, beschreibt eine operationelle Semantik (auch natürliche Semantik genannt), die durch Regelsysteme angegeben wird. Ein Regelsystem setzt Terme mit ihren Normalformen in Relation. Dazu benötigen wir eine -Algebra, die jedem Typkonstruktor und jedem Funktionssymbol aus der Signatur eine Bedeutung gibt: über einer Signatur besteht aus Definition 2.10 Eine -Algebra sowie einer Interpretation der Funktionssymbole aus , Tr¨agermengen eine Funktion wobei für die Interpretation ist. % % Im -Kalkül [Bar84, Thi94] wird von der -Algebra der -Terme Gebrauch gemacht, um eine operationelle Semantik durch syntaktische Umformungen zu beschreiben. Wir 2.2 Formale Beschreibung und operationelle Semantik funktionaler Sprachen 21 benötigen eine -Algebra, um eine rekursive Programmdefinition zu einem kompletten Programm zu machen: ist eine rekursive ProgrammdefinitiDefinition 2.11 Ein rekursives Programm on und eine -Algebra über einer algorithmischen Signatur . Sei im Folgenden ein rekursives Programm. Die in den Tr¨agermen gen aus enthaltenen Konstanten bilden die Normalformen der Berechnungsterme aus über , weil sie nicht weiter reduziert werden können. Damit kann eine Berechnungsrelation als operationelle Semantik für das rekursive Programm angegeben werden. Das dazu verwendete Regelsystem legt fest, ob für das Urteil bewiesen werden kann. Wenn ein einen Ausdruck solcher Beweis existiert, bedeutet das, dass der Wert von ist, und jeder Beweisschritt entspricht dann einem Berechnungsschritt. Existiert kein Beweis, ist der Wert der Berechnung , was als fehlerhafte oder nicht-terminierende Berechnung zu interpretieren ist. Es sind folgende Regeln nötig: % 1. Normalformen können nicht reduziert werden: falls (2.1) 2. Die Grundfunktionen sind strikt, deshalb werden die Argumente vor dem Einsetzen zuerst ausgewertet: für und falls (2.2) 3. Auch die rekursiv definierten Funktionen werden strikt ausgewertet: für und falls 4. Einzig die bedingten Ausdrücke sind nicht-strikt; zuerst wird die Bedingung wertet und abh¨angig von ihrem Ergebnis nur einer der beiden Zweige: - / ! %# $'&)( / &)*"+ & / 0 "! #%$ &)( / & *"+ & (2.3) ausge- (2.4) Dieses Regelsystem ist deterministisch, da für jede Form eines Berechnungsterms nur eine einzige Regel anwendbar ist. 22 Grundlagen funktionaler Programmierung und Semantik Eine nicht-strikte Semantik entsteht, wenn wir statt der Regel (2.3) die Terme zuerst reduzieren, sondern gleich in den Ausdruck einsetzen (call by name): für und falls nicht (2.5) Die mögliche Parallelit¨at bei der Reduktion eines Berechnungsterms ergibt sich implizit durch alle unabh¨angigen Teilb¨aume und Zweige des Ableitungsbaums. Soll jedoch wie im Fall von FASAN eine Sprache untersucht werden, die speziell der Parallelisierung und der verteilten Koordination dient, sollte das parallele Verhalten auch explizit modellierbar sein. Dazu eignen sich Prozessnetze wesentlich besser, die wir im n¨achsten Abschnitt beschreiben. 2.3 Parallele Semantik mit Prozessnetzen Eine Beschreibungsmöglichkeit parallelen Verhaltens verteilter Systeme liefern PetriNetze in ihren vielen Varianten [Rei85, RW91, Jen97]. Sie verbinden in idealer Weise die Formalisierung paralleler Abl¨aufe mit der Anschaulichkeit, die aus ihrer graphischen Darstellung resultiert. Um funktionale Programme mit Petrinetzen beschreiben zu können, wird jedem Funktionssymbol in einem Berechnungsterm eine Transition, also ein Knoten im Netz, zugeordnet. Die genaue Transformation von Programmen in Petri-Netze verschieben wir auf Kapitel 4. Dort ist die angestrebte Semantik eines Programms in der Koordinationssprache FASAN ein Datenflussgraph, der als Abart von Petri-Netzen gesehen werden kann. Wir wollen hier die grundlegenden und in dieser Arbeit benötigten Definitionen von Petri-Netzen geben, die bei Vertrautheit mit der Netz-Terminologie auch übersprungen werden können. , wobei und Definition 2.12 Ein Petri-Netz oder kurz Netz ist ein Tripel disjunkte Mengen sind und . wird Verbindungsrelation genannt, die Elemente von Plätze und die Elemente von Transitionen.2 Die Menge der Netze bezeichnen wir mit . % % ( Die transitive Hülle der Flussrelation wird geschrieben. Anstelle von schreiben wir auch kurz . Wir führen noch eine Schreibweise ein, um alle Vorg¨anger und Nachfolger einer Transition oder eines Platzes bezeichnen zu können: Definition 2.13 Für ein heißt die Menge Vorbereich von , die Menge Nachbereich von . ( ( ( ( ( 2 Ein Petri-Netz ist ein gerichteter bipartiter Graph. Oft wird die Verbindungsrelation auch Flussrelation genannt und mit bezeichnet. Die Plätze werden in der Literatur auch Stellen genannt und für meist geschrieben; für die Menge der Transitionen wird normalerweise geschrieben. Um in den folgenden Kapiteln aber Verwechslungen mit syntaktischen Kategorien, speziell mit Mengen von Funktions-, Selektor- und Typensymbolen zu vermeiden, werden in dieser Arbeit durchgehend die Namen verwendet. 1 4 2.3 Parallele Semantik mit Prozessnetzen PSfrag replacements erzeugen 23 ablegen entnehmen verbrauchen Abbildung 2.1 Graphische Darstellung eines einfachen Netzes für ein Erzeuger-VerbraucherSystem mit einelementigem Puffer. , die jedem Platz Die Markierung eines Netzes ist die Abbildung eine Anzahl von sogenannten Datenelementen zuordnet. Die Menge aller Markierungen eines Netzes bezeichnen wir mit . Das dynamische Verhalten eines Netzes wird durch das Schalten von Transitionen beschrieben: , wenn für alle . gilt: Eine Transition schaltet oder feuert bei einer Markierung , in Zeichen , und erzeugt dabei eine neue Markierung , wobei definiert ist durch falls falls sonst Definition 2.14 aktiviert ein Netze lassen sich graphisch darstellen. Üblicherweise werden die Pl¨atze durch Kreise und die Transitionen durch Quadrate oder Rechtecke dargestellt. Jede Markierung wird als gefüllter Kreis im Platz gezeichnet. So ergibt sich für ein einfaches ErzeugerVerbraucher-System die Darstellung von Abb. 2.1. Die sequentielle Semantik eines Netzes wird durch Ausführungen beschrieben: Definition 2.15 Die Folge von ausgehende sequentielle Ausführung, wenn für alle . mit mit heißt eine gilt: heißt Folgemarkierung von , wenn eine von ausgehende endliche Ausführung existiert, die in überführt. Die Menge der Folgemarkierungen von wird mit bezeichnet. " Definition 2.16 Ein Platz eines Netzes mit Anfangsmarkierung n-beschränkt, wenn für alle Folgemarkierungen gilt: . heißt sicher, wenn alle 1-beschr¨ankt sind. Das Netz " heißt Parallele Programme, insbesondere diejenigen, die mit FASAN beschrieben werden können, bestehen immer aus sequentiellen Komponenten. Wenn ein solches Programm als Netz modelliert werden soll, sind wir daran interessiert, Konflikte beim Zugriff auf die Pl¨atze zu vermeiden. Jede sequentielle Komponente, als Transition im Netz modelliert, soll ohne Konflikte mit anderen Komponenten auf ihren Vor- und Nachbereich zum 24 Grundlagen funktionaler Programmierung und Semantik PSfrag replacements Abbildung 2.2 Graphische Darstellung eines einfachen Prozessnetzes: Die Transitionen und sind parallel. Die Pl¨atze und werden durch die Transition ihres gemeinsamen Nachbereichs synchronisiert. Zweck des Lesens und Schreibens von Datenelementen zugreifen können. Dies kann im Modell der Netze dadurch erreicht werden, dass jeder Platz einen höchstens einelementigen Vor- und Nachbereich hat. Definition 2.17 Ein Netz heißt konfliktfrei, falls für alle gilt: und . Für die Beschreibung einer parallelen Ausführung eines Netzes ist es wichtig, Puffer und Transitionen auf ihre wechselseitige Unabh¨angigkeit vergleichen zu können. Definition 2.18 In einem Prozessnetz ist eine Transition von einer Tran abhängig, wenn sition und wenn also Zeichen (für concurrent). . Wenn nicht von abh¨angt und nicht von , , so heißen und parallel oder unabhängig, in Dieselbe Definiton kann auch auf Pl¨atze bezogen werden. Ein Schnitt durch ein Netz ist eine maximale Menge von paarweise unabh¨angigen Pl¨atzen, wenn also für alle , gilt: und für alle gibt es ein mit . Definition 2.19 Ein Netz mit Startmarkierung heißt Prozessnetz, wenn es konfliktfrei ist, die transitive Hülle irreflexiv (und damit das Netz zyklenfrei) ist, ein Schnitt ist und alle Elemente erreichbar sind, gilt: d. h. wenn für alle und ein . oder Prozessnetze eignen sich besonders gut für die Beschreibung parallelen Verhaltens von Systemen [Bes95, Rei98]. Dazu werden oft die Transitionen mit Aktionsnamen versehen. 2.3 Parallele Semantik mit Prozessnetzen 25 Eine parallele Ausführung wird dann durch Elemente aus dargestellt, wobei beliebig viele paarweise parallele Transitionen aus zwischen einer Markierung und ihrer Folgemarkierung stehen können. Schließlich benötigen wir noch den Begriff der Synchronisation: Zwei Pl¨atze ) synchronisiert, weil Datenelewerden durch einen gemeinsamen Nachbereich ( mente in ihnen aufeinander warten müssen, bevor sie gemeinsam weitergeschaltet werden. Wenn ein Programm in Form eines Prozessnetzes vorliegt, können wir damit sein paralleles Verhalten und seine Synchronisationspunkte explizit beschreiben und erkennen. Was uns noch fehlt, ist die Transformation eines funktionalen Programms in ein Prozessnetz, um explizit das parallele Verhalten des Programms festlegen zu können. Diese Aufgabe stellen wir aber bis Kapitel 4 zurück und führen im n¨achsten Kapitel zuerst eine konkrete Syntax für die Sprache FASAN ein, in der wir dann praktische Anwendungen schreiben können. 26 Grundlagen funktionaler Programmierung und Semantik 27 Kapitel 3 Die funktionale Koordinationssprache FASAN Wir haben uns in Abschnitt 1.2 für die Einführung einer eigenen funktionalen Koordinationssprache namens FASAN (Functions Applied to Substructured Data in Algorithms for Numerical Simulations) entschieden, die die Programmierung verteilter numerischer Simulation unterstützen soll. Besonders zu berücksichtigen sind die in diesem Umfeld entstehenden große Datenmengen, die grobgranulare Parallelisierung sowie spezielle Datenabh¨angigkeitsstrukturen, die sich aus rekursiven Beschreibungen ergeben. Die konkrete Syntax, die im Abschnitt 3.1 eingeführt wird, ist direkt zum Programmieren in FASAN bestimmt. Ein FASAN-Programm besteht im Wesentlichen nur aus Datentypdefinitionen sowie Funktionsdefinitionen mit Fallunterscheidungen und Funktionsanwendungen. Zum genaueren Studium von Programmeigenschaften und insbesondere zur Angabe der im n¨achsten Kapitel vorgestellten Datenfluss-Semantik ist eine abstrakte, einfachere Syntax vorteilhaft, wie sie in Abschnitt 3.2 vorgeschlagen wird. Die Transformation von konkreter in abstrakte Syntax bildet den zweiten Teil dieses Kapitels. 3.1 Sprachbeschreibung Obwohl es in der Informatik viele hochsprachliche Ans¨atze mit starkem Abstraktionsgrad von der Implementierungsplattform gibt, haben sie oft (noch) keinen Eingang in praktische Anwendungen gefunden. Dies liegt nicht zuletzt daran, dass die Einarbeitung in diese Konzepte recht mühsam und aufwendig sein kann. Um als Konzept der Informatik Aussicht auf Akzeptanz und Anwendung zu haben, ist es wichtig, möglichst wenige und möglichst vertraute Elemente aus bekannten Verfahren, Methoden oder Systemen zu übernehmen. Dies wird auch hier für die konkreten Syntax von FASAN versucht, die sich vom Erscheinungsbild an die prozedurale Programmierung anlehnt, denn im Bereich des technisch- 28 Die funktionale Koordinationssprache FASAN wissenschaftlichen Rechnens dominieren nach wie vor FORTRAN und C. Funktionsargumente werden geklammert, Ergebnisparameter von Funktionen benannt. Funktionsrümpfe sind vom Stil her eher zuweisungs- als applikationsorientiert. Des Weiteren wird auf Pattern-matching-Mechanismen und auf die Verwendung von gleichungssystem-artigen Rekursionsschemata zur Definition von Funktionen verzichtet. Insgesamt achten wir also auf eine Notation, die n¨aher an prozeduralen Sprachen steht als an Notationen im Stil von SML, Haskell oder des -Kalküls. Denkbar w¨are auch, sich streng an eine eingeschr¨ankte Java-, C- oder C++-Syntax zu halten. Der entsprechende FASAN-Compiler müsste zus¨atzlich durch Kontextbedingungen die Eigenschaft der Einfachzuweisung an Variablen (Single Assignment) überprüfen. Die von FASAN direkt verwendeten Sprachkonzepte sind Funktionsdefinitionen (auch höherer Ordnung), Datentypdefinitionen und Ausdrücke. Als Meta-Sprache verwenden wir eine erweiterte BNF-Notation: / [ ] [ ] [ ] [ ] [ ] / Alternative oder optionaler Teil ein- oder mehrmalige Wiederholung von null- oder mehrmalige Wiederholung von [ ] (Wiederholung mit Trennzeichen ) [ ] , leeres Wort Identifikatoren in FASAN entsprechen in ihrer Syntax Identifikatoren in Java [AG98] und dürfen zus¨atzlich Apostrophe (’) enthalten. Im Folgenden verwenden wir die Nonterminale ( ! für Variablennamen, für Funktionsnamen, für Konstruktornamen, für Selektornamen und für Typnamen. Für den Variablennamen dürfen auch numerische Konstanten eingesetzt werden (z. B. 9.81e+1), für Typnamen auch Felder (z. B. int[]). Variablen- und Typnamen sind innerhalb einer Funktionsdefinition ab der Stelle ihres ersten Vorkommens sichtbar, alle übrigen Namen in und nach ihrer Definition. Ein FASAN-Programm besteht dann aus einer Sequenz von Funktions- und Typ-Deklarationen. 3.1 Sprachbeschreibung ] .( " .( .( ::= [ 29 Weiterhin kann ein FASAN-Programm zus¨atzlichen Leerraum (Tabulatoren, Leer- und Neue-Zeile-Zeichen) sowie Kommentare (zwischen /* */ oder zwischen // und Zeilenende) enthalten. 3.1.1 Externe Funktionen und externe Typen Mit externen Funktionen werden sequentielle Funktionen aus der Hostsprache, also aus externen Programmbibliotheken, Klassen- oder Objektdateien eingebunden. Die Signatur einer solchen externen Funktion wird im FASAN-Programm vereinbart durch: ::= ( [ [ ] ] [ -> [ [ ] ] ] extern [ Java C C++ FORTRAN ] Da wir FASAN in Java übersetzen werden, können Java-Methoden ohne weitere Maßnahmen aufgerufen werden. Für C, C++ und FORTRAN ist eine Schnittstelle mit JNI (Java Native Interface, [Sun98]) zu schreiben. Diese Aufgabe könnte ein Interface-Generator automatisieren, der für C++-Objekte und C-Strukturen geeignete Java-Schnittstellenobjekte erzeugt. Mit anderen Sprachen wie Pascal und SML gibt es Kompatibilit¨atsprobleme mit den verschiedenen Laufzeitsystemen. Für die Verwendung von Argumenten externer Funktionen in der Hostsprache ist zu beachten, dass in Java, C und C++ Argumente, die nicht von Grundtypen 1 sind, und in FORTRAN alle Argumente durch Referenz an die aufgerufene Methode übergeben werden. Dadurch ist deren Wert modifizierbar. FASAN ist aber eine funktionale Koordinationssprache, und funktionales Verhalten wird auch von den externen Funktionen gefordert. Die eingebundene Funktion darf in der Regel auf keinen Speicherbereich, für den ein Argumentparameter eine Referenz liefert, schreiben. Ist die Referenz die einzige im momentanen Programmablauf, dann bleibt ein Beschreiben des zugehörigen Speicherbereichs ohne Seiteneffekte. Soll der so modifizierte Inhalt über das FASAN-Programm in weiteren Funktionen verwendet werden, muss für ihn 1 Das sind in Java die Typen boolean,byte,char,short,int,long,float,double. 30 Die funktionale Koordinationssprache FASAN auch ein Ergebnisparameter vorgesehen sein, sonst existiert der Wert anschließend aus Sicht des FASAN-Programms nicht mehr. Diese Wiederverwendung der Objekte ist eine leichte Effizienzsteigerung, weil die Neu-Allokation und die sp¨atere Entfernung des alten Objekts durch die Speicherverwaltung entfallen. Als Nachteil ist aber der Verlust der referentiellen Transparenz dieses Objekts zu berücksichtigen. Als einfache und nützliche externe Funktionen werden die arithmetischen Operatoren +,-,*,/, % (modulo), die artihmetischen Vergleichsoperatoren ==, <, >, <=, >= sowie die booleschen Operatoren && (and), || (or), ! (not) vordefiniert übernommen und können direkt in FASAN-Programmen in InfixSchreibweise verwendet werden. Vordefiniert sind auch die Java-Grundtypen und Arrays dieser Grundtypen. Weitere Typen können, analog zu externen Funktionen, aus den Hostsprachen importiert und als solche durch Deklaration im FASAN-Programm verfügbar gemacht werden: ::= extern Java [ isa [ ! ] ] .( Der Zusatz isa“ kennzeichnet den Typ als Spezialisierung der Typen ! in der Ver” erbungshierarchie der Hostsprache (bisher nur für Java unterstützt). Ein Argument spezielleren Typs darf für einen formalen Parameter allgemeineren Typs eingesetzt werden oder an eine Variable allgemeineren Typs zugewiesen werden. Tritt kein solcher Fall im FASAN-Programm auf, kann der Zusatz entfallen. Beispiel 3.1 Eine oft benötigte Datenstruktur ist die einer dünnbesiedelten Matrix, deren konkrete Struktur aber im FASAN-Programm verborgen bleibt: type SMat extern Java Als externe Funktion kann ein SOR-Gleichungslöser nützlich sein: function sor(A:SMat; b:double[])->x:double[] extern Java Um die prototypische Implementierung einfach zu halten, sind in FASAN keine in Java üblichen Type-Casts erlaubt; nur die Vererbungshierarchie wird durch die isaAnnotation externer Funktionen berücksichtigt. Auch auf weitergehende softwaretechnische Konzepte wie Typvariablen (parametrischen Polymorphismus wie in Pizza [OW97b]) wird hier der Einfachheit halber verzichtet. Auf unser Ziel der verteilten Berechnung würden diese Konzepte keinen Einfluss haben. Der FASAN-Compiler akzeptiert auch ein Programm, in dem alle externen Typdeklarationen von Variablen weggelassen werden und stattdessen eine (austauschbare) Konfigurationsdatei mit den Signaturen der Funktionen bereitgestellt wird. Damit kann ein FASAN-Programm auch unabh¨angig von einer Hostsprache geschrieben werden. 3.1 Sprachbeschreibung 31 3.1.2 Rekursive Funktionen Als rekursive Funktionen bezeichnen wir hier diejenigen Funktionen, die in FASAN direkt programmiert sind, im Unterschied zu den externen Funktionen. ::= [ [ ] ] [ -> [ [ ] ] ] ! ] ::= [ ( Eine Zuweisung kann eine Wertzuweisung an eine oder mehrere Variablen sein, wobei die rechte Seite der Zuweisung wieder aus einer Variablen oder einem Funktionsaufruf besteht und beliebig geschachtelt sein kann: ::= [ [ ] ] ::= ::= [ ] s muss gleich viele ErgebDer Funktionsaufruf auf der rechten Seite eines nisparameter haben wie auf der linken Seite Variablen stehen. Steht auf der rechten Seite der Aufruf einer Funktion ohne Ergebnis, so entf¨allt die linke Seite. ( Ein Funktionsaufruf belegt alle Variablen auf der linken Seite der Zuweisung. Eine Variable ist belegt, wenn sie formaler Argumentparameter einer Funktionsdeklaration ist. Alle auf der rechten Seite verwendeten Variablen werden gelesen. Für die Anordnung von Funktionsaufrufen im Block einer Funktionsdeklaration muss gelten: Eine Variable darf nur einmal belegt werden. Eine Variable darf erst gelesen werden, nachdem sie belegt wurde. Eine Variable darf beliebig oft gelesen werden. Zuweisungsfolgen können von einem boolschen Ausdruck abh¨angig gemacht werden: ! ::= Beispiel 3.2 Zur Verdeutlichung der FASAN-Syntax zeigen wir einen möglichen ¨außeren Rahmen für eine Simulation, die nach jeder Berechnung step einer diskreten Lösung x’ prüft, ob die gewünschte Genauigkeit erreicht ist: type Domain extern Java; type Solution extern Java; function solve(x:Solution; omega:Domain)-> x’:Solution { } function simulationStep(omega:Domain;x:Solution;iter:int) -> x’:Solution { x_ = solve (x, omega); if accuracyReached (x_) then x’ = x_; else omega_ = refine (omega); x’ = simulationStep (omega_, x_, iter+1); fi } 32 Die funktionale Koordinationssprache FASAN 3.1.3 Hierarchische Datentypen Hierarchische Datentypen2 bilden den Kern von FASAN. Im Kapitel 4 werden wir sehen, wie sie sich von herkömmlichen Datentypen unterscheiden: FASAN-Datentypen synchronisieren n¨amlich ihre Komponenten nicht. Hierarchische Datentypen werden in einer Typdeklaration vereinbart: .( " ::= [ ] ::= [ ] ! ::= [ ] Hierarchische Datentypen können für ihre Komponenten sowohl Grund- und Referenztypen der Hostsprache als auch hierarchische Datentypen ! verwenden. Hierarchische Datentypdefinitionen dürfen sich auf Typen beziehen, die entweder textuell weiter oben deklariert sind, in der aktuellen Deklaration festgelegt oder in einer textuell folgenden Deklaration vereinbart werden (verschr¨ankte Rekursion). Funktionstypen, die in Abschnitt 3.1.6 eingeführt werden, schließen wir als Teil hierarchischer Datentypen aus. Die Identifikatoren für Konstruktoren und für Selektoren werden durch eine Datentypdefinition vereinbart und müssen voneinander und von allen anderen Funktions- und Typsymbolen verschieden sein. Außerdem wird für jeden Konstruktor eines hierarchischen Datentyps auch ein Pr¨adikat is erzeugt, das prüft, ob das Argument mit dem Konstruktor aufgebaut worden ist. Ein Konstruktoraufruf ist als Abkürzung auch auf der linken Seite zugelassen: Er baut dort aber keine neue Datenstruktur auf, sondern dient der Zerlegung der Datenstruktur auf der rechten Seite und der Zuweisung von Werten an seine Argumente. Wenn also ein Konstruktor für einen hierarchischen Typ die Selektoren als Argumente hat, dann ist ein Aufruf ( ( als syntaktische Abkürzung3 zu sehen für ( ( In Anlehnung an die Java-Schreibweise dürfen Selektoraufrufe auch nachgestellt werden, also etwa gleichbedeutend mit . 2 Hierarchische Datentypen wurden in vorigen Arbeiten als Kabelbäume“ oder wrapper streams“ be” ” zeichnet. 3 Im ursprünglichen FASAN-System, also in den früheren Publikationen [Ebn94, EPZ96, EP96, Pfa97], wurde anstatt eines Konstruktors der -Operator verwendet. Auf der linken Seite einer Zuweisung diente er als Selektor-Operator für alle Komponenten, ebenso wie hier die Konstruktoren. 3.1 Sprachbeschreibung 33 Beispiel 3.3 Hier zeigen wir einige gültige Anwendungen von Konstruktoren und Selektoren für einen hierarchischen Datentyp Tree am Beispiel einer Divide&ConquerFunktion dc: type Node extern Java type Tree = Branch (val:Node; left,right:Tree) | Nil() function down (n, parent: Node) -> n’:Node extern Java function up (n, son1, son2: Node) -> n’:Node extern Java function dc (t:Tree) -> t’:Tree { if isNil(t) then t’ = Nil(); else Branch(v,l,r) = t; // Zerlegung von t // in Komponenten v,l,r v0 = down(v); // Funktion bei Abstieg im Baum l1 = dc(l,v0); // Rekursion linker Teilbaum r1 = dc(r,v0); // Rekursion rechter Teilbaum v1 = up(v0, val(l1), r1.val); //Funktion bei Aufstieg t’ = Branch (v1,l1,r1); fi } 3.1.4 Endrekursion In funktionalen Programmiersprachen ist es üblich, einen rekursiven Aufruf, der als syntaktisch ¨außerster Aufruf in einem Ausdruck den Rückgabewert einer Funktion bestimmt, in eine Iteration umzuformen. Ein solcher Aufruf heißt Endrekursion. Der Aufruf kann für seine Argumente, seine lokalen Variablen und seine Rückgabewerte denselben StackBereich wie der Aufrufer verwenden und damit Speicher sparen. Beispiel 3.4 Erinnern wir uns nochmals an das Beispiel 1.1 auf Seite 2, in dem günstigerweise mehrere Inkarnationen der Funktion wavefront nebenl¨aufig ausgeführt werden. Hier könnte einerseits der rekursive Aufruf bzgl. Speicherplatz optimiert werden. Andererseits bedeutet die Speicheroptimierung der Endrekursion aber eine Sequentialisierung der Iterationen, weil zu jedem Zeitpunkt nur Speicher für einen Aufrufkontext auf dem Stack verfügbar ist. Deshalb wird es dem Programmierer überlassen, sich für Speicheroptimierung (also Ausnutzen der Endrekursion) oder für Laufzeitoptimierung im parallelen Fall durch Ausnutzung der entstehenden Pipeline zu entscheiden. Der erste Fall, also die Variante der Speicheroptimierung durch Auflösung in eine Iteration, wird in FASAN mit dem Schlüsselwort recycle vor dem endrekursiven Aufruf 34 Die funktionale Koordinationssprache FASAN besonders gekennzeichnet. ::= recycle Beispiel 3.5 Eine typische Anwendung von Endrekursion in FASAN ist die Iteration über eine Divide&Conquer-Funktion: function iterate (t:Tree) -> t’:Tree { if stop(t.val) then t’ = t; else t0 = dc(t); t’ = recycle iterate(t0); fi } Nachdem die Parallelit¨at innerhalb der Funktion dc erzeugt wird, ist es hier sinnvoll, recycle zu verwenden und dadurch Speicherplatz zu sparen. 3.1.5 Lokationen zur verteilten Berechnung Funktionsaufrufe sind die Grundlage für Berechnungen in funktionalen Programmen. Deshalb liegt es nahe, sie als parallele Prozesse aufzufassen. Dies ist auch aus der Sicht des Programmierers eine intuitive Sichtweise. Eine mögliche parallele Berechnung eines Funktionsaufrufs wird durch Annotation mit einer sogenannten Lokation angegeben. Der Wert einer Lokation bezeichnet die Nummer des Zielrechners, auf dem die Auswertung des Aufrufs erfolgen soll. Lokationen werden durch Ausdrücke berechnet oder einfach durch den Wert einer Variable vorgegeben: ::= [ on on ] Der Wert der Variablen bzw. das Ergebnis der zweiten Funktionsanwendung muss einen int-Wert im Bereich von bis numHosts sein. Die vordefinierte Variable numHosts ist die Anzahl der im Rechnernetz insgesamt für das FASAN-Programm zur Verfügung stehenden Rechner. Eine zweite vordefinierte Variable ist myHost, die die Nummer des Rechners liefert, auf dem der Lokationsaufruf stattfindet. Ein Wert der Lokation kleiner 0 bedeutet die nicht-parallele Bearbeitung im selben Kontrollfluss. Ist der Wert gleich der Nummer des aktuellen Rechners, so ist dies als Möglichkeit zur Erzeugung eines neuen nebenl¨aufigen Kontrollflusses — im Sinne eines Threads — zu verstehen. Beispiel 3.6 Ein Lastverteilungsfunktion, die einen Funktionsaufruf auf den Rechner mit der n¨achsthöheren Nummer verlagert, etwa in einem ringförmigen Rechnernetz, kann direkt in FASAN codiert werden: 3.1 Sprachbeschreibung 35 function next_proc() -> loc:int { loc = (myHost + 1) % numHosts; } Lokationen sind eine konzeptionell einfache, aber sehr flexible Möglichkeit zur Lastverteilung. Mit der Angabe einer Lokationsfunktion, die zur Laufzeit einen Rechner bestimmt, auf dem der Aufruf erfolgen soll, ist auch dynamische Lastverteilung möglich. Beispiel 3.7 Ein Master-Programm produziert laufend neue Auftr¨age, die auf SlaveProgramme verteilt werden sollen, etwa beim Ray-Tracing. Der Master kann hierfür eine Warteschlange verwalten, in der die zur Zeit nicht oder wenig belasteten Rechner stehen. Die beiden zugehörigen Lokationsfunktionen sehen, als externe Funktionen in Java geschrieben, folgendermaßen aus: java.util.LinkedList lowLoadHosts; int MAX_LOAD = 4; int getFreeHost() { int host = lowLoadHosts.getFirst(); if (load(host) < MAX_LOAD) lowLoadHosts.addLast(new Integer(host)); return host; } void setFreeHost (int host) { if (load(host) < MAX_LOAD) lowLoadHosts.addLast(new Integer(host)); } Die Funktion im FASAN-Programm zum Start eines neuen Auftrags function rayTraceTask (x:...) -> y:...; targetHost:int { y = rayTrace(x); targetHost = myHost; } wird dann beim Aufruf im Master-Programm mit einer Lokation versehen: result, targetHost = rayTraceTask (...) on getFreeHost(); setFreeHost (targetHost); Im Ergebnisparameter von rayTraceTask wird dabei vermerkt, auf welchem Host der Auftrag gerechnet worden ist. Dazu setzt rayTraceTask einfach den Wert von myHost (den Zielrechner) in das Ergebnis ein. Lokationsfunktionen sind auch für komplizierte Verteilungen der Datenstrukturen bzw. der sie bearbeitenden Funktionen geeignet. Ein Beispiel, das die Einbettung einer DünnGitter-Datenstruktur in eine Dualhyperwürfel-Topologie leistet, findet sich in [Pfa97]. 36 Die funktionale Koordinationssprache FASAN Lokationen können als modulares Konzept zur Lastverteilung gesehen werden. Ihre Implementierung kann unabh¨angig vom FASAN-Programm ausgetauscht werden, etwa für eine neue Rechnertopologie oder zur Integration dynamischer Lastverteilungsverfahren. Stellt die Lastverteilungskomponente die obere Schicht im System dar, etwa das marktwirtschaftlich orientierte Lastbalancierungssystem DYNASTY [BPZ96, Bac98], so ist es sinnvoll, die Lokationsergebnisse auf zwei Werte zu beschr¨anken: 0 bedeutet dann die Generierung eines neuen parallel bearbeitbaren Auftrags (eine split-Operation eines Teilproblems), und die Weiterführung im selben Kontrollfluss. Letzteres bedeutet einen merge-Vorgang, falls in vorigen Iterationen entsprechende Funktionsaufrufe für die Teilprobleme noch parallel abgearbeitet wurden. Beispiel 3.8 Für Divide&Conquer-Algorithmen können wir solche zweiwertigen Lokationen auf folgende Weise in des FASAN-Programm integrieren. Das Lastverteilungssystem ruft dabei die Funktion mit einer Variablen needsSplit auf, die angibt, dass rekursive Aufrufe abgespalten und dem System zur Neuplatzierung zur Verfügung gestellt werden: function dc (t:Tree; needsSplit:boolean) -> t’:Tree { if isNil(t) then t’ = Nil(); else if needsSplit then loc = 0; else loc = -1; fi Branch(v,l,r) = t; v0 = down(v); l1 = dc(l) on loc; r1 = dc(r) on loc; v1 = up(v0, l1.val, r1.val); t’ = Branch(v1,l1,r1); fi } Bei den rekursiven Aufrufen von dc mit Lokation größer legt das FASANSystem zwei neue Tasks an, die dann vom Lastverteilungssystem weiterverwaltet werden können. Dieses Beschr¨ankung auf die Werte 0 und kann auch für alle anderen Zielsysteme verwendet werden, die die Verteilung paralleler Teilberechnungen selbst organisieren, also z. B. für POSIX-Threads auf SMP-Maschinen. 3.1.6 Funktionen h¨ oherer Ordnung FASAN unterstützt Funktionen höherer Ordnung, weil damit Rekursionsmuster oft noch kürzer und pr¨agnanter ausgedrückt werden können. Es gibt auch eine Reihe m¨achtiger, meist datentypbezogener Theorien wie den Bird-Meertens-Formalismus (BMF, [Bir88]), der beispielsweise allgemeine Listenoperationen wie map, scan und fold definiert. Auch 3.1 Sprachbeschreibung 37 für numerische Divide&Conquer-Algorithmen wurde der BMF schon erfolgreich verwendet [GL95]. Für Funktionen höherer Ordnung können wir in FASAN entsprechende Funktionstypen definieren: .( ( .( .( -> ::= ::= [ ] Diese Definition von Funktionstypen vermeidet die syntaktische Verschachtelung. Um einen Funktionstyp -ter Ordnung zu definieren, müssen bereits Namen für die benötigten Typen der Ordnung s vereinbart worden sein. , durch .( Mit Funktionstypen kann eine Variable dann auch eine Funktion bezeichnen und auf Argumente angewendet werden: ( ::= [ ] Beispiel 3.9 Eine oft benötigte Funktion 2. Ordnung auf Listen ist die Funktion map. Sie wendet ihr erstes (Funktions-)Argument auf alle Elemente ihres zweiten (Listen-) Arguments an; hier wird die Liste von B¨aumen (ein Wald) verwendet: type Forest = empty() | forest (first:Tree; rest:Forest) function map (f:Tree->Tree; w:Forest)-> w’:Forest { if isempty(w) then w’ = empty(); else forest(x,rest) = w; w’ = forest(f(x), map(f,rest)); fi } Auch Ergebnisse von Funktionsaufrufen können wieder von einem Funktionstyp sein, wenn eine oder mehrere Variablen ausgelassen und syntaktisch durch einen Unterstrich ersetzt werden: ::= ( [ [ ] ] Ein oder mehrere Unterstriche als Funktionsargumente bewirken eine partielle Funktionsanwendung (Currying). Dadurch wird ein Abschluss (engl. closure), also wieder eine Funktion, als Ergebnis erzeugt [Thi94]. Diese neue Funktion hat dann so viele Parameter wie Unterstriche im Aufruf stehen, mit den gleichen Typen der entsprechenden Parameterstellen. Solche Ausdrücke können nicht mit Lokationen versehen werden, weil sie selbst noch keine Berechnungen anstoßen. 38 Die funktionale Koordinationssprache FASAN Beispiel 3.10 Wir gehen von einem hierarchischen Datentyp für B¨aume mit beliebigem Verzweigungsgrad aus: type Tree = | type Forest = | leaf (val:Object) branch (node:Object; sons:Forest) empty() forest (first:Tree; rest:Forest) Eine Funktion höherer Ordnung, die den Baum durchl¨auft und dabei Ergebniswerte für die Berechnungen in den Söhnen nach unten transportiert, zeigt das folgende Programm: function mapDown (f:(Object,Object)->Object; s:Object; t:Tree) -> t’:Tree { if isleaf(t) then t’ = leaf (f(s,t.val)); else branch(x,sons) = t; // sons:Forest y = f(s,x); ff = mapDown(f,y,_); // ff:Tree->Tree ys = map (ff, sons); // ys:Forest t’ = branch(y,ys); fi } Auch in Funktionen höherer Ordnung können wir Ausdrücke mit Lokationen versehen, wodurch wir dann Skeletons im Sinn von Cole [Col98] und Skillikorn [Ski95] erhalten. Skeletons sind parallele Berechnungsrahmen für bestimmte hierarchische Datenstrukturen, die universell für Funktionen einsetzbar sind, die auf Elementen dieser Datenstrukturen arbeiten. Beispiel 3.11 Durch Annotation der map-Funktion aus Beispiel 3.10 mit einer Lokation wird automatisch auch mapDown ein paralleles Skeleton: function map (f:Tree->Tree; w:Forest) -> w’:Forest { if isempty(w) then w’ = empty(); else y = f(w.first) on getFreeHost(); w’ = forest (y, map(f, w.rest)); fi } Funktionen höherer Ordnung sind darüber hinaus für numerische Berechnungen mit mehrdimensionalen Funktionen vorteilhaft, wenn nur der Wert für eine Dimension feststeht. Hier ein komplizierteres, aber genau nach der mathematischen Spezifikation aus [Bon95, S. 7ff] eins zu eins übernommenes Beispiel. 3.2 Eine abstrakte Syntax 39 Beispiel 3.12 Bei der Quadratur nach Archimedes wird das Integral S2 eines zweidimensionalen Körpers unter anderem auf einen Körper mit Inhalt F1 abgestützt, der sich aus eindimensionalen Segmenten S1 zusammensetzt. Diese Segmente wiederum setzen sich aus einbeschriebenen Dreiecken mit Integral D2 zusammen: type fun1D = double->double type fun2D = (double,double)->double function D2 (f:fun2D; x,a,b:double) -> y:double { y = 0.5 * (b-a) * ( f(x,(a+b)*0.5) - 0.5*(f(x,a) + f(x,b)) ); } function F1 (f:fun1D; a2, b2:double) -> y:double { y = 0.5 * (f(a)+f(b)) * (b-a) + S1(f(_), a, b); } function S2 (f:fun2D; a1,a2,b1,b2:double) -> i:double { i = F1 (D2(f,_,a2,b2), a1, b1) + ...; } Damit haben wir bereits den Sprachumfang von FASAN festgelegt. Rein syntaktisch bietet FASAN nur durch die Lokationsannotation von Funktionsaufrufen Neues. Die wesentliche Eigenschaft der hierarchischen Datenstrukturen, dass Komponenten erst bei einem Selektoraufruf und dann direkt von der Lokation des Konstruktoraufrufs nachgezogen werden, ist für den Programmierer einerseits leicht informell nachvollziehbar, ohne dass er es andererseits explizit programmieren muss. Wie dieses Kabelbaum-Prinzip genau zu verstehen ist, legen wir erst im folgenden Kapitel über die operationelle parallele Semantik von FASAN fest. Was wir als Vorbereitung auf syntaktischer Ebene dazu beitragen können, ist eine auf die Semantik abgestimmte abstrakte Syntax sowie die Übersetzung der FASAN-Syntax in diese abstrakte Notation. 3.2 Eine abstrakte Syntax Wie in Kapitel 2 erw¨ahnt, wird für funktionale Programme üblicherweise eine abstrakte Syntax gew¨ahlt, die sich wegen syntaktischer Vereinfachungen für die Definition der Semantik besser eignet als die konkrete Syntax. Wir werden die abstrakte Syntax für FASAN auf die im n¨achsten Kapitel eingeführte Datenfluss-Semantik ausrichten. Unser Ziel ist, zu einem gegebenen FASAN-Programm eine Signatur zu erzeugen, die das Programm auf abstrakterer Ebene widerspiegelt. Die rekursiven Funktionen von FASAN beschreiben eine rekursive Programmdefinition, die zusammen mit einer -Algebra der externen Funktion und Typen zu einem rekursiven Programm wird, für das wir durch das Regelsystem (2.1) bis (2.4) unmittelbar eine operationelle Semantik erhalten. ( 40 Die funktionale Koordinationssprache FASAN Wir teilen zun¨achst die Funktionssymbole in disjunkte Mengen ein: in die Konstruktorsymbole, in die Selektorsymbole, in die durch externe Funktionsdeklarationen vereinbarten Funktionssymbole sowie Pr¨adikate und in die Namen der rekursiven Formeln. Für Elemente dieser Mengen werden wir wieder feste Bezeichner verwenden, die wir nach Bedarf mit Indizes versehen, um auf sie Bezug nehmen zu können. Es seien also , , , , und . ( Im Einzelnen wird dann ein FASAN-Programm in folgende syntaktische Kategorien aufgeteilt, die dem Aufbau eines abstrakten Syntaxbaums entsprechen: 4 ::= ::= ! ::= ::= ! ( ( ! ( #%$ &)( &)*"+ &( / ( * & # ( ( Ein Programm ( in abstrakter Syntax unterscheidet sich syntaktisch von unserer Festlegung einer rekursiven Programmdefinition (Def. 2.9) dadurch, dass keine geschachtelten Funktionsaufrufe (Ausdrücke) möglich sind, sondern in eine Sequenz von Zuweisungen transformiert werden müssen. Weiterhin müssen die Zweige einer Bedingung jeweils in eigene Funktionen und (mit denselben Argument- und Ergebnisparametern) gefasst werden. Die Anwendung eines Abschlusses auf weitere Argumente wird explizit durch das Funktionssymbol gekennzeichnet. / Zum Zweck einer einfachen Aufschreibung verwenden wir darüber hinaus einen generischen Operator , der folgende Verknüpfungen leistet: für Sequenzen für Mengen für Tupel und ( ( ( ( ( ( ( ( ( ( Für dieÜbertragung in abstrakte Syntax setzen wir noch voraus, dass Zeichenketten zu Schlüsselwörtern und Bezeichnern zusammengefasst sind, und dass Trennzeichen und Kommentare entfernt sind. Weiterhin muss das Programm gem¨aß der konkreten Syntax zerteilt sein, also als abstrakter Syntaxbaum vorliegen. Schließlich müssen Wiederholungen, die sich aus Syntaxregeln der Form [ ] und [ ] ergeben, in Sequenzen umgeformt sein, und zwar 14 Der Stern bedeutet Sequenzen, siehe Seite 19. In dieser Schreibweise ist ein Bezeichner für eine endliche Sequenz von Variablen. 4 3.2 Eine abstrakte Syntax 41 ! " Argument- und Ergebnislisten als , Typlisten als , Listen formaler Parameter gleichen Typs als Listen von Selektoren gleichen Typs als ! " " ! ! " . und Die Typisierung von Variablen und Funktionen vermerken wir bei Bedarf in der abstrakten Syntax durch hochgestellte Indizes. Wir wollen nun im Detail die Übertragung eines FASAN-Programms von konkreter Syntax in ein ¨aquivalentes Programm in abstrakter Syntax angeben. # ! .( # ! ! ! "*! / # ! ! / falls ! # ! ! wobei ! !$" # ! !/ falls # ! ! wobei # ! !/ falls ! .( !$" # ! wobei !"# ! .( falls ! !$" # ! !/ # ! ! "# ! wobei .( falls ! !$" # ! ! / # ! "# ! wobei ! Der Übersicht halber indizieren wir die Abbildung # ! mit den syntaktischen Kate gorien, die jeweils übersetzt werden sollen . Abgekürzte Namen wie oder 5 deuten immer auf die abstrakte Syntax hin, im Gegensatz zu den ausgeschriebenen Namen der konkreten Syntax. 3.2.1 Rekursive Funktionen Eine rekursive Funktion ergibt in der abstrakten Syntax eine Sequenz von Funktionen, welche aus den Zweigen von Fallunterscheidungen entstehen: # ! # ! function ->( ! * & # "( ( "*! / / %' Wir sprechen bei $&%(')+*-, ,.0/ / von einer Abbildung im mathematischen Sinn, im Gegensatz zu Funktionen aus der Menge 2143 , die genauer Funktionssymbole sind. 5 42 Die funktionale Koordinationssprache FASAN wobei 00 / ( # ! ! / Im Block eines Funktionsrumpfs steht eine Sequenz von Anweisungen und bedingten Anweisungen; die aus Zweigen von bedingten Anweisungen entstehenden Funktionen werden im zweiten Ergebnis folgender Abbildung zusammengefasst: # ! % %' !$" !$" !$" # ! ! "* / - # ! / # ! !$" # ! - # ! / # ! falls falls ! Geschachtelte Funktionsaufrufe in Zuweisungen ersetzen wir durch eine Sequenz von nichtgeschachtelten Zuweisungen: # ! ( ! ( " # ! # ! wobei " Entsprechend erhalten wir aus einem Ausdruck eine Sequenz von Zuweisungen: # ! " % # ! " !$" !$" !$" # ! " ! "* / # ! " # ! " / # ! Bei einem Ausdruck ist zwischen Variablen, Funktionsaufrufen und Funktionsaufrufen mit Lokation zu unterscheiden. " " % # ! !$" ! " # ! " ! " # ! " wobei neue Variablensymbole Ein Funktionsaufruf mit Lokation ist ebenso einfach zu übertragen: # ! " on / / ! ( / " # ! " wobei / / # ! / ! " " neue Variablensymbole # ! 3.2 Eine abstrakte Syntax 43 Wird eine Variable auf Argumente angewandt, so enth¨alt diese Variable einen Abschluss. Eine solcher Funktionsaufruf, bei dem die Parameter aus dem Abschluss und die aktuellen Argumente zusammengefügt werden müssen, behandeln wir explizit durch die Funktion . # ! ( ! # ! " neue Variablensymbole " wobei ( " Auch die Anwendung einer Funktionsvariable auf Argumente darf mit einer Lokation annotiert werden: # ! / ! ( on / # ! " / / # ! / ! " " neue Variablensymbole " wobei ( ( / " Für bedingte Zuweisungen werden in jedem Fall zwei neue rekursive Funktionen erzeugt, die die beiden Zweige der Fallunterscheidung beinhalten: # ! - ! % %' # ! - if then else / fi !( ! #% $ & ( &) *+ & / " * & # "( * & # "( ! (# ( ( ( #( / ( ( ( ! ! / " / # ! wobei " # ! ! # ! / / ! / / () 0 0 0 0 / 0 0 + / ( Hier haben wir die Abbildungen lhs und rhs verwendet, die die linke bzw. rechte Seite von Zuweisungen liefern: 0 !" !$" ! "* - 0 0 !$" !$" ! "* ! "* 0 44 Die funktionale Koordinationssprache FASAN 3.2.2 Hierarchische Datentypen Die Definition eines neuen hierarchischen Datentyps führt neben dem neuen Typsymbol auch neue Konstruktoren in und Selektoren in sowie Pr¨adikate in ein. " # ! ( .( %' '% # ! " # ! wobei %' Im Einzelnen führen wir mit der durch einen Konstruktor beschriebenen Variante des hierarchischen Datentyps ebendieses neue Konstruktorsymbol ein. Gleichzeitig wird ein Pr¨adikat neu vereinbart: # "+ %.( # ! %' % # ! # !" # ! # ! "* / "+ " # ! # # ! wobei Für jede Selektorfunktion sind zus¨atzliche Attribute zu speichern, die außer der Typisierung noch angeben, für welche Konstruktorvariante sie bestimmt ist und die wievielte Komponente der entsprechenden Datenstruktur sie ausw¨ahlt. # ! # ! % %.( % .( % !" !$" ! "- / # ! # ! ! /" wobei Die beiden wesentlichen Sprachelemente von FASAN — hierarchische Datentypen und rekursive Funktionen — können wir nun in abstrakte Syntax übersetzen. Dazu wollen wir ein Beispiel angeben. Beispiel 3.13 Wir w¨ahlen die Funktion map aus Beispiel 3.10, die in abstrakter Syntax folgendermaßen geschrieben wird: ! "#$% $ '& ) $* ) +,$- "#/ $ ,. 10 & 2-!3* "#14 ($/ . 0 & 5 6 5 - / & 7& 8 & ($ '& 8 & ($ '& / ($ 3.2 Eine abstrakte Syntax 8 - 45 " 3 8 8 " 8 / " + - / - 3 * "!$# " - " / "# / % 8 / " " 3.2.3 Externe Funktionen, externe Typen und Funktionstypen Die drei noch nicht behandelten Varianten von Definitionen in FASAN führen nur neue Symbole ein, für die derÜbersetzer lediglich überprüfen muss, ob sie nicht bereits anderweitig deklariert sind und ob sie ihrer Typisierung entsprechend richtig verwendet werden. Eine externe Funktion legt also nur das neue Funktionssymbol fest, ohne weitere Effekte in der abstrakten Syntax: # ! function -> ( extern & 0 wobei / 0 ( # ! / Externe Typdefinitionen erweitern unseren Vorrat an Typsymbolen: " .( .( # ! " type extern & # ! Für die Typprüfung muss derÜbersetzer eine partielle Ordnung auf den Typsymbolen mitführen, die die Vererbungshierarchie angibt. Für eine Typdefinition der Form " type extern & isa / muss dann in der darunterliegenden -Algebra der externen Typen und Funktionen gel für alle / ten: # ! 46 Die funktionale Koordinationssprache FASAN Für denÜbersetzer bedeutet das, dass er als ersten Schritt alle Funktionstyp-Symbole in Parameter- und Variablenlisten durch ihre Langform ersetzt. Der Übersetzer hat sicherzustellen, dass es keine Namenskonflikte von Funktionstypen und anderen Symbolen gibt. In die Signatur des FASAN-Programms nehmen wir Funktionssymbole, die für Typausdrücke aus stehen, nicht auf. Wegen der Abstützung von FASAN auf Hostsprachen wie Java, die selbst keine Funktionstypen kennen, können Funktionstypen höherer Ordnung nur in FASAN selbst definiert werden. Deshalb benötigen wir die Symbole nicht in der Signatur; die Semantik entsprechender Funktionen wird komplett durch die Datenflussgraphen erfasst. Auf die Typ-Prüfung in FASAN wollen wir jedoch nicht weiter eingehen, weil sie eine Hilfe bei der Programmentwicklung ist, aber zu unserem Ziel der Parallelisierung keinen Beitrag leistet. Das folgende Kapitel wird nun eine operationelle Semantik für FASAN-Programme in abstrakter Syntax angeben. Diese Semantik dient vor allem dazu, das Verhalten von FASAN-Programmen bei verteilter Ausführung beschreiben zu können. Die besondere Behandlung hierarchischer Datentypen, die ihre Komponenten nicht synchronisieren und für direkte Kommunikationswege sorgen, werden im Vordergrund stehen. 47 Kapitel 4 Operationelle Semantik von FASAN Das Verhalten eines FASAN-Programms ist durch die informelle Sprachbeschreibung im vorigen Kapitel intuitiv vermittelt worden. Gerade für eine neue Sprache ist es aber wichtig, formal und exakt festzulegen, wie ihre Programme ablaufen. Das bedeutet allerdings nicht, dass eine Implementierung schon fest vorgegeben sein muss. Die Semantik einer Sprache legt ihre wesentlichen Eigenschaften fest, also neben den determinierten Ergebnissen einer Berechnung auch mehr oder weniger feste Vorgaben für den Verlauf der Berechnung. Unser Ziel ist eine Semantik, die einerseits möglichst viel Spielraum zur Ausnutzung von Parallelit¨at l¨asst, aber andererseits festlegt, wo die (möglicherweise großen) Datenmengen liegen und wohin sie kopiert werden. Für dieses Ziel führen wir Datenflussgraphen als spezielle Prozessnetze ein (Abschnitt 4.1). Sodann legen wir in Abschnitt 4.2 fest, welcher Datenflussgraph einer rekursiven Funktion statisch zugeordnet werden kann. Den tats¨achlichen Berechnungsvorgang in einem FASAN-Programm beschreiben wir in Abschnitt 4.3, wo wir Daten — in Form von Belegungen des Datenflussgraphen — über Funktionsknoten weiterschalten und den Datenflussgraphen dynamisch verfeinern können. Am Ende des Kapitels in Abschnitt 4.4 steht die Zusammenstellung der für den praktischen Einsatz von FASAN wichtigsten Eigenschaften der Datenfluss-Semantik; es werden die Datenlokalit¨at, die direkte Kommunikation und der Ausschluss von Laufbedingungen nachgewiesen. 4.1 Datenflussgraphen FASAN bildet die Koordinationsschicht für die Neben- und Hintereinanderschaltung von externen Funktionen. Diese Komposition von externen Funktionen l¨asst sich vorteilhaft als Graph darstellen. Wir verwenden dabei Datenflussgraphen, die als Abwandlung von Prozessnetzen, wie wir sie in Kapitel 2 eingeführt haben, gesehen werden können. Prozessnetze sind nicht nur anschaulich, sondern liefern auch unmittelbar ein paralleles Ausführungsmodell. Kennzeichen der hier entwickelten Datenflussgraphen ist ihre Dy- 48 Operationelle Semantik von FASAN namik, die durch Entfaltung von Knoten zu Untergraphen entsteht; das Netz wird also dynamisch vergrößert. Definition 4.1 Ein Datenflussgraph ist ein Prozessnetz1 , wobei zus¨atzlich für alle auf dem Vorbereich und dem Nachbereich jeweils eine lineare Ordnung definiert ist, die die Reihenfolge der Ein- bzw. Ausg¨ange angibt. Die Menge der Datenflussgraphen bezeichnen wir mit . Die Pl¨atze eines Datenflussgraphen bezeichnen wir durchwegs als Puffer und die Tran sitionen als Funktionsknoten. In Datenflussgraphen nennen wir den Vorbereich eines Funktionsknotens dessen Argumente, den Nachbereich dessen Ergebnisse. Den Vorbereich eines Puffers nennen wir auch dessen Erzeuger, den Nachbereich dessen Verbraucher. Da in einem Datenflussgraphen der Vor- und der Nachbereich eines Puffers und für höchstens einelementig sind, schreiben wir speziell für auch auch . ! " Bisweilen verwenden wir Vor- und Nachbereiche auch als Sequenzen, z. B. als Argumente für Funktionsaufrufe. In diesen F¨allen ist , wobei kleiner sein muss als bezüglich der in Definition 4.1 verlangten linearen Ordnung. Für das dynamische Verhalten eines Datenflussgraphen wollen wir Daten und nicht nur Zahlen aus in den Puffern speichern. Aus dem FASAN-Programm haben wir durch die abstrakte Syntax eine algorithmische Signatur gewonnen. Für jede -Algebra über dieser Signatur lassen wir Elemente der Tr¨agermengen als Belegung für die Puffer des Datenflussgraphen zu. Durch partielle Funktionsanwendungen ist es außerdem möglich, dass sogenannte Abschlüsse als Belegung für Puffer vorkommen. Ein Abschluss ist ein Tupel aus einer Funktion und einer Sequenz von Argumentvariablen, wobei als Argument auch das PseudoElement vorkommmen kann, das angibt, dass der Funktionsaufruf an dieser Parameterstelle noch kein Argument bekommen hat. Die Menge der Abschlüsse bezeichnen wir mit % Als Markierung in den Puffern können sich also unterschiedliche Datenelemente befinden. Eine solche verallgemeinerte Markierung nennen wir Belegung 2 . Definition 4.2 Die Belegung , ein Fehordnet jedem Puffer entweder einen Inhalt aus , einen Abschluss aus lerelement , ein Pseudo-Element oder, falls der Puffer leer ist, zu. Die Menge aller Belegungen bezeichnen wir mit . 1 2 Siehe Definition 2.19 auf Seite 24. Eine Belegung ist auch unter der Bezeichnung der individuellen Marken bekannt [Rei85]. 4.2 Abbildung der abstrakten Syntax auf Datenflussgraphen Eine Belegung definiert auch eine Markierung flussgraphen, n¨amlich falls sonst : 49 für die Puffer des Daten- 2 Einem Funktionsknoten wird bei seiner Erzeugung eine Funktion aus zugeordnet, die als seine Belegung aufgefasst werden kann. Oft drücken wir die Be legung kurz durch aus. Indizes an Puffernamen dienen dagegen allein der , und haben Identifizierung, etwa durch eine Sequenz von Datennamen wie in keine weitere Bedeutung. 4.2 Abbildung der abstrakten Syntax auf Datenflussgraphen Wir gehen von FASAN-Funktionen in abstrakter Syntax aus, d. h. alle rekursiven Funktionen sind normalisiert in dem Sinn, dass es keine geschachtelten Aufrufe gibt und alle Zwischenwerte mit expliziten Variablen innerhalb von -Ausdrücken bezeichnet sind. Außerdem sind die Konstruktoren in der Menge , die Selektoren in der Menge , die Typsymbole in und die externen Funktionen mit als bekannt vorausgesetzter Se mantik in zusammengefasst. Diese Mengen bilden also die Signatur des FASAN Programms, .( .( *& # In der Datenfluss-Semantik werden rekursive Funktionen aus als Datenfluss-(Teil-) Graphen interpretiert. Sie stellen die Verbindung der Argument- und Ergebnispuffer von Funktionsknoten, die mit rekursiven oder externen Funktionen belegt sind, her. Ziel ist die Spezifikation einer Abbildung ('& # ! Wie wir es schon bei der Übersetzung von der konkreten in die abstrakte Syntax getan haben, klammern wir Argumente, die syntaktische Kategorien der abstrakten Syntax bezeichnen, wieder extra mit , um hervorzuheben, dass es sich um die Zuordnung einer Semantik zu einer Sequenz handelt und nicht um auswertbare Argumente. Definition 4.3 Für einen Datenflussgraphen heißen die Mengen die Eingänge von und die Ausgänge von . - Die Mengen in und out sind nur Teilmengen des Vorbereichs bzw. Nachbereichs des Graphen. Wenn Funktionsknoten mit nullstelligen Funktionen belegt sind bzw. Er gebnispuffer keinen Verbraucher haben, kann tats¨achlich bzw. - 50 Operationelle Semantik von FASAN sein. Im Folgenden setzen wir implizit voraus, dass bei der Entfaltung eines Datenfluss graphen die Mengen in und out des alten Graphen auch für den neuen übernommen werden, wenn wir nichts anderes vermerken. Die folgende Abbildung gibt an, welcher Graph durch eine rekursive Funktionsdefinition festgelegt wird. Wir bauen den Graphen dabei schrittweise von oben nach un ” ten“, Zuweisung für Zuweisung, auf, indem an schon vorhandene Puffer der durch die Zuweisung definierte Graph angeh¨angt wird. Dadurch entstehen nicht notwendig Datenflussgraphen, sondern allgemeinere Netze, weil Puffer noch mehrere Verbraucher haben können. Dies werden wir in einem letzten Schritt bei der Entfaltung durch die Abbildung jedoch beheben. / 0 ('& # ! ('& # ! * & # ( ( wobei / 0 neue Puffer für alle der mit indizierte Puffer aus - (&# ( Ausgehend von einem vorhandenen Graphen werden neue Funktionsknoten an bestehende Knoten angeh¨angt und neue Ausg¨ange erzeugt. ('& # - % ('& # - !" ('& # - ! -" / ( & # ('& # - / ('& # Der einfachste Fall der Abbildung - % ist die Zuweisung von Variable an Variable, die wir mit der Identit¨atsfunktion id vornehmen: ('& # - wobei ( neuer Funktionsknoten neuer Puffer Zur statischen Übersetzung werden Konstruktoren und Selektoren wie die übrigen Funktionen aus behandelt. Ein Aufruf dieser Funktionen ergibt einfach einen mit den entsprechenden Ein- und Ausg¨angen in Relation gesetzten Funktionsknoten. Die besondere Semantik dieser Knoten zeigt sich erst dynamisch bei der Berechnung durch eine 4.2 Abbildung der abstrakten Syntax auf Datenflussgraphen spezielle Art der Entfaltung. - ('& # ( neuer Funktionsknoten für die mit indizierten Puffer aus neue Puffer für alle wobei 51 für alle ( ( Für eine bedingte Zuweisung wird zun¨achst ein neuer Funktionsknoten im Gra phen angelegt mit der speziellen Belegung "! / . Erst der Wert des Pr¨adikats (bzw. die Belegung von ) entscheidet, welcher der beiden neuen Funktionsknoten den einzubauenden Teilgraphen bei der Entfaltung von liefert. ('& # - wobei ( "! #%$ &)( &)*+,& / neuer Funktionsknoten ( ( die mit indizierten Puffer aus neuer Puffer für alle Eine rekursive Funktionsdefinition für alle eines FASAN-Programms beschreibt immer einen Datenflussgraphen, der bei der Entfaltung für einen mit belegten Knoten eingesetzt werden kann. Um beim Aufbau eines Datenflussgraphen die Bedingung der Konfliktfreiheit aus Definition (4.1) für alle Puffer zu erfüllen, muss nach jedem Puffer mit ein neuer Verteilerknoten eingefügt werden, dessen Belegung die Funktion ist: / / / mal / Solche -Knoten werden folgendermaßen nach einem Puffer gefügt: / / % falls sonst in den Graphen ein- 52 Operationelle Semantik von FASAN PSfrag replacements p p addfork fork fork Abbildung 4.1 Einfügen eines -Knotens für Puffer mit mehr als einem Verbraucher wobei neue Puffer für alle neuer Funktionsknoten / / 0 auf alDie Abbildung wendet diese Normalisierung le Puffer ihres Arguments an. Beim Aufbau eines Datenflussgraphen muss keine Rück / 0 im sicht auf die Konfliktfreiheit genommen werden, wenn die Bedingung durch Nachhinein sichergestellt wird. Die Annotation eines Funktionsaufrufs mit einer Lokation ergibt einen parallelen Funktionsknoten mit einem zus¨atzlichen Eingang für den Lokationsparameter. Der Unterschied zu einem normalen Funktionsknoten wirkt sich erst zur Laufzeit aus, wenn geeignete Belegungen der Eing¨ange die Auswertung oder die Entfaltung des parallelen Funktionsknotens verursachen. - ('& # wobei ( ( (&# ( der neu in eingefügte Funktionsknoten ist mit % In der abstrakten Syntax können Funktionen an die Parameter einer Funktion zugewiesen und diese Parameter wiederum auf weitere Argumente angewendet werden. Die Anwendung von Funktionsparametern auf weitere Argumente wird aber immer in einem Aufruf der Funktion gekapselt. Eine solche Funktion höherer Ordnung kann ebenfalls im Datenflussgraphen entfaltet werden, wie zum Beispiel die Funktion map aus Beispiel 3.13 auf Seite 44. Den dazugehörenden Datenflussgraphen zeigt Abb. 4.2. In der graphischen Darstellung schreiben wir Belegungen von Funktionsknoten (also das Funktionssymbol) in das Rechteck. Außerdem verzieren wir die Rechtecke von Konstruktor- und Selektorknoten mit einem Oval, das die Öffnung eines Kabelbaums symbolisieren soll. Aber nicht nur formale Parameter können für eine Funktion stehen. Auch an lokale Variablen innerhalb des Rumpfs einer Funktionsdefinition können Funktionen zugewiesen werden, wenn die rechte Seite ein Funktionsaufruf mit mindestens einem Unterstrich als Argument ist. Dabei wird im Allgemeinen ein Abschluss erzeugt, wenn außer Unterstri- 4.2 Abbildung der abstrakten Syntax auf Datenflussgraphen 53 fork PSfrag replacements head fork tail / map apply Cons Abbildung 4.2 Datenflussgraph des -Zweigs 8 / der Funktion map fork fork node apply curried fork PSfrag replacements sons mapDown map Branch Abbildung 4.3 Datenflussgraph des -Zweigs der Funktion mapDown chen auch echte Argumente in der Argumentliste auftreten (partielle Funktionsanwendung). W¨ahrend wir für neue Puffer die Belegung (kein Datenelement) annehmen, werden die speziellen Puffer für ausgelassene Funktionsargumente mit initialisiert. Ein solches feh- lendes Argument wird durch die Abbildung in einen speziellen mit belegten Puffer curried übersetzt. Abb. 4.3 veranschaulicht dies am Beispielprogramm 3.10 von Seite 38 für die auf B¨aumen arbeitende Funktion mapDown. Werden die Eing¨ange des Datenflussgraphen mit einem Datenelement belegt, kann durch Schalten rechenbereiter Funktionsknoten eine Ausführung erzeugt werden. Dies ist Thema des n¨achsten Abschnitts. (&# 54 Operationelle Semantik von FASAN 4.3 Datenflussgraphen als Berechnungsmodell Für die Beschreibung der tats¨achlichen Berechnungen eines FASAN-Programms werden wir zwei Abbildungen auf Datenflussgraphen definieren. Die erste Abbildung beschreibt die Auswertung eines Funktionsknotens und damit die Änderung der Belegung eines Datenflussgraphen. Die zweite Abbildung, die sogenannte Entfaltung eines Funktionsknotens, modifiziert die Struktur des Datenflussgraphen, indem sie neue Knoten erzeugt anstelle des entfalteten Knotens in den Datenflussgraphen einsetzt. Die Entfaltung von Datenflussgraphen ist eng verwandt mit Konzepten, die sich auch in anderen Systemen finden. Die Entwicklungsmethodik FOCUS mit der Agentensprache ANDL [SS95], mit deren Hilfe Basiskomponenten über Kan¨ale verbunden werden können, kennt ebenfalls eine Verfeinerung ihrer Agenten [SS95, BGG 96, HS97a, HS97b]. Datenflussgraphen enthalten aber keine Zyklen, was sie von Agentennetzen in ANDL unterscheidet. Somit beschreiben Datenflussgraphen unmittelbar Prozesse (im Sinne von Prozessnetzen). Zur Beschreibung der Dynamik in einem Datenflussgraphen erweitern wir diesen um Belegungen für die Puffer und um Lokationen für die Funktionsknoten: Definition 4.4 Ein paralleler Datenflussgraph mit Belegung , der zus¨atzlich mit einer Funktion & mit & ist ein Datenflussgraph versehen ist, die für jeden Funktionsknoten dessen Lokation angibt, also die Nummer des Rechners, auf dem er sich befindet.3 Die Menge dieser Funktionen nennen wir & . Das Kriterium zum Schalten eines Funktionsknoten ist, dass er aktiviert oder — in der Sprache unserer Datenflussgraphen — rechenbereit ist, wenn also für alle gilt: . Wenn ein Funktionsknoten schaltet, wendet er entweder die Funktion, mit der er belegt ist, auf seine Argumente an; dies nennen wir Auswertung. Oder aber er wird durch einen neuen Teilgraphen ersetzt, was wir als Entfaltung bezeichnen. Auswertung erzeugt also nur eine neue Belegung, w¨ahrend die Entfaltung auch den Datenflussgraphen ver¨andern kann. %& Die Kriterien, nach denen wir entscheiden, ob ein Knoten ausgewertet oder entfaltet wird, sind in der Abbildung zusammengefasst (wobei " ): Dabei ist schnitt 3.1.5). 3 der in der konkreten FASAN-Syntax vordefinierte Variable numHosts (vgl. Ab- 4.3 Datenflussgraphen als Berechnungsmodell %& % % & % %& & & * & & * & ( ! * & ( ! * ! & ( ! * & ( ! * & ( ! * & & 55 % % & und und falls falls und falls und und falls falls falls falls sonst, speziell falls für ein Alle Abbildungen, die eine Auswertung im Datenflussgraphen beschreiben, haben demnach die Funktionalit¨at - & * % % wogegen Entfaltungsabbildungen die Funktionalit¨at & (! * % % besitzen. % % % & Durch die Fehlerabbildung wird das Schalten in einem Datenflussgraphen zu einer strikten Abbildung: wobei falls sonst - & * (! * im Wir gehen nun die Regeln zum Schalten von Funktionsknoten mit und Detail durch. Wenn nichts anderes vermerkt wird, muss zum Schalten eines Knotens immer die Voraussetzung für alle erfüllt sein. 4.3.1 Auswertung von Funktionsknoten In einem markierten Datenflussgraphen wird ein rechenbereiter Funktions knoten ( ) ausgewertet, indem dem Datenflussgraphen eine neue Belegung zugeordnet wird: und und (4.1) & * 56 Operationelle Semantik von FASAN PSfrag replacements eval Abbildung 4.4 Auswertung ( Schalten“) eines Funktionsknotens ” ! " ! " für alle mit ( ( 5 falls ( falls falls und sonst wobei mit " Beachten müssen wir bei der Auswertung, dass das Kriterium der Rechenbereitschaft für immer erfüllt ist. Damit ein solcher argumentloser einen Funktionsknoten mit Funktionsknoten nicht immer schalten und dadurch die Sicherheitsbedingung für ein mit verletzen kann, wird er nach seiner Auswertung aus dem Graphen entfernt: und (4.2) (! * wobei & ! " !( ( " ( für und & sonst Ist ein Eingang von mit belegt, erzeugt dies einen Abschluss als Belegung für den Puffer im Datenflussgraphen. Dieser Abschluss muss zus¨atzlich zum Funktionsnamen auch alle übergebenen Argumente mitführen. Dann ergibt sich die neue Belegung nach dem Schalten des Knotens als und (4.3) & * wobei ! " ! " alle mit für ! " für sonst 4.3 Datenflussgraphen als Berechnungsmodell 57 Dies war der einfache Fall, in dem eine Funktion auf eine nicht-vollst¨andige Liste von Argumenten angewandt wird. Es kann aber auch eine Abschluss — der also schon einige der für die Funktion benötigten Argumente mitführt — auf weitere Argumente angewandt werden. Wie dies im Datenflussgraphen vollzogen wird, zeigt der n¨achste Abschnitt. 4.3.2 Entfaltung von -Knoten Ein -Knoten hat als ersten Eingang immer einen Puffer, dessen Belegung ein Abschluss ist. Wir benötigen zuerst eine Abbildung , die das Zusammenfügen von Variablen eines Abschlusses mit den aktuell übergebenen Argumenten leistet: falls falls und falls und 2 % !$" ! *" ! "* $! " ! "* ! *"* ! "* Für die folgenden drei Regeln zur Entfaltung eines Funktionsknotens setzen wir: ! ! " ! " " Bei der Entfaltung eines -Knoten sind drei F¨alle zu unterscheiden. 1. Fall: Nach Einsetzen der neuen Argumente fehlen der Funktion im Abschluss immer noch Argumente. erzeugt dann einen neuen Abschluss: (4.4) (! * wobei & falls sonst & (! * & & wobei !( ( " ( falls für 5 sonst 2. Fall: Alle Argumente sind vorhanden, und die Funktion des Abschlusses ist ein Ele ment der Menge . Dann kann die Funktion ausgewertet werden: und (4.5) 58 Operationelle Semantik von FASAN zs as PSfrag replacements (g,_,y,_) x z apply x y unfold Abbildung 4.5 Entfalten eines "!$# z f -Knotens durch die Regel (4.6) 3. Fall: Alle Argumente sind vorhanden, die Funktion des Abschlusses ist aber eine rekursive Funktion, ein Konstruktor oder ein Selektor. Dieser letzte Fall führt zur Vorbereitung einer Entfaltung des Knotens , indem der Knoten mit der Funktion belegt wird und einen neuen Vorbereich bekommt: und (4.6) (! * wobei & neue Puffer für alle neuer Funktionsknoten ! " falls sonst & 4.3.3 Entfaltung von Funktionsknoten Ein Funktionsknoten des Datenflussgraphen , der mit einer rekur siven Funktion belegt ist, kann ebenfalls schalten, sofern er rechenbereit ist. Gem¨aß der Abbildung wird er aber nur entfaltet, wenn keiner seiner Eing¨ange mit belegt ist. und (4.7) ('! * wobei & ! ! %& & " " ( & # 4.3 Datenflussgraphen als Berechnungsmodell PSfrag replacements unfold 59 ! " ! " - falls mit 8 Abbildung 4.6 Entfaltung eines Funktionsknotens 5 6 & & & falls falls falls Das Schalten eines Knotens für bedingte Ausdrücke führt ebenfalls zu dessen Entfaltung, wenn alle seine Eing¨ange belegt sind: ( ! * ('! * ! wobei / falls / - und 10. und falls & (4.8) "! / ! / sonst 4.3.4 Entfaltung von Konstruktor- und Selektorknoten Der Aufbau und die Zerlegung hierarchischer Datenstrukturen orientieren sich an dem Modell eines Kabelbaums, das schematisch in Abb. 4.7 dargestellt ist. Das Bild deutet die spezielle Semantik an, die für die Beispiele aus der Einführung nötig ist: Durch Entfaltung wird der Kabelbaum aufgedröselt“ und die Hülle“, die die Konstruktoren und ” ” Selektoren um die Kabel“ legen, entfernt. ” Jede der Datenkomponenten kann den direkten Weg zu ihrem Ziel nehmen, und dies unabh¨angig und nicht synchronisiert von den anderen Komponenten. Zu diesem Zweck erzeugt der Konstruktorknoten nur ein Datenelement mit Identifikationsnummern, die auf seine Eing¨ange, also die Komponenten der Datenstruktur, verweisen. Mit Hilfe dieser Identifikationsnummern kann dann bei Selektion einer Komponente 60 Operationelle Semantik von FASAN T = c (A,B,C) A C B Konstruktor A B C Y Z Aufbau T Selektoren Zerlegung Y X Z X c(X,Y,Z) = T Abbildung 4.7 Schematische Darstellung des Aufbaus und der Zerlegung eines Kabelbaums der richtige Funktionsknoten gefunden werden. Daher muss der Inhalt der Komponenten — eine unter Umst¨anden große Datenmenge — nicht den ganzen Weg der umfassenden Datenstruktur nehmen, sondern wird auf einer direkten Verbindung von den Puffern der Komponenten bezogen. / ein, Für die Spezifikation dieses Aspekts führen wir eine injektive Abbildung die einem Puffer eine eindeutige Nummer gibt derart, dass mit der (partiellen) Abbildung für ein aus dem Definitionsbereich von gilt: / / / / / Ist ein Argument eines Konstruktor- oder Selektorknotens mit belegt, bedarf dies keiner Sonderbehandlung; wird über die Kabel des Kabelbaums einfach weitergeleitet. Ein nullstelliger Konstruktor wird dagegen wie eine sonstige nullstellige Funktion behandelt: Nachdem er einmal geschaltet hat, wird er aus dem Datenflussgraphen entfernt. Dies haben wir auch schon bei der Abbildung (4.3) in die Fallunterscheidungen mit einbezogen. Dort wurde festgelegt, dass ein Konstruktor als einziger Funktionsknotentyp im Graphen nicht alle seine Eing¨ange belegt haben muss, um zu schalten. Es genügt, wenn ein Eingang belegt ist: mit und es gibt ein %& ( ! * wobei ! " / für alle falls (4.9) sonst Bei den drei folgenden Entfaltungsregeln (4.10) bis (4.12) für Selektorknoten bestimmen 4.3 Datenflussgraphen als Berechnungsmodell PSfrag replacements / 61 / unfold c c Abbildung 4.8 Entfaltung eines Konstruktorknotens im Datenflussgraphen wir zuerst: neuer Funktionsknoten / falls falls falls [betrifft nur (4.10)] sonst Bei der Entfaltung eines Selektorknotens sind drei F¨alle zu unterscheiden, je nach Belegung und Nachfolger des Puffers . 1. Fall: Der Puffer ist belegt, das Datenelement also schon verfügbar. Hier muss nur das vorhandene Datenelement in das Ergebnis des Selektors übertragen werden. und und (4.10) & (! * & 2. Fall: Der Puffer ist nicht belegt und hat auch keinen Verbraucher. Hier wird ein neuer Replikationsknoten erzeugt, und zwar auf dem Rechner des Konstruktorknotens, da dieser Knoten u. U. von mehreren Selektoren verwendet wird und wir sonst einen ungewollten Umweg der Daten über die Lokation des ersten entfalteten Selektors verursachen würden. und und (4.11) & & (! * 62 Operationelle Semantik von FASAN p p unfold s s PSfrag replacements q q Abbildung 4.9 Datenflussgraphen bei der Entfaltung eines Selektorknotens (1. Fall) p p rep unfold s s PSfrag replacements q q Abbildung 4.10 Datenflussgraphen bei der Entfaltung eines Selektorknotens (2. Fall) p p rep rep unfold s s PSfrag replacements q q Abbildung 4.11 Datenflussgraphen bei der Entfaltung eines Selektorknotens (3. Fall) wobei & & & falls sonst 3. Fall: Der Puffer ist nicht belegt, hat aber einen Replikationsknoten als Verbraucher. Eine neue Verbindung zwischen diesem Knoten und dem Ergebnis des Selektors wird aufgebaut. und (4.12) & & ('! * 4.3 Datenflussgraphen als Berechnungsmodell 63 unfold rep Abbildung 4.12 Entfaltung eines Replikationsknotens / Ein Replikationsknoten wird im Gegensatz zu einem -Knoten nicht ausgewertet, sondern bei der Entfaltung entfernt und die Belegung seines Eingangs nicht gelöscht (Abb. 4.12): (4.13) & & (! * wobei falls sonst 4.3.5 Lokationen Hat ein Funktionsknoten einen zus¨atzlichen Eingang für den Wert der Lokation, wird er, bevor er tats¨achlich entfaltet wird, auf diese neue Lokation verlagert. Auch ein Knoten kann einen zus¨atzlichen Eingang für eine Lokation haben. und (4.14) ('! * & 0 ( ! * & ( ! * & ! " falls und ! wobei sonst falls falls & & In dieser Entfaltungsregel ist 0 0 ! falls für alle anderen Funktionsknoten * & # "( ( " " 64 Operationelle Semantik von FASAN z PSfrag replacements z unfold & Rechnergrenze Abbildung 4.13 Entfaltung eines parallelen Funktionsknotens Die Puffer vor und nach einem Funktionsknoten, der auf einen anderen Rechner verschoben wurde, spielen eine besondere Rolle: Definition 4.5 Ein Puffer heißt Rechnergrenze in einem parallelen Datenfluss graphen & , wenn & & . ('! * zu Die Puffer des Vor- und Nachbereichs von werden durch die Entfaltung Rechnergrenzen. Der Funktionsknoten bildet auf dem Zielrechner einen eigenst¨andigen Datenflussgraphen. Da hier explizit Puffer zwischen Funktionsknoten mit unterschiedlicher Lokation vorhanden sind, beschreibt dieses Modell asynchrone Kommunikation. Die Rechnergrenzen und können im Falle der Implementierung mit gemeinsamem Speicher als Variablen angesehen werden, auf die sowohl der ursprüngliche als auch der neu erzeugte Thread zugreifen kann. Im Fall verteilter Ausführung stellen sie gleichzeitig Kommunikationspuffer zwischen beiden Prozessen dar. Asynchrone Kommunikation entspricht einer höheren Abstraktionsebene und ist meist leichter zu implementieren. Sie kann aber systematisch auf synchrone Kommunikation zurückgeführt werden [Sch98]. %& Wir haben jetzt die Spezifikation der parallelen Semantik eines FASAN-Programms fertiggestellt. Die Abbildung (4.3) verflechtet die Entfaltungsschritte zur Erzeugung neuer Teilgraphen mit Auswertungsschritten, in denen Berechnungen mit Funktionen der darunterliegenden -Algebra stattfinden und Belegungen über Funktionsknoten weitergeschaltet werden. Bevor wir jedoch dieses Semantik-Kapitel schließen, wollen wir noch einige wichtige Eigenschaften der Berechnung mit Datenflussgraphen nachweisen. Hier wird sich die etwas ausführliche und stellenweise nicht ganz direkte Übertragung von FASAN-Programmen über eine abstrakte Syntax in Datenflussgraphen bezahlt machen, weil die Beweise kurz und durch die graphische Darstellbarkeit leicht nachvollziehbar sind. 4.4 Eigenschaften der Datenfluss-Semantik 65 4.4 Eigenschaften der Datenfluss-Semantik Für die Parallelisierung haben hierarchische Datentypen von FASAN gegenüber den gewohnten Baumkonstruktoren drei entscheidende Effizienzvorteile, die wir in den folgenden Lemmata (4.7) bis (4.9) zusammenfassen. Für die Entwicklung von FASANProgrammen ist die mit Lemma (4.10) gezeigte Eigenschaft der Verklemmungsfreiheit wichtig. Wir wollen aber zun¨achst noch ein paar anschauliche Begriffe einführen. Daten sind hier immer als das Ergebnis der Belegungsfunktion zu sehen, also als Elemente aus . Liefert die Belegung eines Puffers , so sagen wir, der Puffer enth¨alt keine Daten. Wenn ein Funktionsknoten schaltet, so sagen wir, die Daten passieren den Knoten. Sequenz ! " , wenn Definition 4.6 Ein Pfad von nach , eines Datenflussgraphen und ist eine 5 . sowie eine -Algebra für & Gegeben sei ein paralleler Datenflussgraph eines FASAN-Programms. über der Signatur Zuerst wollen wir uns vergewissern, dass Daten auf ihrem Weg von einem Konstruktorknoten zu einem Selektorknoten den direkten Weg nehmen. Lemma 4.7 (Direkte Kommunikation) Es seien ein Konstruktorknoten, . Der Pfad gehöriger Selektorknoten sowie und der Entfaltung von und entsteht, enth¨alt höchstens eine Rechnergrenze. ein zu, der nach Beweis: Die Entfaltung eines Konstruktorknotens mit der Regel (4.9) bel¨asst zun¨achst die Daten in den Puffern . Erst wenn die Entfaltung des Selektorknotens eine neue Verbindung zwischen und erzeugt, können die Daten weitergeschaltet werden. Zwischen und ist dann höchstens ein Replikationsknoten vorhanden (Regel (4.12)), oder ein solcher wird erzeugt (Regeln (4.10) und (4.11)). Da in beiden F¨allen gilt: & & , müssen die Daten maximal eine Rechnergrenze, n¨amlich den Puffer , passieren. Liegen vor und nach diesem im Beweis genannten Zwischenknoten keine Rechner- grenzen, so bleiben die Daten lokal auf ihrem Rechner: Lemma 4.8 (Datenlokalität) Es seien torknoten sowie und & & , so enth¨alt der Pfad keine Rechnergrenze. wieder ein Konstruktorknoten, ein Selek auf demselben Rechner, . Liegen und nach der Entfaltung von und Beweis: Wegen Lemma 4.7 gibt es nach der Entfaltung von Konstruktor- und Selektor knoten maximal einen Zwischenknoten zwischen und , also , 66 Operationelle Semantik von FASAN den die Daten passieren müssen. Außerdem ist nach den Regeln (4.11) und (4.12) & & . Zusammen mit der Voraussetzung & & folgt die Behauptung. Ein großer Vorteil der Entfaltungsregeln (4.9) bis (4.12) ist die Unabh¨angigkeit der Argumente eines Konstruktorknotens und damit auch die Unabh¨angigkeit ihrer Belegungen, also der Komponenten des hierarchischen Datentyps: Auf den direkten Verbindungen können die Komponentendaten getrennt voneinander übertragen werden und stehen sobald wie möglich den Verbraucherfunktionen zur Verfügung. Lemma 4.9 (Höherer Parallelitätsgrad) Daten werden durch die Zusammenfassung in einem hierarchischen Datentyp nicht synchronisiert. Beweis: Ein Konstruktorknoten erzeugt bei seiner Entfaltung die Baumreferenz gem¨aß Regel (4.9) bereits, sobald nur ein Eingang belegt ist, also schon dann, wenn nur eine Datenkomponente vorhanden ist. Die Puffer, die vor der Entfaltung die Argumente von bilden, werden also von nicht synchronisiert. ein Des Weiteren ist auch die Selektorseite des Datentyps zu untersuchen: Sei Selektorknoten, der ein Datenelement vom Konstruktorknoten erhalten hat. Seien außerdem die Puffer und . Die Unabh¨ angigkeit der Puffer bleibt auch nach der Entfaltung des Selektorknotens erhalten: Jedes Da tenelement in diesen Puffern kann unmittelbar zum Puffer passieren: Ist es schon als Belegung von vorhanden, wird es gleich nach verlegt (Regel (4.10)). Ist noch nicht belegt, wird ein direkter Pfad über einen Replikationsknoten angelegt (Regeln (4.11) und (4.12)), über den das Datenelement sp¨ater unabh¨angig von den ande ren Komponenten zu passieren kann. / ! " Für die Korrektheit des FASAN-Programms ist weiterhin wesentlich, dass keine Laufbedingungen (race conditions) auftreten können. Laufbedingungen ¨außern sich allgemein dadurch, dass Berechnungsergebnisse von der zeitlichen Reihenfolge gewisser Aktionen abh¨angen, die auf gemeinsame Speicherbereiche schreiben. In der Datenfluss-Semantik ist die Situation recht übersichtlich, weil dort die Puffer den Speicherbereichen entsprechen. Die nach Definition (4.1) vorausgesetzte und bei der Ent faltung von Funktionsknoten eingehaltene Konfliktfreiheit besagt, dass jeder Puffer . Laufbehöchstens einen Erzeuger und höchstens einen Verbraucher hat, dingungen können hier allenfalls entstehen, wenn ein Erzeuger mehrfach schalten kann und vorhandene Belegungen seiner Ergebnispuffer überschreibt, bevor der Verbraucher die erste Belegung gelesen hat. Dass aber eine solche Situation nicht auftreten kann, besagt folgendes Lemma, das impliziert, dass es keine Möglichkeit gibt, einen schon belegten Puffer erneut zu belegen (außer durch ). Lemma 4.10 (Keine Laufbedingungen) In jedem Pfad eines Datenflussgraphen . gibt es maximal einen Puffer mit 4.4 Eigenschaften der Datenfluss-Semantik 67 Beweis: Wir zeigen, dass ausgehend von einem einelementigen Datenflussgraphen mit belegten Eing¨angen nur wieder Datenflussgraphen erzeugt werden, die Pfade mit maximal einer Belegung enthalten. Dies ist also eine Induktion über die Schaltvorg¨ange von Knoten. Für jeden Schaltvorgang bezeichnen wir den schaltenden Knoten mit und seine Argumente und Ergebnisse mit . Außer bzw. dem bezeichnen wir einen Puffer ohne Erzeuger mit (Anfang eines maximalen Pfads) und einen ohne Verbraucher (Ende eines maximalen Pfads). Induktionsanfang: Ein Datenflussgraph mit einelemetigem enth¨alt nur Pfade , wobei belegt ist und nicht, also genau ein Puffer in jedem Pfad. ! ! " ! " " Induktionsannahme: Wir setzen einen Datenflussgraphen mit maximal einem belegten Puffer pro Pfad aus. Induktionsschluss: Für den n¨achsten Schaltvorgang müssen wir unterscheiden, welche Schaltregel Verwendung findet: & * ('! * & * ('! * mit mit Regel (4.3), 1. Schalten durch mit Regel (4.1), - Regel (4.4) oder mit Regel (4.5): Hier handelt es sich um reine Auswertungen eines Funktionsknotens, die die Struktur des Datenflussgraphen unver¨andert und nur die Belegungen auf dessen Ausg¨ange passieren lassen. Nach Induktions voraussetzung war für jeden Pfad der Puffer der einzige belegte Puffer in diesem Pfad. Nach dem Auswerten ist nicht mehr belegt, und der einzige belegte Puffer des Pfads. 2. Schalten durch mit Regel (4.2): Hier schaltet ein Funktionsknoten oh- ! " & * ne Argumentpuffer und wird dann aus dem Datenflussgraphen entfernt, sodass ein Knoten des Nachbereichs von zu einem Anfang eines maximalen Pfades wird. Nach einem Funktionsknoten , der mit einem anderen Puffer synchronisiert, kann kein Puffer belegt sein. Wegen der Konfliktfreiheit des Graphen ist auch kein Puffer zwischen und belegt. Damit ist der einzige belegte Puffer in allen von ausgehenden Pfaden. mit Regel (4.6): Hier findet eine Ersetzung der (be3. Schalten durch legten) Puffer mit neuen belegten Puffern als Argumente des entfalteten Knotens statt. Den neuen Puffern werden keine Erzeuger zugeordnet, und in den Pfaden ausgehend von entstehen keine zus¨atzlichen Belegungen im Vergleich zu den ursprünglichen Pfaden. mit Regel (4.7) oder Regel (4.8) oder mit Re4. Schalten durch (! * (! * ('! * gel (4.14): Diese Entfaltungen eines Knotens lassen die Belegung des Graphen unver¨andert; die bleiben belegt und unabh¨angig. Die Pfade des neuen Graphen sind dann die, die durch Ersetzung von durch Pfade des neuen Teilgraphen entstehen. Alle Puffer dieses Teilgraphen sind nicht belegt, wodurch jeder Pfad des Gesamtgraphen weiterhin maximal einen belegten Puffer enth¨alt. mit Regel (4.9) oder durch mit Regel (4.13): 5. Schalten durch Die Entfaltung eines Konstruktorknotens oder eines Replikationsknotens be- (! * (! * 68 Operationelle Semantik von FASAN legt zwar dessen Ergebnis , ohne die Belegung in dessen Argumenten zu löschen. Es wird aber die Verbindung zu den getrennt, wodurch jeder Pfad des ursprünglichen Graphen in zwei Teilpfade und zerlegt wird, von denen jeder nur einen belegten Puffer ( bzw. ) enth¨alt. mit Regel (4.10): Die Belegung des ursprünglichen Ar6. Schalten durch gumentpuffers des zugehörigen Konstruktorknotens wird zwar in kopiert. Allerdings wird dann zu einem Anfangsknoten aller Pfade, in denen er enthalten ist, weil die Verbindung zum Selektorknoten entfernt wird. Für diese Pfade gilt wegen der Konfliktfreiheit des Datenflussgraphen und wegen der Synchronisation durch von abh¨angige Funktionsknoten (vgl. Schalten durch Regel (4.2)), dass sie außer keine weiteren belegten Knoten enthalten. mit Regel (4.10): Die Entfaltung eines Selektorknotens 7. Schalten durch ! ! " " ! ('! * " (! * erzeugt eine Verbindung von dem ursprünglichen Argumentpuffers des zu gehörigen Konstruktorknotens über einen Replikationsknoten zu . Es wird keine neue Belegung erzeugt. Da außerdem die Pfade, die vor der Entfaltung von ausgingen, keine belegten Puffer enthielten, kann wegen der Induktionsvoraus setzung maximal ein Puffer aus belegt sein. Insgesamt ist also sichergestellt, dass keine Daten als Belegungen beim Schalten im Datenflussgraphen überschrieben und verloren gehen können. ! " Neben Laufbedingungen sind Verklemmungen typische und h¨aufige Fehler, die bei der parallelen Programmierung gemacht werden können. Eine Verklemmung liegt vor, wenn zwei oder mehr Aktionen wechselseitig oder zyklisch auf Ergebnisse der anderen warten, oder unterschiedliche Sperren (z. B. für Betriebsmittel) belegt haben und vor deren Freigabe auf weitere Sperren warten, die wechselseitig schon von anderen Aktionen belegt sind. In einem Datenflussgraphen gibt es allerdings keine Sperren oder sonstige Synchronisationsmechanismen. Einzig die Rechenbereitschaft entscheidet über das Schalten von Funktionsknoten. Wechselseitiges Warten auf Ergebnisse ist wegen der Zyklenfreiheit der Datenflussgraphen ebenfalls ausgeschlossen. Bei Datenflussgraphen bekommen wir also quasi umsonst Verklemmungsfreiheit der durch sie beschriebenen Berechnungen. Die Zyklenfreiheit, zusammen mit Lemma 4.10, bringt auch einen Vorteil auf Implementierungsebene. Sie eröffnet die Möglichkeit einer ziemlich einfachen Speicherverwaltung: Ein Puffer mit kann aus dem Speicher gelöscht werden, sobald bei einem Schaltvorgang eine neue Belegung den Wert liefert. Ein Funktionsknoten kann gelöscht werden, wenn alle seine Eing¨ange gelöscht worden sind. 4.4 Eigenschaften der Datenfluss-Semantik 69 Da die Vorbereiche von Konstruktorknoten immer belegt bleiben, wenn sie einmal von einer Marke erreicht wurden, muss hier eine Zusatzmaßnahme getroffen werden: Ein Refe renzz¨ahler in diesem Puffer z¨ahlt, wie viele Referenztupel für diesen Puffer aktuell im Datenflussgraph existieren. Ein -Knoten erhöht den Referenzz¨ahler, wenn er ein solches Tupel als Argument bekommt. Ein Selektorknoten, der den Referenzz¨ahler verbraucht, erniedrigt ihn. Ist der Referenzz¨ahler 0, kann der Puffer freigegeben werden. / Fassen wir nochmal das bisher Erreichte zusammen: Mit FASAN haben wir eine Koordinationssprache, deren Semantik durch die parallelen Datenflussgraphen formal spezifiziert ist. Auch die Erzeugung der Datenflussgraphen aus einem FASAN-Programm über die abstrakte Syntax haben wir vollst¨andig angegeben. Eine Implementierung von FASAN, die dieser Semantik entspricht, profitiert von den wesentlichen Sicherheitseigenschaften der Semantik, also von der Verklemmungsfreiheit, der Abwesenheit von Laufbedingungen und der im verteilten Fall richtigen Kommunikation. Sie bietet darüber hinaus die Effizienzeigenschaften der Datenlokalit¨at, der direkten Kommunikation und des höheren Parallelit¨atsgrades. Wir besprechen im Weiteren eine solche Implementierung von FASAN. 70 Operationelle Semantik von FASAN 71 Kapitel 5 Übersetzung von FASAN in Java In diesem Kapitel wollen wir die konkrete Implementierung von FASAN vorstellen. Die Übersetzung in die Zielsprache Java führen wir nicht durchgehend formal aus, was angesichts der Größe des implementierten Compilers von ca. 200 kB zu viel Raum in Anspruch nehmen würde. Vielmehr arbeiten wir die wichtigsten Teile der Übersetzung und ihren Bezug zur Datenfluss-Semantik sowie besondere aufgetretene Schwierigkeiten heraus. Dementsprechend finden sich in diesem Kapitel zuerst ein Erfahrungsbericht über frühere Implementierungen von FASAN (Abschnitt 5.1) sowie die Gründe für die Auswahl von Java als Zielsprache (Abschnitt 5.2). Sodann werden die Grundideen der Übersetzung in ein sequentielles Java-Programm (Abschnitt 5.3), in ein Programm mit ThreadUnterstützung für Rechner mit mehreren Prozessoren (Abschnitt 5.4) und die Erweiterung für Rechnernetze mit Hilfe von RMI (remote method invocation, Abschnitt 5.5) dargestellt. 5.1 Erfahrungen aus vorhandenen Implementierungen Das im Rahmen dieser Arbeit entwickelte FASAN-System ist aus einer Reihe von Arbeiten über funktionale Sprachkonzepte für adaptive numerische Anwendungen hervorgegangen. Im allgemeinsten Konzept mit dem Namen HOPSA [BDS92] sind beliebige Stromfunktionen zugelassen, die als Argumente und Ergebnisse nicht einzelne Datenelemente, sondern Ströme von Daten haben. Eine Stromfunktion kann für einen Berechnungsschritt beliebig viele Daten von den Strömen lesen und auf die Ergebnisströme schreiben. Ein ¨ahnlicher Ansatz wird auch in [Zen92] verfolgt, wo sequentielle Prozeduren in einer funktionalen Koordinationsschicht beliebig von ihren Eingangsströmen lesen und auf ihre Ausgangsströme schreiben können. Direkte Implementierungen existieren aber zu keinem der beiden Konzepte. 72 Übersetzung von FASAN in Java Das Prinzip der hinter- und nebeneinandergeschalteten Stromfunktionen wurde jedoch in Implementierungen für die Sprache (auf Grundlage von Smalltalk, [Vei90]), für die Sprache Gent (auf Basis von ParMod, [Rau92]) und in der ersten Version von FASAN (auf Basis von C mit PVM, [Ebn94]) verwendet. und Gent konnten allerdings auf Grund ihrer Implementierungsplattformen parallele und verteilte Ausführung nur simulieren. All diese Systeme hatten einige Nachteile, die erst für große und mit FASAN/C echt verteilt laufende Anwendungen in Erscheinung traten: 1. Umständliche elementare Operationen: Die ausschließliche Abstützung auf Stromfunktionen als elementare Operationen verlangte die Kapselung aller Daten in Strömen. Dies machte die Programmentwicklung, insbesondere die Anpassung der externen eingebundenen Funktionen, ziemlich umst¨andlich und brachte zus¨atzlichen Overhead in die Systeme. 2. Hoher Speicherverbrauch: Alle genannten Systeme erzeugten zur Laufzeit direkt den Datenflussgraphen als dynamische Datenstruktur. Das hatte zwar den Vorteil, dass bei mehrfacher Berechnung des gleichen Funktionsaufrufs die Funktionsknoten des Datenflussgraphen immer wieder für verschiedene Daten verwendet, die Kosten für das Erzeugen des Graphen bei den folgenden Iterationen gespart und die direkten Kommunikationswege mehrfach genutzt werden konnten. Der gravierende Nachteil dabei aber ist der hohe Speicherverbrauch, wenn die rekursiven Funktionsaufrufe bis in große Aufruftiefen als Datenflussgraph entfaltet werden müssen. Der Programmierer war somit etwa bei FASAN/C immer gezwungen, die Auffaltung des Graphen von Hand zu begrenzen. 3. Programmtechnische Nachteile: Nicht-nullstellige Kabelbaumkonstruktoren durften beispielsweise nur in einem Zweig einer Fallunterscheidung vorkommen. Diese Schwierigkeit wurde aber in einer neueren Version von FASAN/C (das in [EPZ96, EP96] zu Grunde liegt) behoben. Auch in der operationellen Semantik im vorigen Kapitel tritt diese Einschr¨ankung dank der Replikationsknoten nicht mehr auf. Als Parameter für externe Funktionen waren nur die einfachen Datentypen von C sowie Arrays davon erlaubt. Höhere Datenstrukturen (structs) wurden nicht direkt unterstützt, sondern konnten nur als Byte-Felder und damit nur innerhalb einer Rechnerarchitektur-Familie übertragen werden. 4. Problematische Fehlersuche: Ein weiterer Nachteil lag ebenfalls in der direkten Implementierung der Datenflussgraph-Datenstrukturen begründet: Mangels Werkzeugen zum Debugging oder zur graphischen Analyse bereitete die Fehlersuche in einem FASAN/C-Programm große Schwierigkeiten. Der Programmierer wurde n¨amlich mit den Interna des Datenfluss-Laufzeitsystems im C-Debugger konfrontiert, welches leicht durch fehlerhaft programmierte externe Funktionen zum Absturz gebracht werden konnte. Es stellte sich weiterhin jedoch heraus, dass für die Programmierung der im Rahmen der 5.2 Auswahl der Zielsprache 73 numerischen Simulation betrachteten Anwendungen die Funktionen vollkommen ausreichend sind, die in gewohnter Weise von jedem Eingang ein Datenelement lesen und auf jeden Ausgang ein Datum schreiben. Dies war selbst in Anwendungen mit so komplizierten Datenabh¨angigkeits- und Kommunikationsstrukturen wie dem in [Pfa97] mit FASAN/C entwickelten Helmholtz-Löser auf Basis dünner Gitter der Fall. Schließlich sind auch Methoden aus numerischen Bibliotheken, die als externe Funktionen eingebunden werden können, keine Stromfunktionen, sondern benötigen für jeden Eingabeparameter genau ein Argument und liefern für jeden Ausgabeparameter genau ein Ergebnis. So lag es also nahe, in dem hier beschriebenen neuen FASAN-System auf Stromfunktionen zu verzichten. Dies haben wir in der operationellen Semantik auch schon dadurch berücksichtigt, dass für jeden Puffer eines Datenflussgraphen die Bedingung gilt, dass also jeder Puffer (in der Terminologie der Netze) 1-beschr¨ankt und der Datenflussgraph damit sicher ist. 5.2 Auswahl der Zielsprache Obwohl es sich um eine prototypische Entwicklung handelte, hat sich das Implementieren des ersten FASAN/C-Systems als recht aufwendig sowohl in der Programmierung als auch beim Suchen nach Fehlern herausgestellt. Neben den typischen Fehlern bei eigener Programmierung der Speicherverwaltung trug auch die große semantische L ücke zwischen den verteilten Funktionsaufrufen (FASAN) und dem Nachrichtensystem (PVM) zu erhöhtem Debug-Aufwand bei. Es war also ein Zielsystem gefragt, das möglichst neben einer automatischen Speicherverwaltung auch verteilte Funktions- bzw. Prozeduraufrufe bereitstellt. Das neue Zielsystem sollte mindestens so gut portabel sein wie PVM und eine einfache Installation ermöglichen. Denn dies spielt eine wesentliche Rolle für die Akzeptanz eines Konzepts oder Werkzeugs. Erfordert ein System viel Installationsaufwand oder größere Anpassungen an eine Hardware-Architektur, hat es kaum Aussicht auf Erfolg. Andererseits durfte für den Bereich der numerischen Simulation, in dem meist langdauernde Berechnungen auf sehr großen Datenstrukturen vorgenommen werden, die Effizienz nicht außer Acht gelassen werden, was Skript-Sprachen wie Perl von vornherein ausschloss. Den aufgeführten Kriterien schien uns Java [AG98, Bis98, Sun98] als Zielsystem zur Zeit am besten zu genügen. Es vereint eine Reihe der benötigten Konzepte, die sowohl die Implementierung als auch die Verwendung von FASAN vereinfachen. Ein willkommener Vorteil von Java ist die erstaunlich weite Verbreitung sowohl im akademischen als auch im kommerziellen Bereich. Es ist mittlerweile ein Quasi-Standard und in den meisten Rechnerumgebungen installiert, sodass hier gar keine zus¨atzliche Installationsarbeit anf¨allt. Ein besonderer Pluspunkt ist die Portabilität von Java-Programmen, die sich gleich über drei Ebenen erstreckt [Rou97]: 74 Übersetzung von FASAN in Java 1. Quellcode-Portabilit¨at: Alle Grunddatentypen haben eine fest definierte L¨ange, Initialisierungswerte und Arithmetik. Damit liefert ein Java-Programm identische Ergebnisse unabh¨angig von Wortl¨angen der Hardware. 2. CPU-Portabilit¨at: Das als Byte-Code kompilierte Programm ist unver¨andert auf allen CPUs ausführbar, im Gegensatz zu Maschinencode von C, C++ etc. 3. Betriebssystem-Portabilit¨at: Systemaufrufe, sei es für Netzwerkverbindungen, für die Ein- und Ausgabe, für die Benutzeroberfl¨ache etc., sind in Java in standardisierten und Kompatibilit¨atstests unterworfenen Klassen gekapselt und sparen bei der Portierung und in heterogenen Umgebungen immens Programmieraufwand. Java verfügt auch über die gewünschte automatische Speicherverwaltung, die sowohl bei der Implementierung des FASAN-Systems als auch sp¨ater beim Entwickeln von FASAN-Programmen und bei der Fehlersuche viel Zeit erspart. Für das FASAN-Konzept der externen Funktionen bietet der sogenannte Serialisierungsmechanismus, den Java nicht nur zum Verschicken von einfachen Daten wie Zahlen oder Feldern, sondern für beliebige und sogar zyklische Datenstrukturen über Rechnernetze verwendet, große Flexibilit¨at. Weiterhin ist FASANs Sprachunabh¨angigkeit von den externen Funktionen durch die standardisierte Einbindungsmöglichkeit von C-, C++- und damit auch FORTRAN-Code in FASAN-Programme über die JNI-API (Java Native Interface) gegeben. Bezüglich der Unterstützung paralleler und verteilter Systeme findet man in Java gleich beides: Unterstützung von Threads [OW97a] mit sprachintegrierten Synchronisationskonstrukten für gemeinsamen Speicher, und das Klassen-Paket für entfernte Methodenaufrufe (RMI, [Far98]) samt verteilter Speicherverwaltung für verteilte Systeme und Rechnernetze. Der Abstraktionsgrad der Java-Konzepte für Threads und entfernte Methodenaufrufe reicht gerade aus, um ein Rapid Prototyping zu ermöglichen. Die FASAN-Funktionen spiegeln sich dabei ziemlich direkt in den erzeugten Java-Funktionen wider, wodurch die Fehlersuche in FASAN-Programmen mit dem Java-Debugger durchführbar ist. Für jeden Funktionsaufruf in FASAN mit Lokations-Annotation kann prinzipiell ein separater Kontrollfluss als Thread erzeugt oder bei verteilter Ausführung auf einem Rechnernetz ein entfernter Methodenaufuf verwendet werden, um die nötige Parallelit¨at zu erreichen. Wie das Java Grande Forum Panel [Pan98] darlegt, werden ernsthafte, auch über den akademischen Bereich hinausgehende Überlegungen angestellt, wie Java als Standardsprache für numerisches Hochleistungsrechnen verwendet bzw. dazu noch tauglich gemacht werden kann. Ein wesentliches Argument für Java ist dabei seine Ausgewogenheit zwischen vertrauten Konzepten und der Beseitigung von zeitfressenden Fehlerquellen durch neuartige Konzepte. Hier stellt sich natürlich die Frage, ob bei so viel Unterstützung von der Java-Seite überhaupt Bedarf für FASAN besteht. FASANs Beitrag liegt in der Vereinfachung der Koordination für parallele oder verteilte Ausführung. Denn bei der Synchronisation und dem 5.2 Auswahl der Zielsprache 75 entfernten Methodenaufruf handelt es sich um vom Java-Programmierer explizit zu verwendende Konstrukte, mit (fast) allen Fehlerquellen, die explizite verteilte Programmierung mit sich bringt. Die Datenfluss-Semantik des vorigen Kapitels aber hat gezeigt, dass gerade solche hinterh¨altigen Fehler mit FASAN gar nicht passieren können. Abgesehen davon sparen wir mit jedem auf die implizite Seite einer Sprache verschobenen Konstrukt Verwaltungsaufwand im Programm und gewinnen mehr Flexibilit¨at für kurzfristige oder experimentelle Änderungen. Bei FASAN muss z. B. keinerlei Code zum Senden und Empfangen von Nachrichten geschrieben werden, der für die direkten Verbindungen und den Transport der hierarchischen Datenstrukturen nötig ist. Die drei wichtigsten Argumente für FASAN sind aber die im vorigen Kapitel nachgewiesene automatisch erreichte Datenlokalit¨at, die direkte Kommunikation, sowie der erhöhte Parallelit¨atsgrad trotz komfortabler und für die Algorithmen logischer Aufschreibung. 5.2.1 Auswahl der Parallelisierungsplattform Untersuchen wir das als Parallelisierungsplattform recht komfortabel erscheinende RMI etwas genauer. Speziell interessieren die Kosten von RMI bei zwei einfachen Beispielen, die aber beide für die von FASAN anvisierten Anwendungen relevant sind. Große Arrays von Grundtypen, vor allem float[] und double[], werden in numerischen Anwendungen oft verwendet, also auch bei FASAN in den externen Funktionen. Die Dauer des Transports mittels RMI aus dem JDK 1.2 auf einer Ethernet-Verbindung zwischen Workstations vom Typ Sun Ultra-60 sind in Abb. 5.1 dargestellt. Für kleine Arrays ist die Übertragungszeit ann¨ahernd konstant. Auff¨allig ist das starke Ansteigen der Zeit ab 512 Array-Eintr¨agen, ab der die Seitengröße des physikalischen Speichers der Maschine überschritten wird. Danach steigt der Zeitbedarf etwa proportional zur ArrayGröße an.Ähnliche Beobachtungen wurden auch in der Arbeit [VvM 98] gemacht, wo zus¨atzlich durch Modifikation im JIT Compiler (Just In Time Compiler) von Java bedeutende Effizienzsteigerungen erreicht wurden. Wenn wir das Standard-JDK verwenden, scheint es also günstiger zu sein, große Arrays bei entfernten Methodenaufrufen in kleinere Teile zu zerlegen. Ein weiterer Effekt waren die im Experiment beobachteten sehr starken Streuungen der Zeiten, die teilweise mit der variierenden Netzlast begründet werden können. Eine noch wichtigere Rolle spielen hierarchische Objektstrukturen im FASAN-System, und zwar sowohl für die von FASAN definierten hierarchischen Datentypen als auch für Strukturen, die in den externen Funktionen verwendet werden, etwa dünnbesiedelte Matrizen. Bei den Versuchsergebnissen in Abb. 5.2 f¨allt auf, dass die Setup-Zeiten für die Kommunikation vernachl¨assigbar sind. Die Zeiten für dieÜbertragung einer Datenstruktur scheint aber leider leicht überproportional mit ihrer Größe anzusteigen. Des Weiteren sehen wir, dass die Zeit zur Übertragung der gleichen Datenmengen in einem Baum im Schnitt um eine Größenordnung teurer ist als in einem flachen Array. Verantwortlich sind Mechanismen bei Javas Serialisierung, also beim Packen der Baumstruk- 76 Übersetzung von FASAN in Java 262144 65536 RMI mit double[]-Argument und -Ergebnis RMI mit double[]-Argument RMI mit double[]-Ergebnis 16384 Dauer [msec] 4096 1024 256 64 16 4 1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 Arraylaenge Abbildung 5.1 Dauer eines RMI-Aufrufs mit einem Array als Argument bzw. Ergebnis tur. Die Datenstruktur wird automatisch auf Zyklen überprüft, und für jedes Teil-Objekt muss zus¨atzlich Typinformation übertragen werden. Hier wie in einer weiteren Quelle von Effizienzeinbußen l¨asst aber die Klassenhierarchie von RMI Optimierungsmöglichkeiten zu: 1. Für Objekte mit bestimmten Eigenschaften, etwa Objekte ohne zyklische Referenzen wie die hierarchischen Datentypen von FASAN, können effiziente Methoden zur Serialisierung und Deserialisierung von Hand programmiert werden. Dieser Weg wird sich im FASAN-System selbst zur Implementierung globaler Referenzen als sehr nützlich erweisen. 2. Ein weiterer Schwachpunkt von RMI ist die aktuelle TCP/IP-basierte Implementierung des Protokolls JRMP, das standardm¨aßig für entfernte Methodenaufrufe eingesetzt wird. Kommunikationsverbindungen (die Sockets) werden nur für jeweils einen RMI-Aufruf verwendet und dann wieder geschlossen [Pan98]. Zus¨atzliche Kosten verursachen Sicherheits- und Ausfallprüfungen im Protokoll. Jedoch l¨asst die Klassenhierarchie von RMI die Möglichkeit offen, mit etwas Programmieraufwand eine eigene Socket Factory zu schreiben, die Sockets für mehrfache Kommunikationen wiederverwenden kann [Sun98]. Die nur einmalige Verwendung von Sockets liegt in der Konzeption von RMI als ClientServer-System für unsichereÜbertragungskan¨ale begründet. Diese eingebaute Sicherheit kann unter Umst¨anden auch für FASAN vorteilhaft sein, wenn weit entfernte Rechner kooperativ ein Programm auswerten (Global Computing) und auf diese Sicherheits- und Redundanzmechanismen angewiesen sind. 5.2 Auswahl der Zielsprache 77 262144 RMI mit Baum-Argument und -Ergebnis RMI mit Baum-Argument RMI mit Baum-Ergebnis 65536 Dauer [msec] 16384 4096 1024 256 64 16 4 1 2 3 4 5 6 7 8 Baumtiefe 9 10 11 12 13 14 Abbildung 5.2 Dauer eines RMI-Aufrufs mit einem Bin¨arbaum als Argument bzw. Ergebnis Da also auf Protokoll-Ebene Schw¨achen von RMI für das Hochleistungsrechnen teilweise beseitigt werden können, stellt sich die Frage, ob es sinnvolle Alternativen zu RMI auf gleicher Abstraktionsebene gibt. Eine solche Alternative w¨are JavaSpaces [Far98], eine Implementierung des TupelraumModells, das auch in Linda Verwendung findet [Wes92]. JavaSpaces bietet zwar einen höheren Abstraktionsgrad von der parallelen Plattform als RMI, RMI jedoch eignet sich für unsere Zwecke besser für die Auswertung eines FASAN-Programms, da ein verteilter Aufruf in FASAN direkt einem RMI-Aufruf entspricht. Da JavaSpaces auf RMI aufsetzt, ist durch seinen Einsatz auch keine Effizienzsteigerung, sondern eher zus¨atzlicher Overhead zu erwarten. Eine zweite Alternative w¨are CORBA (Common Object Request Broker Architecture, [Vin96]), das mit dem IIOP (Internet Inter ORB Protocol) eine RMI sehr ¨ahnliche API hat. Allerdings ist zu berücksichtigen, dass CORBA mit möglichst vielen Sprachen vertr¨aglich sein muss. Daher bietet die CORBA IDL (Interface Definition Language) im Prinzip nur den kleinsten gemeinsamen Nenner der Sprachen. Spezielle Java-spezifische Optimierungen des Protokolls sind damit nicht möglich. Außerdem ist auch CORBA — ebenso wie RMI — in erster Linie für Client-Server-Computing gedacht ist, mit den entsprechenden Effizienzeinbußen. Zudem müsste der Anwender sich für CORBA einen passenden ORB (Object Request Broker) besorgen und installieren. Die Gründe, die letztlich für die Auswahl von RMI den Ausschlag gegeben haben, sind: 1. RMI ist fester Bestandteil des JDK. Damit ist dann auch FASAN sofort überall dort lauff¨ahig, wo ein JDK installiert ist. 78 Übersetzung von FASAN in Java 2. RMI ist zwar nicht besonders effizient; die Klassenhierarchie bietet jedoch Möglichkeiten, bei Bedarf stellenweise von Hand Optimierungen einzufügen, etwa die Zerteilung großer Datenobjekte für die Serialisierung. 3. Als Quasi-Standard für die verteilte Programmierung mit Java steht RMI auch im Zentrum von Verbesserungsbestrebungen, von denen FASAN dann direkt profitieren kann. So wurden durch zahlreiche Forschungsgruppen schon Überlegungen angestellt und Vorschl¨age unterbreitet, wie eine optimierte Implementierung von RMI für den Bereich des technisch-wissenschaftlichen Rechnens zu gestalten ist [Pan98]. Somit ist also RMI, gerade für prototypische Implementierungen, die zur Zeit beste Alternative. 5.3 Übersetzung in ein sequentielles Grundprogramm Ausgangspunkt für die Übersetzung ist die abstrakte Syntax aus Kapitel 3 auf Seite 39. Wir müssen festlegen, wie zum einen die rekursiven Funktionen aus zu übersetzen sind, und zum anderen, wie die hierarchischen Datentypen aus mit ihren Konstrukto ren aus und Selektoren aus zu implementieren sind, damit sie der Semantik der Datenflussgraphen und dem Kabelbaum-Prinzip Rechnung tragen. .( 5.3.1 Rekursive Funktionen Alle rekursiven Funktionen werden in einer Klasse zusammengefasst, die wir den FASAN-Server nennen. Ihre Kennzeichen sind Zuweisungen mit nicht-geschachtelten Ausdrücken, explizite Variablen für Zwischenergebnisse sowie bedingte Ausdrücke als einzige Kontrollstruktur. Prinzipiell lassen sie sich ziemlich direkt in Java-Methoden übertragen. Greifen wir zur Verdeutlichung das Beispiel 3.5 von Seite 34 für die Iteration über B¨aumen auf. Beispiel 5.1 Die Java-Methode, die aus der FASAN-Funktion iterate (ohne das Schlüsselwort recycle) im FASAN-Server generiert wird, benennt und typisiert alle Variablen, auch die Zwischenergebnisse: public class TreeExample { [...] public Tree iterate (Tree t) { // Typvereinbarungen aller Variablen Tree t_TAG_; Tree t0; boolean $2; Node $1; 5.3 Übersetzung in ein sequentielles Grundprogramm 79 // $1 $2 if Liste aller Zuweisungen = t.val(); = javaFunctions.stop($1); ($2) { t_TAG_ = t; } else { t0 = javaFunctions.dc(t); t_TAG_ = iterate(t0); } return t_TAG_; } } Die Typen von Variablen ergeben sich aus den Ergebnisparametern der aufgerufenen Funktion auf der rechten Seite von Zuweisungen. Namen für Hilfsvariable können wir einfach mittels Durchnummerieren innerhalb einer Funktion vergeben. Die Struktur des FASAN-Programms ist im erzeugten Java-Code besser zu erkennen, wenn eine FASAN-Funktion auch genau einer Java-Methode entspricht. Für die Zweige von Fallunterscheidungen verzichten wir daher auf Hilfsfunktionen, die den Funktionsknoten entsprechen würden. Diese Hilfsfunktionen sind im Datenflussgraphen im Grunde nur ein technisches Hilfsmittel, um für Fallunterscheidungen die strikte Abbildung des Schaltens von Funktionsknoten zu umgehen. Ein programmtechnischer Vorteil entsteht dadurch auch bei endrekursiven Aufrufen im Rumpf der Funktion. Da dieser Aufruf nicht in einer Hilfsfunktion steht, ist die Übersetzung in eine effiziente Schleife sehr leicht: 1. Der Funktionsrumpf wird in eine Endlos-Schleife eingeschlossen. 2. Alle Zweige, die nicht die endrekursiven Aufrufe enthalten, werden um ein break; erg¨anzt, also um den Abbruch der Iteration. 3. Bei den anderen Zweigen treten an Stelle des endrekursiven Aufrufs Zuweisungen von den Aufrufparametern an die formalen Parameter. Beispiel 5.2 Belassen wir in der FASAN-Funktion iterate aus dem vorigen Beispiel das Schlüsselwort recycle zur Optimierung der Endrekursion, wird eine Schleife erzeugt. public Tree iterate (Tree t) { Tree t_TAG_; Tree t0; boolean $2; Node $1; while (true) { $1 = t.val(); $2 = javaFunctions.stop($1); 80 Übersetzung von FASAN in Java if ($2) { t_TAG_ = t; break; } else { t0 = javaFunctions.dc(t); t = t0; } } return t_TAG_; } Hat eine Funktion mehr als einen Ergebnisparameter, ist allerdings eine neue Klasse FRAME__ erforderlich, die alle diese Ergebnisse zusammenfasst und deren Instanzen in diesen F¨allen als Ergebnisse verwendet werden. Eine solche Klasse wird für jede re kursive Funktion erzeugt. Beispiel 5.3 Um in einem verteilten Algorithmus eine Grundlage für die Lastverteilung zu gewinnen, muss eine Funktion, die einen Baum adaptiv erweitert, zus¨atzlich zu dem erweiterten Baum eine Datenstruktur load mit Sch¨atzungen der Rechenlast der einzelnen Baumknoten zurückliefern. Mit dem externen Typ LTree ergibt eine solche Funktion type LTree extern Java; function ExtendTree (T:Tree; destin:int[]; opts:Options) -> T’:Tree; load’:LTree die Java-Klasse public class FRAME__ExtendTree { public transient Tree T; public transient int[] destin; public transient Options opts; public Tree T_TAG_; public LTree load_TAG_; public FRAME__ExtendTree() { } public FRAME__ExtendTree(Tree T_TAG_, LTree load_TAG_){ this.T_TAG_ = T_TAG_; this.load_TAG_ = load_TAG_; } } Im Vorgriff auf die verteilte Ausführung markieren wir die Eingangsparameter mit transient, damit sie nicht kopiert und verschickt werden, wenn der FRAME als Ergebnis eines entfernten Methodenaufrufs zurückgesendet wird. Solche FRAME-Klassen werden wir im Übrigen auch zur Parameterübergabe an Threads im Abschnitt 5.4 gebrauchen können. 5.3 Übersetzung in ein sequentielles Grundprogramm 81 5.3.2 Hierarchische Datentypen .( Für jeden hierarchischen Datentyp legen wir eine Verpackungs“-Klasse mit ” dem Namen an, die den Puffern des Datenflussgraphen entspricht. Sie enth¨alt nur eine Variable content, die den tats¨achlichen Inhalt — also die Belegung des Puffers — repr¨asentiert. Außerdem enth¨alt sie zwei Konstruktoren: einen mit Parametern für den Fall, dass der Inhalt schon vorliegt, und einen ohne Parameter, sodass im Sinn der Datenflussgraph-Semantik der Puffer erzeugt werden kann, auch wenn noch kein Inhalt verfügbar ist. Des Weiteren enth¨alt die Klasse alle Selektor- und Pr¨adikatfunktionen, die in der Datentyp-Definition vereinbart wurden. Beispiel 5.4 Der hierarchische Datentyp für Bin¨arb¨aume type Tree = Leaf (leafVal:Node) | Branch (branchVal:Node; left,right:Tree) erzeugt die Verpackungsklasse public class Tree { Tree__CONTENT content; public Tree() { } public Tree(Tree__CONTENT c) { content = c; } public boolean isLeaf() { return (content instanceof Leaf); } public Node leafVal() { return ((Leaf) content).leafVal; } public boolean isBranch() { return (content instanceof Branch); } public Node branchVal() { return ((Branch) content).branchVal; } public Tree leftTree() { return ((Branch) content).leftTree; } public Tree rightTree() { return ((Branch) content).rightTree; } } .( ist __CONTENT nur ein Java-Interface ohne Für jeden hierarchischen Datentyp Variablen und Methoden. Implementiert wird dieses Interface durch Klassen, die wir für , die diesen Datentyp aufbauen, generieren. die Konstruktoren aus 82 Übersetzung von FASAN in Java Beispiel 5.5 Der Konstruktor Branch aus dem hierarchischen Datentyp Tree führt auf die Java-Klasse public class Branch implements Tree__CONTENT, Serializable { Node branchVal; Tree leftTree; Tree rightTree; public Branch() { } public Branch(Node branchVal, Tree leftTree, Tree rightTree) { this.branchVal = branchVal; this.leftTree = leftTree; this.rightTree = rightTree; } } 5.3.3 Externe Funktionen und Typen Der FASAN-Compiler erzeugt zus¨atzlich zu der Klasse für den FASAN-Server (z. B. TreeExample) ein Java-Interface mit der Namensendung ExternFunctions (z. B. TreeExampleExternFunctions), in dem alle externen Funktionen aufgeführt sind, die das FASAN-Programm benötigt. Eine Instanz einer Klasse, die dieses Interface implementiert, muss dem FASAN-Server bei dessen Erzeugung übergeben werden. Beispiel 5.6 In unserem Beispiel beinhaltet der FASAN-Server also class TreeExample { public static TreeExample fasanFunctions; public static TreeExampleExternFunctions javaFunctions; public int myHost = 0, numHosts = 1; public TreeExample (TreeExampleExternFunctions teef) { fasanFunctions = this; javaFunctions = teef; } [...] } Die Klassenvariablen fasanFunctions und javaFunctions werden mitgeführt, um nicht in jeder Klasse der hierarchischen Datentypen eine Variable zum Zugriff auf die rekursiven und externen Funktionen halten zu müssen. Zu Zwecken der Initialisierung oder auch für optimierte sequentielle Funktionen hat es sich als angenehm für den Programmierer erwiesen, hierarchische Datenstrukturen von FASAN auch in externen Funktionen aufbauen und zerlegen zu können. Um nicht immer zwei Instanzen (Verpackung und Inhalt) kreieren zu müssen, versehen wir die Konstruktor-Klasse mit einer Factory-Methode. 5.3 Übersetzung in ein sequentielles Grundprogramm 83 Beispiel 5.7 Zur Erzeugung eines Baums in externen Funktionen ist folgende Factory-Methode in der Klasse Branch nützlich, die die Erzeugung mittels Branch.create(n,l,r) erlaubt: public static Tree create (Node branchVal, Tree leftTree, Tree rightTree) { return new Tree (new Branch(branchVal, leftTree, rightTree)); } 5.3.4 Funktionen h¨ oherer Ordnung Programmierer, die schon mit funktionalen Sprachen gearbeitet haben, wissen die Ausdrucksm¨achtigkeit von Funktionen höherer Ordnung zu sch¨atzen. Auch in FASAN können Funktionen höherer Ordnung nützlich sein, um etwa Konzepte des Bird-MeertensFormalismus [Bir88] oder der Skeletons [Col98] zu implementieren. Eine einfache Möglichkeit derÜbersetzung von Funktionen höherer Ordnung nach Java würde das Pizza-Paket bieten [OW97b], das als Oberklasse von Java erlaubt, Methoden als Argumente und Ergebnisse von anderen Methoden zu verwenden. Da es aber nicht zum Standard des JDK gehört, müsste es extra installiert werden, was wir ja gerade im Sinn der einfachen Installation und Portabilit¨at des FASAN-Systems vermeiden wollen. Für Funktionen höherer Ordnung in FASAN benötigen wir zwei Mechanismen: Wir müssen Funktionen, die Parameter anderer Funktionen sind, oder allgemei ner Abschlüsse (partielle Funktionsanwendungen, wie sie als s in der Datenflussgraph-Semantik entstehen), handhaben können. Dafür brauchen wir ein Java-Interface als Typ für derartige Parameter und Variablen. Wenn die Typprüfung schon zur Übersetzungszeit stattfindet — was auf jeden Fall sinnvoll und effizienter als Laufzeit-Prüfung ist — genügt hier ein einziges interface FRAME extends Runnable{ }. Diese Interface ist dann der Typ jedes Parameters und jeder Variablen, die Funktionstyp hat. Zur Laufzeit können diesen Parametern und Variablen dann beliebige Instanzen davon abgeleiteter FRAMEs zugewiesen werden. Um passende konkrete Abschluss-Objekte in Java erzeugen zu können und um an einen bestehenden Abschluss weitere Argumente binden zu können, erweitern wir die Klassen FRAME__ , die für jede rekursive Funktion aus generiert werden. Wir erweitern zun¨achst die FRAMEs um ein boolean-Feld names curried, dessen L¨ange die Anzahl der Argumente der entsprechenden Funktion ist. Das -te Element dieses Feldes gibt an, ob das -te Argument der Funktion noch aussteht, also durch Currying ausgelassen wurde. Zus¨atzlich ist eine run()-Methode notwendig, mit der die Funktion 84 Übersetzung von FASAN in Java zum Rechnen angestoßen wird, wenn alle Argumente vorhanden sind. Dies entspricht im Datenflussgraphen der Auswertung eines -Knotens. Beispiel 5.8 In die Funktion mapDown aus Beispiel 3.10, function mapDown (f:(Object,Object)->Object; s:Object; t:Tree) -> t’:Tree, kann als erstes Argument z. B. eine Funktion function SolveDown (parent:Object; node:Object) -> newNode:Object eingesetzt werden, deren Klasse beinhalten muss: class FRAME__SolveDown implements FRAME { boolean[2] curried; Object parent; Object node; Object newNode; public void run() { if (!curried[0] && !curried[1]) newNode = SolveDown(parent,node); } } Es sind also FRAME-Klassen nicht nur für rekursive Funktionen , sondern auch für alle externen Funktionen eines FASAN-Programms zu generieren. Zus¨atzlicher Code ist auch an folgenden Stellen der Methoden im FASAN-Server zu erzeugen: 1. dort, wo eine Funktionsanwendung partiell ist, wo also ein neues Abschluss-Objekt vom Typ FRAME erzeugt und mit den hier schon übergebenen Argumenten initialisiert werden muss; und 2. an den Stellen, wo eine Funktionsvariable — also ein Abschluss — auf eine Argument-Liste angewendet wird. Hier ist zu prüfen, ob nur die neuen Argumente in den FRAME einzutragen sind, oder ob die run()-Methode angestoßen wird, falls alle Argumente zur Verfügung stehen. Da diese Konzepte für Funktionen höherer Ordnung nur programmiertechnische Vorteile bringen, aber für die Parallelisierung nichts Neues beitragen, verzichten wir hier auf weitere Beispiele und die detaillierte Angabe des Java-Codes. 5.4 Erweiterungen für gemeinsamen Speicher mit Threads 85 5.4 Erweiterungen für gemeinsamen Speicher mit Threads In diesem Abschnitt sollen Techniken vorgestellt werden, mit deren Hilfe die parallele Ausführung eines FASAN-Programms auf mehreren Prozessoren mit gemeinsamem Speicher — also typischerweise auf SMP-Maschinen — ermöglicht wird. Wir werden den in Java integrierten Thread-Mechanismus verwenden. Dabei ist bezüglich der Lokationsfunktionen im FASAN-Programm nur entscheidend, dass überhaupt neue Kontrollflüsse erzeugt werden. Wo ein neuer Thread ausgeführt wird, bestimmt ab Java 2 (JDK1.2) der Scheduler des Betriebssystems, da Java ab dieser Version native POSIX-Threads unterstützt [AG98, Sun98]. Liefert der Lokationsaufruf einen Wert größer oder gleich Null, so erzeugt der FASAN-Code einen neuen Thread. Ansonsten erfolgt der Funktionsaufruf auf dem Stack im selben Kontrollfluss. Zun¨achst halten wir fest, dass Funktionen höherer Ordnung bei der Implementierung als paralleles und verteiltes Programm keine technischen Konsequenzen haben. Die FRAMEKlassen, die im vorigen Abschnitt schon erkl¨art wurden, sind zum Programmstart für alle rekursiven und externen Funktionen gem¨aß dem SPMD-Modell (single program multiple data) auf allen Prozessoren bzw. Rechnern verfügbar und können damit die benötigten Abschlüsse bilden. Ein Abschluss selbst führt noch keine Berechnungen aus; er tr¨agt also nichts zur Parallelit¨at eines Programms bei. Auch externe Funktionen, als sequentiell vorausgesetzt, können wie bisher eingebunden werden. In diesem und dem n¨achsten Abschnitt müssen wir uns also nur um die Anpassung der FASAN-Serverklasse und der Klassen für die hierarchischen Datentypen kümmern. 5.4.1 Rekursive Funktionen Thread-Systeme erledigen das Scheduling und die Verteilung der Threads (in FASAN sind das die mit Lokationen versehenen Funktionsaufrufe, siehe Seite 34) auf die verfügbaren Prozessoren selbst. Damit können wir uns den expliziten Aufbau des Datenflussgraphen und die Verwaltung der Funktionsknoten zur Laufzeit sparen. Für jede rekursive Funktion, die mindestens einmal im FASAN-Programm mit einer Lokation aufgerufen wird, erzeugen wir eine Methode, die den Funktionsaufruf in einem eigenen Thread starten kann. Ist das Ergebnis der Lokation mindestens Null, so wird der Funktionsaufruf dann parallel in einem neuen Thread ausgeführt, und der Aufrufer kann unmittelbar nach dem Start des neuen Threads die n¨achsten Funktionsaufrufe ausführen. Beispiel 5.9 Für eine Funktion Solve (par:Node; T:Tree; destin:int[]; opts:Options) -> T’:Tree 86 Übersetzung von FASAN in Java erzeugen wir zus¨atzlich eine Methode im FASAN-Server, die als zus¨atzliches Argument die Lokation bekommt und entsprechend einen neuen Thread starten kann: public Tree Solve__THREAD (Node par, Tree T, int[] destin, Options opts, int newHost) { if (newHost < 0) // sequentieller Aufruf, kein Thread erwünscht return Solve(par, T, destin, opts); // sonst leere Verpackung für Ergebnis anlegen FRAME__Solve $frame = new FRAME__Solve(new Tree()); // Argumente initialisieren $frame.par = par; $frame.T = T; $frame.destin = destin; $frame.opts = opts; $frame.newHost = newHost; // neuen Thread erzeugen und starten new Thread($frame).start(); // warten, bis der Thread wirklich gestartet ist boolean threadStarted = false; while (! threadStarted) { try {Thread.sleep(1);} catch(Exception $e) { } synchronized ($frame) { threadStarted = $frame.threadStarted; } } return $frame.T_TAG_; } Nachdem der Thread gestartet ist, kann diese Methode sofort wieder zurückkehren. Im Ergebnisparameter ist dabei nur eine Verpackung Tree ohne Inhalt vorhanden. Dieser Inhalt wird von dem Thread erst im Laufe seiner Berechnung gefüllt. 5.4.2 Hierarchische Datentypem Um die Parallelit¨at wie im Datenflussgraphen auszunützen, ist es wichtig, dass Ergebnisse von Funktionen zun¨achst nur Verpackungen“ sind (analog zu den Puffern im Graphen als ” Verpackung für Belegungen). Diese Verpackungen werden erst sp¨ater von dem Thread, der den Funktionsaufruf abarbeitet, mit Daten gefüllt. Gleichzeitig ist aber die Korrektheit des FASAN-Programms zu gew¨ahrleisten. Der Selektor- oder Pr¨adikataufruf, der tats¨achlich auf den Inhalt der Verpackung zugreift, muss warten, bis der oben genannte Thread die Daten eingetragen hat. Dies l¨asst sich dadurch erreichen, dass die Selektor- und Pr¨adikat-Funktionen für ein Verpackungsobjekt 5.4 Erweiterungen für gemeinsamen Speicher mit Threads 87 in dem gleichen Monitor [HG89] zusammengefasst sind wie der Thread, der die Daten bereitstellt. Als Monitor bezeichnet man ein Programmstück, das zu jedem Zeitpunkt maximal ein Thread ausführen kann. Java ordnet jedem Objekt einen Monitor zu [OW97a]. Der Code, den dieser Monitor umfasst, kann über mehrere Klassen verteilt sein und definiert sich als alle Programmstücke, die über ein und dasselbe Objekt synchronisiert sind, die also in durch synchronized eingeleiteten Programmblöcken stehen. Für unseren Zweck synchronisieren“ wir über die Verpackungsobjekte, die die Ergeb” nisse paralleler Funktionsaufrufe erfassen. Der Thread, der die Funktion aufruft, die den Inhalt für die Verpackung berechnet, stellt sicher, dass er bis zum Abschluss der Funktion Eigentümer der betreffenden Verpackungsobjekte ist, also in dieser Zeit im Monitor der Verpackung rechnet. In dem gleichen Monitor müssen die Selektor- und Pr¨adikatfunktionen ausgeführt werden. Beispiel 5.10 Für die obige FASAN-Funktion Solve muss die Klasse Tree des Ergebnis-Objekts so modifiziert werden, dass die Selektor- und Pr¨adikat-Aufrufe im Monitor des Objekts ausgeführt werden, etwa public class Tree { [...] public synchronized Tree leftTree() { return ((Branch) content).leftTree; } } Zus¨atzlich ist die FRAME-Klasse für die Funktion Solve so zu erweitern, dass sie erstens von einem Thread ausgeführt werden kann (also die Schnittstelle Runnable mit einer Methode run() implementiert) und zweitens w¨ahrend der Ausführung in den Monitoren aller Ergebnisobjekte bleibt. Es wird dann folgender FRAME erzeugt: public class FRAME__Solve implements FRAME, Runnable { [...] public Tree T_TAG_; public int newHost; public boolean threadStarted = false; public void run() { // Monitor der Tree-Verpackung T_TAG_ betreten... synchronized (T_TAG_) { // ... und dies dem Aufrufer signalisieren synchronized (this) { threadStarted = true; } // Funktion im Monitor ausführen Tree $result = fasanFunctions.Solve(par, T, destin, opts); // Ergebnis-Verpackung mit Inhalt füllen T_TAG_.content = $result.getContent(); } } } 88 Übersetzung von FASAN in Java Wird eine Funktion im FASAN-Programm mindestens einmal mit einer Lokation aufgerufen und damit potentiell parallel ausgeführt, müssen auch Ergebnisparameter, die von einem Grund- oder externen Typ sind, eine Verpackungsklasse haben, in die der neue Thread das Berechnungsergebnis als Inhalt eintragen kann. Der Aufruf einer externen Funktion, die dieses Ergebnis verwendet und damit auf den Inhalt der Verpackung zugreift, muss dann solange warten, bis dieser Inhalt verfügbar ist. Das erreichen wir, indem der betreffende externe Funktionsaufruf mit einem Argument der Form variable.getContent() erfolgt. Die betreffende Verpackungsklasse (z.B. DOUBLE_ARRAY) des externen Typs beinhaltet dann die Methode public synchronized double[] getContent() { return content; } Der Vorteil der Verpackungsklassen ist allgemein darin zu sehen, dass die Parallelit¨at auch bei den Verbrauchern des Inhalts erhöht wird. Dies können wir unmittelbar im Datenflussgraphen an parallelen Teilgraphen erkennen. So können die Teilberechnungen, die auf die Ergebnisse führen, ab einer bestimmten Stelle unabh¨angig voneinander sein. Werden Puffer für die Ergebnisse bereitgestellt, kann ein folgender Funktionsaufruf, der nur von einem dieser Puffer abh¨angt, losrechnen, w¨ahrend noch nicht alle Ergebnisse vorhanden sind. Aus Sicht des FASAN-Programmierers ist es also wichtig, die passenden (und lieber zu viele als zu wenige) Funktionsaufrufe mit Lokationen zu versehen, damit eine Funktion das Programm nicht durch Warten an einem Monitor vor einem Selektor- oder Pr¨adikataufruf blockiert. 5.5 Erweiterungen für verteilten Speicher mit RMI Die Verteilung eines FASAN-Programms auf mehrere Rechner geschieht dadurch, dass auf jedem Rechner ein FASAN-Server gestartet wird. Jeder Server kann entsprechend der Lokationsannotierungen in jedem anderen Server eine rekursive Funktion durch entfernten Methodenaufruf starten. Hierfür ist die Server-Klasse von java.rmi.UnicastRemoteObject abzuleiten und außerdem ein Interface mit allen entfernt aufrufbaren Funktionen zu generieren, welches sich wiederum von dem Interface java.rmi.Remote ableitet. Beispiel 5.11 Unsere Server-Klasse TreeExample muss dann für die verteilte Ausführung folgendermaßen ge¨andert werden: import java.rmi.*; import java.rmi.registry.*; import java.rmi.server.*; 5.5 Erweiterungen für verteilten Speicher mit RMI 89 public class TreeExample extends UnicastRemoteObject implements TreeExample__INTERFACE { public static ExampleTree fasanFunctions; public static ExampleTreeExternFunctions javaFunctions; public int myHost, numHosts; Registry registry; /* die Namen aller FASAN-Server */ String[] hostNames; /* alle FASAN-Server */ public TreeExample__INTERFACE[] servers; public TreeExample (TreeExampleExternFunctions javaFunctions, int myHost, String[] hostNames) throws RemoteException { [...] } [...] } Es ist hier eine Menge Code für organisatorische Details vom FASAN-Compiler zu generieren. Speziell müssen die Server ihre Namen in den Registrierungsprozessen eintragen und ihre Referenzen untereinander austauschen. Darauf gehen wir hier aber nicht n¨aher ein. Die class-Dateien, die der Java-Compiler aus den Server-Klassen generiert, müssen anschließend noch mit dem Compiler rmic in sogenannte Stub- und Skeleton-Klassen für die verteilte Kommunikation übersetzt werden, siehe Abbildung 5.3. Diese Klassen automatisieren die Verwaltungsarbeit für die entfernten Methodenaufrufe, insbesondere die Serialisierung und Deserialisierung von Argumenten und Ergebnissen. 5.5.1 Rekursive Funktionen Bei Parallelrechnern mit verteiltem Speicher und bei Workstation-Netzen muss der Programmierer durch Lokationen selbst angeben, wo der neue Kontrollfluss bzw. Prozess gestartet werden soll; vgl. die Beispiele im Abschnitt 3.1.5. Der FASAN-Code hat hier zus¨atzlich noch sicherzustellen, dass der Wert des Lokationsaufrufs eine gültige Rechnernummer ist. Andernfalls wird der Funktionsaufruf auf dem Rechner(knoten) des Rufenden ausgeführt. Zus¨atzlich zu der eigentlichen Berechnungsmethode und der Thread-Startmethode werden für jede rekursive Funktion drei Methoden generiert. Die erste ist einfach eine Schnittstelle für einen entfernten Aufruf wie im folgenden Beispiel. 90 Übersetzung von FASAN in Java TreeExample.fas fasan2 TreeExampleExternFunctions.java Tree.java Leaf.java Branch.java FRAME__... .java GlobalRef.java javac ... TreeExample.java javac TreeExample.class TreeExampleInterface.java javac TreeExampleInterface.class rmic TreeExample_Stub.class TreeExample_Skel.class Abbildung 5.3 Vom FASAN-Compiler erzeugte Klassen und deren weitere Übersetzung Beispiel 5.12 public Tree Solve__REMOTE (Node par, Tree T, int[] destin, Options opts, int newHost) throws RemoteException { return Solve__THREAD (par, T, destin, opts, newHost); } Diese Methode ist aus Effizienzgründen notwendig, weil die generierten Stub- und Skeleton-Klassen für den gewöhnlichen nicht-verteilten Aufruf zu viel Overhead produzieren und wir deshalb weiterhin die einfachen nicht-verteilten Methoden brauchen. Die zweite generierte Methode ist eine Verzweigung in die benötigte sequentielle, parallele oder verteilte Variante. Beispiel 5.13 public Tree Solve__SWITCH (Node par, Tree T, int[] destin, Options opts, int newHost) { if (newHost < 0) // => sequentiell return Solve(par, T, destin, opts); else if (newHost == myHost || newHost >= numHosts || servers[newHost] == null) // => parallel return Solve__THREAD(par, T, destin, opts, newHost); else { // => verteilt Tree $frame = null; try { $frame = servers[newHost]. Solve__REMOTE (par, T, destin, opts, newHost); } 5.5 Erweiterungen für verteilten Speicher mit RMI 91 catch (RemoteException ex) { /* Error message*/ } return $frame; } } Diese SWITCH-Methode wird an allen Stellen aufgerufen, an denen im FASANProgramm ein rekursiver Funktionsaufruf mit einer Lokation annotiert ist. Der zweite Fall, dass der Aufruf in einem eigenen Thread, aber auf demselben Rechner wie der Aufrufer ausgeführt wird, kann auch bei verteilter Berechnung sinnvoll sein, und zwar aus folgenden Gründen: 1. Falls der Rechner mehrere Prozessoren hat, können alle parallel genutzt werden, sofern eine Version des JDK (ab 1.2) installiert ist, die native Threads unterstützt. 2. Hat eine Funktion mehrere Ergebnisparameter, zu deren Berechnung ab einem gewissen Schnitt durch den Datenflussgraphen die Teilgraphen unabh¨angig voneinander sind, werden die Ergebnisse quasi-parallel berechnet. Benötigt die Berechnung eines der Ergebnisse mehr Zeit, blockiert es die Verfügbarkeit der übrigen Ergebnisse nicht, selbst wenn diese l¨angere Berechnung syntaktisch weiter oben im Programm steht. 5.5.2 Hierarchische Datentypen Bei der verteilten Ausführung eines FASAN-Programms stellt ein Verpackungsobjekt für einen hierarchischen Datentyp gleichzeitig eine globale Referenz dar, die möglicherweise über viele Zwischenstationen l¨auft, bis sie bei einem Selektor ankommt. Im Datenflussgraphen wird hierfür ein Tupel aus dem Konstruktornamen und den Referenzen auf die Komponentenpuffer erzeugt. Bezüglich Parallelit¨at gehen wir noch einen Schritt weiter als in der Datenfluss-Semantik: Die globalen Referenzen für die Inhalte werden schon erzeugt und nach Start des neuen Threads gleich zurückgegeben, selbst wenn noch keine der Komponenten des Datentyps verfügbar ist. Die referentielle Transparanz von FASAN erlaubt eine relativ einfache Implementierung globaler Referenzen. Denn sie garantiert, dass jede Variable den ihr einmal zugewiesenen Wert beh¨alt. In prozeduralen Sprachen ist das gerade nicht der Fall, und dort h¨atten wir dann mit dem Problem der Koh¨arenz von Variablenwerten zu k¨ampfen, sobald sie als Kopien in mehr als einem Server im verteilten System gehalten werden. Für die Implementierung von globalen Referenzen bietet Java eine scheinbar einfache Möglichkeit: die RemoteObjects, wie wir sie schon für die FASAN-Server selbst verwendet haben. Sie bieten programmiertechnische Vorteile: 1. Bei entfernten Aufrufen haben sie die benötigte Call-by-Reference-Semantik, damit der Inhalt von Verpackungsargumenten nicht gleich mitübertragen wird wie bei einem für die übrigen Java-Objekte üblichen entfernten Call-by-Value. 92 Übersetzung von FASAN in Java 2. Sie besitzen außerdem eine Variante der hashCode-Methode, die allen physikalischen Kopien eines Objekts, auch auf verteilten Rechnern, denselben objektspezifischen Hash-Code zuordnet. Dieser Code könnte somit unmittelbar als Schlüssel in einer Art Cache für Verpackungsinhalte verwendet werden, um Inhalte bei mehrfacher Dereferenzierung nicht mehrmals übertragen zu müssen. 3. Der Zugriff auf ihren Inhalt — in unserem Fall durch die Selektor-Aufrufe — könnte als Remote-Methode gestaltet werden, und würde ohne zus¨atzlich zu programmierenden Code den Inhalt für eine Referenz automatisch von deren Ursprungsrechner kopieren. Leider zeigte eine erste Implementierung auf dieser Basis mehrere gravierende Nachteile für unseren Anwendungsbereich: 1. Gelangt eine Referenz wieder zurück auf den Ursprungsrechner, ist sie eine Kopie der Original-Referenz, auch wenn sich der Inhalt auf demselben Rechner befindet. Alle Methodenaufrufe (speziell die Dereferenzierung des Inhalts) geschehen dann durch Call-by-Value, wodurch für den Inhalt unnötige Kopien angelegt werden. Bei der Größe der Daten, die im technisch-wissenschaftlichen Umfeld entstehen, kann dies auf keinen Fall akzeptiert werden und führt viel zu schnell zu Speichermangel. 2. Der Speicher von RemoteObjects wird durch einen in Java integrierten verteilten Garbage Collector automatisch verwaltet. Es hat sich aber leider gezeigt, dass dieser nur für eine sehr beschr¨ankte Anzahl von verteilten Objekten brauchbar ist und bei der Menge an Baumknoten, die typischerweise in den rekursiven numerischen Algorithmen vorkommen, das FASAN-Programm fast zum Erliegen bringt. Es ist also unumg¨anglich, eine eigene Implementierung für die verteilten hierarchischen Datenstrukturen zu finden. Kernpunkt jeder Implementierung der Datentypen von FASAN ist die Tatsache, dass der Inhalt einer Verpackung, selbst wenn er schon verfügbar ist, zun¨achst auf seinem Ursprungsrechner zurückbleibt, bis er durch einen Selektoraufruf angefordert wird. Anstatt RemoteObjects erzeugt der FASAN-Compiler eine Klasse class GlobalRef implements Serializable { public int host; int keyCode; transient int refCount; [...] } Die Variablen host und keyCode bilden zusammen eine globale Referenz. Jeder FASAN-Server enth¨alt eine Hash-Tabelle, in die alle Inhalte eingetragen werden, die über globale Referenzen auch für andere Rechner verfügbar sind. Wir nennen diese HashTabelle Referenztabelle. Die Standard-Methode hashCode(), die einen eindeutigen Schlüssel für jedes Java-Objekt erzeugt, ist hierzu nicht erwünscht. Denn verschiedene globale Referenzen, die auf denselben Inhalt verweisen, sollen auch den gleichen Wert als 5.5 Erweiterungen für verteilten Speicher mit RMI 93 Schlüssel in den Referenztabellen haben. Deshalb überschreiben wir die hashCode()Methode und die darauf aufbauende Vergleichsmethode für Objekte. public int hashCode() { return keyCode; } public boolean equals(Object obj) { return keyCode==obj.hashCode(); } Entsprechende Konstruktoren, die keyCode geeignet belegen, sind zus¨atzlich notwendig: public GlobalRef() { keyCode = super.hashCode(); host = ExampleTree.fasanFunctions.myHost; } public GlobalRef (int host, int keyCode) { this.keyCode = keyCode; this.host = host; } Außerdem erweitern alle Verpackungsklassen der hierarchischen Datentypen die Klasse GlobalRef, sind also selbst globale Referenzen und können zwischen FASAN-Servern ausgetauscht werden. In allen Verpackungsklassen muss aber die content-Variable als transient deklariert werden, damit der Inhalt zun¨achst noch nicht mitkopiert wird. Beispiel 5.14 Unsere Klasse Tree aus den Beispielen 5.4 und 5.10 müssen wir abermals ¨andern. import java.io.*; public class Tree extends GlobalRef implements Serializable { transient Tree__CONTENT content; public Tree () { } public Tree (Tree__CONTENT c) { content = c; } public Tree (int host, int keyCode) { super (host, keyCode); } [...] } Die Analogie in der Datenfluss-Semantik sind die Datenelemente, die im Vorbereich eines Konstruktors verweilen, und für die nur ein Tupel mit Namen bzw. Referenzen durch den Datenflussgraphen wandert. 94 Übersetzung von FASAN in Java Host x Host y GlobRefTable 2: server[x].getContentOfTree(42); Host z GlobRefTable GlobRefTable 1: getContent() 3: getContent() GlobalRef Tree 42 x GlobalRef 4 42 x 1 GlobalRef 42 x 1 Tree_CONTENT GlobalRef 42 x 1 Abbildung 5.4 Ein Szenario von globalen Referenz-Objekten, die durch Remote-Aufrufe erzeugt wurden und über die Referenztabelle auf denselben Inhalt verweisen. Gepunktete Pfeile bedeuten nur die logische Dereferenzierung, die über den Umweg der Referenztabelle implementiert wird. Diese explizite Implementierung von globalen Referenzen hat gegenüber der Verwendung von RemoteObjects noch folgende weitere Vorteile: Es müssen nicht für jede Verpackungsklasse zus¨atzlich Interfaces generiert und keine Stub- und Skeleton-Klassen mit rmic erzeugt werden. Die Klasse ist für die Einfachzuweisung der Inhalte entworfen und entsprechend einfacher als allgemeine Remote-Klassen. Der entstehende Overhead f¨allt (wegen der referentiellen Transparenz von FASAN) geringer aus. Wird die Referenz über eine andere entfernte Funktion auf demselben Rechner weitergereicht, wird keine neue Kopie angelegt. Was die Klassen der hierarchischen Datenstrukturen betrifft, so haben alle Methoden zur Dereferenzierung (also getContent und alle Selektorfunktionen) dafür zu sorgen, dass ein noch nicht verfügbarer Inhalt gegebenenfalls automatisch besorgt wird. Falls der Inhalt nicht in der Variable content verfügbar ist, muss er aus der lokalen Referenztabelle geholt werden. Beispiel 5.15 public synchronized Tree__CONTENT getContent() { if (content == null) content = ExampleTree.fasanFunctions.$refTab. getContentOfTree(host, keyCode); return content; } 5.5 Erweiterungen für verteilten Speicher mit RMI 95 Die Selektor- und Pr¨adikatfunktionen stützen sich jetzt ihrerseits auf die getContent()-Methode, um im Fall, dass die content-Variable null ist, den Inhalt über die Referenztabelle zu bekommen. Wenn die Tabelle den Inhalt nicht eingetragen hat, muss sie ihn ihrerseits aus der Tabelle des Ursprungsrechners der globalen Referenz holen. Beispiel 5.16 also Die Pr¨adikatfunktion und die Selektoren des Branch-Konstruktors lauten public boolean isBranch() { return (getContent() instanceof Branch); } public Node branchNode() { return ((Branch) getContent()).branchNode; } public Tree leftTree() { return((Branch) getContent()).leftTree; } public Tree rightTree() { return((Branch) getContent()).rightTree; } Wenden wir uns nochmal den Referenzz¨ahlern zu. Ein Referenzz¨ahler gibt an, wie viele Kopien der globalen Referenz auf dasselbe Objekt verweisen. Da in FASAN keine zyklischen Datenstrukturen aufgebaut werden können, ist dieses Verfahren der direkten Markierung durch Referenzz¨ahler zur Speicherverwaltung trotz seiner Einfachheit effizient [AMR92]. Um keine Speicherprobleme zu bekommen, müssen globale Referenzen, auf die von keinem anderen Rechner aus mehr zugegriffen werden kann, aus der Referenztabelle genommen werden, damit die Speicherverwaltung von Java den Platz wiederverwenden kann. Da es sich um globale Referenzen handelt, müssen Rechner, die die Referenzen nicht mehr benötigen, an den Ursprungsserver, in dessen Referenztabelle der Inhalt eingetragen ist, eine entsprechende Meldung zur Dekrementierung dessen Z¨ahlers schicken. Die Java-Klasse GlobRefTable, die die Referenztabelle implementiert, kann z. B. die Klasse java.util.Hashtable erweiteren oder verwenden. Neben der Methode clear(), die am Ende eines FASAN-Programms alle Eintr¨age in den Referenztabellen der FASAN-Server löscht, sind folgende zwei Methoden zur Aktualisierung der Tabelle nötig: synchronized int incrRefCount(GlobalRef key) muss zun¨achst prüfen, ob eine Referenz auf einen anderen Rechner verweist. Wenn ja, wird die gleiche Methode in der Referenztabelle des entsprechenden Rechners aufgefufen. Ist andernfalls eine Referenz mit dem Hash-Code von key in der lokalen Tabelle noch nicht eingetragen, wird key eingetragen. Anschließend wird der Referenzz¨ahler des jetzt sicher vorhandenen Eintrags inkrementiert und als Ergebnis zurückgegeben. 96 Übersetzung von FASAN in Java synchronized int decrRefCount(GlobalRef key) prüft ebenfalls zuerst, ob es sich bei key um eine Referenz auf einen anderen Rechner handelt und delegiert den Aufruf entsprechend dorthin. Ist key lokal, wird der Referenzz¨ahler des Eintrags in der lokalen Tabelle dekrementiert und die Referenz ggf. aus der Tabelle gelöscht, wenn der Referenzz¨ahler 0 ist. int _CONTENT getContentOf (int host,int keyCode) wird für jeden hierarchischen Datentyp aus der Menge erzeugt. Diese Methode liefert den Inhalt zur Referenz, entweder aus der lokalen Tabelle oder aus der Tabelle auf dem Rechner host. ( In die Referenztabelle tragen wir dann alle Referenzen ein, die mindestens einmal auf andere Rechner kopiert wurden. Dies kann in drei F¨allen vorkommen beim Aufruf einer verteilten Funktion für die Argumentparameter, bei der Rückkehr einer verteilten Funktion für die Ergebnisparameter, bei Anforderung des Inhalts für eine globale Referenz von einem anderen Rechner aus, die aus dem Aufruf einer Selektor- oder Pr¨adikatsfunktion resultiert. Mit Hilfe der GlobalRef-Klasse kann eine sehr einfache Implementierung dieser Verwaltungsstrategie erfolgen. Erstens immer dann, wenn eine Objekt r vom Typ GlobalRef auf einen anderen Rechner geschickt wird, denn dann wird ein neues Objekt erzeugt und der Referenzz¨ahler auf dem Ursprungsrechner r.host ist zu erhöhen. Zweitens ist der Referenzz¨ahler auf dem Ursprungsrechner zu dekrementieren, wenn ein Objekt vom Typ GlobalRef von der Speicherverwaltung entfernt wird. Dem JavaProgrammierer steht hierfür eine Kontrollmöglichkeit zur Verfügung, wann ein Objekt kopiert (serialisiert) und wann es vernichtet (finalisiert) wird. Indem wir in die GlobalRef-Klasse die beiden folgenden Methoden aufnehmen, erhalten wir genau den gewünschten Effekt. Den ersten Fall, die Inkrementierung des Ursprungs-Referenzz¨ahlers bei Anlegen einer Kopie der Referenz, erreichen wir durch Überschreiben der Methode private void writeObject (ObjectOutputStream out) throws IOException { ExampleTree.fasanFunctions.$refTab.incrRefCount(this); out.defaultWriteObject (); } Vor der Speicherfreigabe eines Objekts ruft Java dessen Finalisierungsmethode auf, sofern diese vorhanden ist. Dies nützen wir hier zum Dekrementieren des Ursprungsreferenzz¨ahlers aus: public void finalize() { ExampleTree.fasanFunctions.$refTab.decrRefCount(this); } Dabei sind noch Optimierungen möglich; zum Beispiel kann die Inkrementierung der Referenzz¨ahler verzögert und erst in der Methode readObject() von GlobalRef geschehen, falls der Funktionsaufruf auf dem Ursprungsrechner der Referenz erfolgt. 5.6 Praktische Verwendung von FASAN-Programmen 97 5.6 Praktische Verwendung von FASAN-Programmen Die Berechnung mit einem FASAN-Programm erfolgt ausschließlich durch Aufruf einer rekursiven Funktion eines FASAN-Servers mit aktuellen Parametern. In der Implementierung werden numHosts FASAN-Server erzeugt, deren Klassen die definierten FASAN-Funktionen beinhalten und die gegenseitig diese Funktionen gem¨aß den Lokationen aufrufen. Prinzipiell kann an jeder Stelle eines Java-Programm ein Aufruf einer FASAN-Funktion stehen. Beispiel 5.17 Ein einfaches Java-Programm kann für Berechnungen auf einem Baum die automatisierte Verteilung durch ein FASAN-Programm nutzen, indem es folgende FASAN-Funktion aufruft: function Main (t:Tree; opts:Options) -> t’:Tree. Das Java-Programm könnte so aussehen: import java.rmi.*; import java.rmi.registry.*; public class TreeExampleCaller implements TreeExampleExternFunctions { public static void main (String[] args) { int numHosts = Integer.parseInt(args[0]); String[] hostNames = ...; Options opts = ...; Node startNode = ...; try { TreeExample server = new TreeExample(new TreeExampleCaller(), 0, hostNames); Tree t = Leaf.create (startNode); Tree result = server.Main(t, opts); postprocess(result.nd_TAG_); } catch (Exception e) { e.printStackTrace(); } System.exit(0); } // hier muss die Implementierung der // externen Funktionen folgen... } Wir haben nun die wichtigsten Techniken beschrieben, um die Datenfluss-Semantik in Java zu implementieren und ein übersetztes FASAN-Programm auf mehreren Rechnern verteilt ablaufen zu lassen. Damit können wir uns dem zweiten Teil der Aufgabenstellung 98 Übersetzung von FASAN in Java zuwenden, n¨amlich der FASAN-Implementierung einer größen Anwendung, die über den Charakter einfacher Beispiele hinausgeht. Dieser Anwendung für die numerische Simulation ist das ganze folgende Kapitel gewidmet. 99 Kapitel 6 Anwendung von FASAN: Die rekursive Substrukturierung in der numerischen Simulation Wir werden uns nun mit einem wichtigen Anwendungsgebiet für FASAN im Bereich der numerischen Simulation genauer besch¨aftigen. Es handelt sich um ein rekursives Gebietszerlegungsverfahren zur numerischen Berechnung von Lösungen elliptischer linearer partieller Differentialgleichungen mit konstanten Koeffizienten, wie sie beispielsweise in Problemen der Struktur- und der Strömungsmechanik auftreten. Zu deren numerischer Lösung wird oft die Finite-Element-Methode [Bat96, Bra97, Bur91] eingesetzt, die bei dem betrachteten Differentialgleichungstyp ein lineares Gleichungssystem liefert. Bei dem hier verwendeten Verfahren entsteht — entsprechend der rekursiven Zerlegung des Gebiets, auf dem eine diskrete N¨aherungslösung für die Differentialgleichung berechnet werden soll — ein Abhängigkeitsbaum von linearen Gleichungssystemen. Das Verfahren bietet eine Reihe von Vorteilen bei der Lösung von Simulationsaufgaben: Es ist modular aufgebaut: Bei richtiger Implementierung können unterschiedliche Diskretisierungstechniken und unterschiedliche Löser für die Gleichungssysteme eingesetzt werden. Die sich natürlicherweise ergebenden Baumstrukturen bieten sich für die Zerteilung und verteilte Bearbeitung an. Durch die hierarchische Strukturierung ist die adaptive Vertiefung des Baums, die einer Verfeinerung des Gitters entspricht, möglich. Es eignet sich nicht nur als eigenst¨andiges Verfahren, es kann auch auf einfache Weise andere Programme einbinden und aufrufen, welche Lösungen für bestimmte Teilgebiete liefern. Der Zusammenbau der Lösungen wird dann vollautomatisch über die hierarchische Baumstruktur geleistet. Obgleich sich das Verfahren prinzipiell für die verteilte Bearbeitung sehr gut eignet, ist die tats¨achliche Erstellung einer Implementierung nicht trivial. Denn die rekursive Baum- 100 Die rekursive Substrukturierung in der numerischen Simulation struktur, die Datenstrukturen für die Gleichungssysteme und für ihre wechselseitigen Beziehungen sowie die Adaptivit¨at des Verfahrens machen die Programmierung der benötigten Kommunikationsroutinen, etwa mit einer Message-Passing-Bibliothek wie PVM oder MPI, aufwendig und anf¨allig für Programmierfehler. Erschwerend kommt dabei hinzu, dass verschiedene Lösungsmethoden zwar vom Prinzip her leicht zu integrieren sind, aber jede Methode auf den Austausch unterschiedlicher Daten zwischen den Gleichungssystemen angewiesen ist. Genau hier setzt FASAN an: Es eignet sich zur Formulierung rekursiver Algorithmen, wobei das Versenden der entstehenden hierarchischen Datenstrukturen automatisch gem¨aß der Verteilung der rekursiven Funktionsaufrufe erfolgt, und zwar, wie wir in Kapitel 4 gesehen haben, unter größtmöglicher Erhaltung der Datenlokalit¨at. Damit die entsprechenden Datenstrukturen kommuniziert werden, sind nur die entsprechenden formalen Parameter bei den Funktionen nötig, die auf dem Baum arbeiten. Um dieses vielseitig einsetzbare Verfahren verstehen zu können, benötigen wir noch einige Grundlagen, bevor wir uns der Programmierung in FASAN wieder zuwenden. Wir wollen also zun¨achst in Abschnitt 6.1 grundlegende Techniken zur Beschreibung der adaptiven rekursiven Gebietszerlegung vorzeigen. Es schließt sich Abschnitt 6.2 an, der beschreibt, wie die Gleichungssysteme in den Knoten des Substrukturierungsbaums aufgebaut werden. Dabei gehen wir etwas genauer auf die Technik der Transformation der Gleichungssysteme auf die hierarchische Basisdarstellung ein. Abschnitt 6.3 erw¨ahnt kurz existierende Lösungsverfahren, w¨ahrend Abschnitt 6.4 drei neue Lösungsvarianten für den Baum von Gleichungssystemen vorstellt. Schließlich werden Möglichkeiten zur statischen und dynamischen Lastverteilung bei paralleler Ausführung in Abschnitt 6.6 diskutiert. 6.1 Gebietszerlegung Die Grundlagen von Gebietszerlegungsverfahren sind in [Bur91] und vor allem [SBG96] nachzulesen. Die spezielleren Details des hier verwendeten rekursiven Verfahrens sowie die ersten beiden weiter unten beschriebenen Lösungsverfahren stammen darüber hinaus aus den Arbeiten von Hüttl und Schneider [HS94, Sch95, Hüt96], die wiederum auf den origin¨aren Ideen von George und Reid [Geo73, GL78, Rei84] basieren. Bei dem Verfahren der rekursiven Substrukturierung wird das Gesamtproblem analog zur Schurkomplementmethode [Saa96, SBG96] in Teilprobleme zerlegt, so wie es am Beispiel einer an der linken Seite eingespannten Kragscheibe in Abb. 6.1 links oben dargestellt ist. Die Zerlegung geschieht aber stufenweise: Jedes Teilgebiet kann rekursiv wieder in Teile zerlegt werden (Abb. 6.1, links). Neben der synthetischen Zerlegung zur adaptiven lokalen Verfeinerung der Gitterweite nach Maßgabe eines Fehlersch¨atzers kann die Zerlegung für die oberen Baumebenen auch auf natürliche Weise erfolgen, z. B. wenn das Bauteil aus mehreren Komponenten zusammengesetzt ist. 6.1 Gebietszerlegung 101 f Freier Rand Separator Fester Rand Inkompat. Punkte 1 " " 2 3 " 1 3 2 7 5 6 5 7 9 8 10 11 14 15 Sfrag replacements 3 2 4 6 4 8 9 10 11 4 5 14 15 6 16 17 18 19 20 21 22 23 8 9 10 11 17 18 19 7 14 15 16 18 17 19 20 22 21 23 16 20 21 22 23 Abbildung 6.1 Rekursive Substrukturierung: Links ist die Gebietszerlegung einer an der linken Seite eingespannten Kragscheibe dargestellt. Dick eingezeichnet sind jeweils die Separatorlinien, die die Gebiete rekursiv für die n¨achsttiefere Ebene aufteilen. Die Nummern sind Identifikatoren Rechts ist der Bottom-up-Schritt skizziert, in dem jeweils das Gleichungsfür die Teilgebiete. " " " 3 für die Freiheitsgrade im system 6 Vaterknoten aus den Gleichungssystemen der Sohnknoten assembliert wird. Bei der Assemblierung der Knotengleichungssysteme ist die Rolle der Freiheitsgrade der Lösung zu beachten: = Freiheitsgrade auf dem Separator; = Freiheitsgrade auf dem Teilgebietsrand; = inkompatible (h¨angende) Punkte auf dem Separator; = durch Dirichlet-Randbedingungen festgelegte Werte. Ist die Zerlegung festgelegt, werden die linearen Gleichungssysteme, die die Lösung auf dem jeweiligen Teilgebiet beschreiben, schrittweise von unten nach oben aufgebaut (Abb. 6.1, rechts). Wir nennen sie Knotengleichungssysteme und den entstehenden Abh¨angigkeitsbaum von Knotengleichungssystemen den Substrukturierungsbaum. Der (gesuchte) Lösungswert von an einem Punkt des Gitters eines Baumknotens heißt Freiheitsgrad. Ist für einen Freiheitsgrad der Lösungswert schon vorgegeben wie an der linken Seite der Kragscheibe, handelt es sich um eine Dirichlet-Randbedingung. 102 Die rekursive Substrukturierung in der numerischen Simulation Start Top-down Gitter bzw. Baum verfeinern Baumdurchlauf des Lösers Gleichungssysteme erstellen nein Bottom-up Lösungsgenauigkeit erreicht? nein ja Diskretisierungsfehler klein genug? ja Ende Abbildung 6.2 Ablauf der rekursiven Substrukturierung. Die mit Ovalen dargestellten Schritte erforden komplette Durchl¨aufe des Substrukturierungsbaums. Der algorithmische Rahmen setzt sich aus drei Durchl¨aufen des Substrukturierungsbaums zusammen: 1. Verfeinerung des Substrukturierungsbaums bis zur benötigten Tiefe durch Gebietszerlegung, 2. Erstellen der Gleichungssysteme in den Baumknoten und 3. Lösen der Gleichungssysteme. Da die Gleichungssysteme bei realistischen Anwendungen insgesamt sehr viele Freiheitsgrade (in der Größenordnung von und mehr) enthalten, werden iterative Löser eingesetzt, die bei sehr großen Gleichungssystemen bezüglich Rechenzeit und Speicher effizienter arbeiten. Stellt sich mit der Lösung heraus, dass die Diskretisierung in einem Teilgebiet nicht fein genug war, um die physikalische Realit¨at gut genug anzun¨ahern, werden die oben genannten drei Baumdurchl¨aufe wiederholt. Ein grobes Ablaufdiagramm entspricht somit Abb. 6.2. 6.1.1 Beschreibung der Substrukturierung Zum Aufbau des Substrukturierungsbaums ist die Identifizierung von Punkten im Gebiet eines Baumknotens mit den Punkten in den Teilgebieten seiner Söhne nötig. Zun¨achst müssen wir beschreiben können, wie und an welchen Kanten Teilgebiete aus den Sohnknoten zum Gebiet des Vaters zusammengesetzt werden. Um das Gleichungssystem eines Baumknotens zu erstellen, werden von den Sohnknoten jeweils nur Punkte von deren R¨andern verwendet. Zum Zusammenbau werden, wie wir in Abb. 6.3 sehen, die wesentlichen Berandungen des Gebiets des Knotens nummeriert, unterteilt nach Punkten und Linien. Nur die Punkte an den Ecken eines Knotens (P1 bis P4) werden einzeln nummeriert; die übrigen Punkte auf den Linien sind als Bin¨arb¨aume (T1 bis T5, im Folgenden als Linienbäume bezeichnet) organisiert. Die Zusammensetzung 6.1 Gebietszerlegung P1 103 P2 T1 T5 T4 I P4 T3 T4 P4 P3 T1 P2 I II T5 T2 T6 T7 III IV T2 II P1 T3 P3 Abbildung 6.3 Substrukturierungsschemata bei rekursiver Zerteilung in zwei Teilgebiete (links) und in vier Teilgebiete (rechts) eines Gebiets wird dann durch Auflisten der R¨ander der Teilgebiete in der Nummerierung des Gesamtgebiets festgelegt. Manche Linien sind auf zwei Teilgebiete aufgeteilt. Der Anteil einer Linie wird durch die Funktionen left (linke bzw. obere Teillinie), right (rechte bzw. untere Teillinie) und node (Mittelpunkt) zum Ausdruck gebracht. Mit der schematischen graphischen Darstellung aus Abb. 6.3 (links) folgt die Beschreibung I = (P1, left(T1), node(T1), T5, node(T3), left(T3), 4, T4), II = (node(T1), right(T1), P2, T2, P3, right(T3), node(T3), T5). Dabei sind die Namen für Punkte und Linienb¨aume beliebig; wichtig ist der Bezug von Teilgebiet zu Gesamtgebiet, von Sohn- zu Vaterknoten. Eine Teilung des Vaters in vier Sohnknoten können wir mit folgender Substrukturierungsbeschreibung ausdrücken: I = (P1, node(T5), II = (node(T1), node(T2), III = (node(T4), node(T3), IV = (node(T5), P3, left(T1), T6, right(T1), T7, T6, left(T3), T7, right(T3), node(T1), node(T4), P2, node(T5), node(T5), P4, node(T2), node(T3), left(T5), left(T4)), left(T2), left(T5)), right(T5), right(T4)), right(T2), right(T5)). 6.1.2 Klassifikation der Punkte Die Organisation der Punkte auf den Linien in Bin¨arb¨aumen hat nicht nur für die Substrukturierungsbeschreibung Vorteile. Sie ermöglicht es auch, bequem einzelne Teilgebiete adaptiv zu verfeinern. Die Eintr¨age in den Knoten eines Linienbaums sind als Indizes im jeweiligen Knotengleichungssystem zu verstehen. Diese Technik kombiniert die Möglichkeit der Adaptivit¨at der Gebietszerlegung, was auf nicht-balancierte B¨aume führt, und die mögliche Verwendung 104 Die rekursive Substrukturierung in der numerischen Simulation von Standardroutinen für die Lösung der Gleichungssysteme. Deren Vektoren können daher im Speicher in zusammenh¨angenden Feldern abgelegt und für deren Systemmatrizen beliebige Datenstrukturen für dünnbesiedelte Matrizen verwendet werden. Wir bezeichnen die Matrix des Gleichungssystems eines Baumknotens als System matrix, als Lösungsvektor und als Lastvektor. Bei der Vergabe der Indizes für die Punkte ist zu beachten, dass für eine möglichst effiziente Gleichungssystemlösung die Freiheitsgrade zun¨achst in vier Blöcke eingeteilt werden: Externe Freiheitsgrade (Indexmenge , “ ” in Abb. 6.1) auf dem Gebietsrand des Baumknotens. Die Werte dieser Freiheitsgrade werden vom Vaterknoten festgesetzt. In der Wurzel des Substrukturierungbaums ist diese Menge leer. Interne Freiheitsgrade (Indexmenge , “ ” in Abb. 6.1) auf dem Separator des Baumknotens. Dies sind die Freiheitsgrade, deren Werte im Baumknoten selbst durch Relaxation oder Rücksubstitution (je nach Lösungsverfahren) bestimmt werden. In der Wurzel können auch auf den R¨andern interne Freiheitsgrade liegen, wenn sie nicht durch Dirichlet-Randbedingungen festgelegt sind. % Feste Randwerte für den Lösungsvektor (Indexmenge , “ ” in Abb. 6.1) durch Dirichlet-Randbedingungen aus dem Gleichungssystem entkoppelte Freiheitsgrade. Solche Freiheitsgrade werden mit ihrem festgelegten Wert ganz aus dem Gleichungssystem entkoppelt, was wir in Abschnitt 6.2.3 genauer beschreiben. Dazu ist es wichtig, dass für Systemmatrix und Lastvektor alle Beitr¨age aus den Söhnen aufgesammelt sind. Dies ist am Gebietsrand jeweils am Endpunkt eines Separators der Fall, und im Gebietsinneren auf den kompletten Separatoren. Hängende (inkompatible) Punkte (Indexmenge , “ ” in Abb. 6.1) auf dem Separator. Es handelt sich hier um Punkte, die beim Zusammensetzen des Gebiets am Separator nicht in beiden Teilgebieten vorkommen. Gem¨aß den linearen Ansatzfunktionen bei der Diskretisierung werden die Werte dieser Punkte durch lineare Interpolation der Werte der Nachbarknoten bestimmt. Die Freiheitsgrade können (wegen dieser Interpolationsvorschrift) dann ebenfalls aus dem Gleichungssystem entkoppelt werden (genauer in Abschnitt 6.2.3). Daneben kann noch der Index –1 vorkommen, der angibt, dass der betreffende Freiheitsgrad schon in einer tieferen Baumebene entkoppelt wurde und im Gleichungssystem des aktuellen Baumknotens nicht mehr vorkommt. Ist die Klassifikation abgeschlossen, werden den Punkten Indizes zugeteilt, die den Indizes im Knotengleichungssystem entsprechen. Die Zuteilung geschieht am besten in hierarchischer (breitenrekursiver) Reihenfolge, siehe Abb. 6.4. Für das Zusammenfügen zweier Teilgebiete ist noch die Richtung festzulegen, in der in jeder Ebene des Linienbaums die Punktnummerierung erfolgt. Am allgemeinsten verwendbar ist die Festlegung von links nach rechts“ und ggf. von oben nach unten“. Ein ” ” Rundherumlaufen“ auf dem Rand l¨asst sich dagegen schwieriger auf die Separatoren ” übertragen, ist aber auch möglich. 6.2 Aufbau der Gleichungssysteme 105 15 PSfrag replacements 6 3 18 17 13 7 8 12 14 19 16 1 4 9 10 5 11 2 Abbildung 6.4 Die Nummerierung der Punkte eines Knotens des Substrukturierungsbaums geschieht blockweise hierarchisch. Punkte einer Hierarchiestufe sind jeweils durch gleich dicke Kreisr¨ander angedeutet. Zuerst werden die externen Freiheitsgrade auf dem Rand nummeriert, dann die internen Freiheitsgrade im Gebietsinnern und schließlich (feste) Dirichlet-Randwerte und h¨angende Punkte (alle jeweils in hierarchischer Reihenfolge). Wenn wir die oberen Baumebenen von Hand substrukturieren, sollten die resultierenden Separatoren möglichst wenig Knoten aufweisen, denn deren Anzahl bestimmt die Dimension des in jedem Baumknoten zu lösenden linearen Gleichungssystems. Hat das Gebiet Löcher“, wirkt sich das auch unmittelbar in einer geringeren Anzahl von Punkten aus, ” an denen die diskrete Lösung berechnet werden muss. 6.2 Aufbau der Gleichungssysteme Nach den Vorarbeiten für den Aufbau des Substrukturierungsbaums über dem Gebiet des zu berechnenden Problems können jetzt die Knotengleichungssysteme erstellt werden. Konvention: Wird ein Vektor mit einer Indexmenge indiziert, z.B. , so ist die Projektion dieses Vektors auf einen Vektor der Dimension dieser eingeschr¨ankten Indexmenge gemeint. Das Gleiche gilt auch bei Matrizen für die Zeilen- und Spaltenindizes. Die nicht weiter unterteilten Teilgebiete bilden die Bl¨atter des Substrukturierungsbaums. In ihnen haben nach Anwendung einer Diskretisierungstechnik — z. B. der Finite-Element-Methode [Bat96] — die beiden Teilprobleme jeweils die Gestalt eines blockstrukturierten linearen Gleichungssystems, bei dem alle Freiheitsgrade auf dem Gebietsrand des Blattknotens von den Freiheitsgraden im Gebietsinnern getrennt sind: (6.1) Die gleiche Blockstrukturierung haben auch die Gleichungssysteme, die in Nicht-BlattKnoten des Substrukturierungsbaums jeweils aus den Gleichungssystemen der Sohnknoten aufgebaut werden. Es wird also immer unterschieden zwischen Punkten auf dem Rand (Indexmenge ), deren Freiheitsgrade, Matrixeintr¨age und Eintr¨age im Lastvektor an den Vaterknoten weitergegeben werden, und den Punkten auf den Separatoren im Gebietsinneren (Indexmenge ). 106 Die rekursive Substrukturierung in der numerischen Simulation 6.2.1 Assemblierung der Gleichungssysteme aus den Sohnknoten Für jeden Knoten (parent) des Substrukturierungsbaums ist die Systemmatrix und der Lastvekor zu assemblieren. Die Sohnknoten (child, ) tragen dazu jeweils ihre (bereits erstellte) Systemmatrix und ihren Lastvektor bei. Zun¨achst müssen die Gleichungssysteme der Söhne in einem einzigen Gleichungssystem für den Vater untergebracht werden. Hierzu ist die Akkumulation der Eintr¨age der Systemmatrizen und der , durch geeignete Assem Lastvektoren der Söhne blierungsmatrizen nötig. 1 1 Die Anzahl der Spalten einer Assemblierungsmatrix stimmt mit der Vektordimensi on des Sohnknotens überein, die Zeilenanzahl mit der Vektordimension des Vaterknotens. Jede Zeile für einen Punkt aus des Sohns hat genau eine 1: falls der Punkt im Vater auf Index und im Sohn auf liegt sonst Zeilen sind, kann diese Matrix Nachdem die Nicht-Null-Zeilen genau die ersten wesentlich speichereffizienter in einem Feld der L¨ ange gespeichert werden mit , falls . Werden Werte im Substrukturierungsbaum nach unten transportiert, also vom Vater zu ei nem Sohn , geschieht dies einfach mit der Transponierten der Assemblierungs bzw. das matrix. Praktisch kann die Assemblierungsmatrix Feld durch gleich zeitigen Durchlauf entsprechender Linienb¨aume von Vater- und Sohnknoten aufgestellt werden. 6.2.2 Hierarchisierung des Gleichungssystems Eine allgemeine Möglichkeit zur substantiellen Verbesserung des Konvergenzverhaltens iterativer Löser für Finite-Element-Probleme ist durch den Wechsel von den standardm¨aßig verwendeten Knotenpunktbasen auf hierarchische Basen gegeben [Gri94, JL91, Yse86]. Im Folgenden führen wir die Technik der Hierarchisierung von ein- und zweidimensionalen Funktionen kurz ein (vgl. [Bun92, Gri87, Hüt96, Zen90]) und zeigen, wie sie für unsere Beispielprobleme in Kapitel 7 auf die Knotengleichungssysteme angewandt werden. 6.2 Aufbau der Gleichungssysteme 107 1 1 0 3 2 3 1 3 2 3 0 =l !! " "#"#"# $ # $$%$% 0 0 x & (links) und hierarchische Basis ' Abbildung 6.5 Knotenpunktbasis x (rechts) auf eindi- mensionalem Gebiet *) Gegeben sei ein quadratisches Grundgebiet ( sowie ein diskretes Gitter ( (des alt. Levels ) auf ( , das in jeder Kooridinatenrichtung + ¨aquidistante Punkte enth¨ ) Zur n¨aherungsweisen Darstellung einer Funktion ( mittels eines bzgl. ( , multilinearen Interpolanten verwendet man üblicherweise die Knotenpunktbasis (siehe für den eindimensionalen Fall Abb. 6.5, links). Sie besteht aus den + bzgl. + , die jeweils an genau einem ( stückweise multilinearen Funktionen - , Stützpunkt den Wert 1 und an allen anderen Stützpunkten den Wert 0 annehmen. Entschei aufgespannte Raum auch mit Hilfe einer hierarchischen dend ist nun, dass der von , Basis . erzeugt werden kann (Abb. 6.5, rechts). Diese ist wie folgt rekursiv definiert: . . 0/ , 1/ 2 / . / 3 , Dabei ist die Menge der Basisfunktionen, die auf den zu Gitterpunkten verschwinden. , gehörenden / Zweidimensionale Basisfunktionen, wie sie für die in dieser Arbeit beschriebenen An4) wendungen auf ( benötigt werden, ergeben sich durch Bildung des Tensorprodukts aus jeweils einer eindimensionalen Basisfunktion in jeder Koordinatenrichtung [Gri87, Bun92]. In Abb. 6.6 sind Beispiele solcher Basisfunktionen dargestellt. 4 / Raum kann nun als Linearkombination Eine Funktion aus dem von , aufgespannten / der Knotenbasisfunktionen - /65 ( , , /65 - ( 7) oder gleichwertig als Linearkombination von hierarchischen Basisfunktionen /65 ( /65 8 8- ( 8 7) 8- . / , 108 Die rekursive Substrukturierung in der numerischen Simulation 1 1 0.8 0.8 0.6 0.6 0.4 0.4 0.2 0.2 1 1 0.8 00 0.8 00 0.6 0.2 0.4 0.4 0.6 0.6 0.2 0.2 0.8 0.4 0.4 0.6 1 0 1 1 0.8 0.8 0.6 0.6 0.4 0.8 0.2 1 0 0.4 0.2 0.2 1 1 0.8 00 0.8 00 0.6 0.2 0.4 0.4 0.6 0.2 0.8 1 0 0.6 0.2 0.4 0.4 0.6 0.8 0.2 1 0 / & mit Spitze über den Ecken des Abbildung 6.6 Statt der 4 Funktionen der Knotenpunktbasis / Funktionen aus / mit dem gesamten Gebiets (links oben) enth¨alt die hierarchische Basis ' / Gebiet als Tr¨ ager, von denen eine rechts oben dargestellt ist. Unten links und rechts: Basisfunk/ & / Knotenpunktbasis tionen aus ; w¨ahrend diese Funktionen bei Verwendung der nur in 3 vorkommen, sind sie bei hierarchischer Basis in allen R¨aumen ' mit enthalten. eindeutig dargestellt werden. Die 8 werden auch als hierarchische Überschüsse bezeich net, weil sie nicht den Funktionswert an der Stelle beinhalten, sondern jeweils nur die Differenz des Funktionswertes zum Mittelwert der hierarchischen Nachbarpunkte. In iterativen Lösungsverfahren für die rekursive Substrukturierung sind dann die hierarchischen Transformationen Hierarchisierung und Dehierarchisierung erforderlich, d. h. also die Transformationen von nach 8 und umgekehrt. Wie diese Transformation geschehen muss, leiten wir aus den Transformationen für die Basisfunktionen ab. ( Die Hierarchisierung erfolgt im Substrukturierungsbaum stufenweise. Die Gleichungssysteme in den Teilgebieten sind dabei schon hierarchisiert. Es ist also nur der letzte Schritt, die Bildung der Basisfunktionen mit Spitze an den Ecken des neuen Gebiets und Tr¨ager über das ganze Gebiet notwendig, siehe Abb. 6.6, rechts oben. Im eindimensionalen Fall ist diese neue Basisfunktion der hierarchischen Basis die Summe aus der an diesem Eckpunkt vorhandenen Funktion und der Basisfunktion im Mittelpunkt der Linie, siehe Abb. 6.7 links. Wegen des Tensorprodukt-Ansatzes für die zweidimensionalen Basisfunktion gehen für die neue hierarchische Basisfunktion in einem Eckpunkt die vorhandenen Funktionen mit den Gewichten / / / / 6.2 Aufbau der Gleichungssysteme 1/2 109 1/2 1/2 T1 P1 1/2 P2 1/4 P1 T1 P2 1/4 1/2 H T4 T5 T6 T7 1/2 T2 1/4 1/4 1/2 P1 T1 1/2 P2 P4 P3 T3 1/2 1/2 Abbildung 6.7 Hierarchisierungsschritt in einem Baumknoten. Links ist der Schnitt durch die Basisfunktionen an der Linie T1 dargestellt, vor und nach der Hierarchisierung H. Rechts sind die Faktoren für den kompletten Hierarchisierungsschritt für einen Baumknoten über diesem Gebiet eingetragen. Diese Faktoren sind gleichzeitig die Eintr¨age in der Hierarchisierungstabelle. ein, also die Funktionen mit Spitze über den Mittelpunkten der beiden anliegenden Kanten mit Faktor 1/2 und die Funktion über dem Mittelpunkt des Gebiets mit Faktor 1/4, siehe Abb. 6.7, rechts. Die Hierarchisierungsmatrix setzt sich dann aus den in Abb. 6.7 (rechts) angedeuteten Eintr¨agen zusammen, wobei die Bezeichnungen für Punkte und Linien aus Abschnitt 6.2.2 übernommen sind. Dazu werden Matrix, Lastvektor auf die HierarchischeBasis-Darstellung transformiert durch / / 1 1 Allgemeiner können wir bei der Hierarchisierung des Knotengleichungssystems auch von einer Vorkonditionierung sprechen [SBG96], weil sie die Konditionszahl der Systemmatrix und damit das Konvergenzverhalten von iterativen Lösern des Gleichungssystems verbessert. Diese Vorkonditionierung kann alle Punkte des Gebiets miteinbeziehen, insbesondere solche, für die Dirichlet-Randwerte vorgegeben werden. Deshalb wird die Vorkonditionierung vorgenommen, bevor diese Punkte in den folgenden Schritten aus dem Gleichungssystem entkoppelt werden. Der Hierarchisierungsschritt selbst kann bei balancierten B¨aumen und hierarchischer Nummerierung der Punkte allein durch Indexrechnung erfolgen, siehe dazu [BES97]. Eine allgemeinere und bew¨ahrte Technik für diese Transformation ist eine Hierarchisie rungstabelle, die eine explizite Speicherung von vermeidet und die sich aus der Anordnung der Punkte in den Linienb¨aumen und Ecken des Gebiets ergibt. Sie enth¨alt Tupel der Form . Jeder solche Eintrag gibt den Einfluss an, den ein hierarchischer Nachbar auf den Knoten mit dem Faktor hat. Bei der Hierarchisierung eines Vektors muss also für jeden Tabelleneintrag die Operation " " " ausgeführt werden (siehe Abschnitt 6.3.1). 110 Die rekursive Substrukturierung in der numerischen Simulation Wir erhalten somit folgende Hierarchisierungstabelle: node(T1) P1 1/2 node(T4) P4 1/2 node(T1) P2 1/2 node(T4) P1 1/2 node(T2) P2 1/2 node(T5) P1 1/4 node(T2) P3 1/2 node(T5) P2 1/4 node(T3) P3 1/2 node(T5) P3 1/4 node(T3) P4 1/2 node(T5) P4 1/4 Die Verwendung von Hierarchisierungstabellen hat neben dem offensichtlich massiv ein gesparten Speicherplatz für die Hierarchisierungsmatrix den Vorteil, dass sie programmtechnisch generisch ist: Mit einer einfachen Iteration über die Tabelle kann die Hierarchisierung für beliebige Elemente (also auch z.B. bei Triangulierungen) vollzogen werden, wenn für den entsprechenden Elementtyp eine Hierarchisierungstabelle vorliegt. Hierarchisierungstabellen sind auch vorteilhaft, um bei den Baumdurchl¨aufen nicht zwischen zwei- und viergeteilten Knoten unterscheiden zu müssen. Denn hierarchisiert wird bei Zweiteilung nur in jeder zweiten Ebene des Substrukturierungsbaums, also erst wenn vier Teilgebiete zusammengesetzt sind. In den übrigen Ebenen wird eine leere Tabelle für den Hierarchisierungsschritt verwendet. 6.2.3 H¨ angende Punkte und Dirichlet-Randwerte Beim Zusammensetzen zweier Teilgebiete kann der Fall eintreten, dass manche Punkte auf dem Separator nur in einem (dem st¨arker verfeinerten) Teilgebiet vorkommen (siehe Abb. 6.8). Ein solcher Punkt heißt h¨angend und ist aus dem Gleichungssystem zu entkoppeln. Dazu benötigen wir Matrizen, die Reihenoperationen auf der Matrix beschreiben: Definition 6.1 Die Elemente der Matrix falls falls sonst und sind festgelegt als Bei der Darstellung in Knotenpunktbasis muss für jeden inkompatiblen Punkt der Wert aus den Werten und seiner Nachbarpunkte interpoliert werden: / Dies ist n¨amlich auch der Wert, der in dem weniger verfeinerten Gebiet für implizit (wegen der multilinearen Basisfunktionen) angenommen wird. Insgesamt muss die 6.2 Aufbau der Gleichungssysteme 111 PSfrag replacements / Abbildung 6.8 H¨angende Punkte auf dem Separator des Knotens sind mit “ ” markiert. Die Freiheitsgrade der h¨angenden Punkte müssen aus dem Gleichungssystem entkoppelt und bei der Berechnung der Lösung interpoliert werden. Lösung in jedem Punkt stetig sein. Diese Interpolation bedeutet für das Gleichungssys tem, dass sich der Einfluss des Punktes zu gleichen Teilen auf und verteilt: / / / / Bei Verwendung der hierarchischen multilinearen Basisfunktionen ist diese Transforma tion nicht notwendig, , da der interpolierte Wert — der hierarchische Überschuss . Lediglich — Null sein muss und entsprechend für jeden h¨angenden Punkt gilt: die betreffenden Zeilen und Spalten sind aus der Systemmatrix zu streichen. Dies wird schon durch die Projektion auf die Indizes der internen und externen Punkte im folgenden Transformationsschritt für die Dirichlet-Randwerte geleistet. Wenn für einen Freiheitsgrad die Dirichlet-Randwertbedingung vorgeschrieben ist, wird folgende Transformation notwendig: Die -te Spalte der Matrix wird mit diesem Wert multipliziert und vom Lastvektor abgezogen: Alle Zeilen und Spalten, die den festen Randwerten zugeordnet sind, werden aus der Matrix gestrichen. Ebenso auch die Zeilen und Spalten der Freiheitsgrade der h¨angenden Punkte, deren Einfluss durch die Transformation auf die Nachbarpunkte übertragen worden ist. Übrig bleibt also das Gleichungssystem für die Indexmenge . 112 Die rekursive Substrukturierung in der numerischen Simulation 6.2.4 Algebraische Vorkonditionierung Hier werden nun Transformationen des Gleichungssystems vorgenommen, die nur noch die nicht-entkoppelten Punkte betreffen und der Lösung des Gesamtsystems dienen. Hierzu gehört die vollst¨andig direkte Lösung mittels statischer Kondensation (Abschnitt 6.3), aber auch Verbesserungen der Kondition der Systemmatrix durch partielle Elimination der st¨arksten Kopplungen (Abschnitt 6.4.1) oder durch Vorkonditionierungen, die nicht von der geometrischen Lage der Punkte Gebrauch machen, z. B. ILU-Vorkonditionierungen [Hac93]. Wir beschreiben dies allgemein durch Transformationsmatrizen und , also Diese Verfahren sind löserspezifisch und werden deshalb weiter unten bei dem entsprechenden Lösungsverfahren besprochen. In effizienten Vorkonditionierungsverfahren ist die Transformationsoperation symmetrisch, , und erh¨alt damit die Symmetrie des Knotengleichungssystems. 6.2.5 Auswirkung auf die L¨ osungsiterationen Die bisher beschriebenen Transformationen des Gleichungssystem haben nicht nur Auswirkungen auf den Gleichungssystemaufbau (den Build-Schritt), sondern auch auf die Werte, die w¨ahrend des iterativen Lösungsvorgangs (in Top-down- und Bottom-upSchritten) zwischen Vater- und Sohnknoten ausgetauscht werden. Bei den verschiede nen Lösungsvarianten werden typischerweise -Werte vom Vater zu den Söhnen gereicht, w¨ahrendÄnderungen des Lastvektors und Residuenwerte von den Söhnen zum Vater übertragen werden. Die notwendigen Transformationen sehen wir in Abb. 6.9. Das obere Gleichungssystem (V) haben wir aus den Gleichungssystemen (I) und (II) der Söhne erhalten. Werden Werte von für die externen Freiheitsgrade der Söhne vom Vater nach unten gereicht, ist Transformation (T1) nötig.Änderungen, die den Lastvektor betreffen und nach oben wandern, werden von Transformation (T2) behandelt. 6.3 Bisherige Lösungsvarianten Die gesuchte Lösung ergibt sich durch Kombination der Gleichungssysteme aller Baumknoten. Der Ansatz für einen direkten Löser eliminiert die Freiheitsgrade schrittweise von der untersten Baumebene zur Wurzel hinauf. Die ursprüngliche Idee der Nested Dissection [GL78] lieferte eine Nummerierung aller Freiheitsgrade gem¨aß der Hierarchie der Separatoren, um den Fill-in des Gesamtgleichungssystems gegenüber geometrischer Nummerierung zu vermindern. Diese hierarchische Nummerierung wird in [Sch95, Hüt96] explizit zum Aufbau eines Baums von Gleichungssystemen, des Substrukturierungsbaums, verwendet. 6.3 Bisherige Lösungsvarianten 113 2 7 ) 4 3 53 63 PSfrag replacements !#"%$&'%#"!( ( , * . + 0 / , * . + 1 1 %8":9;!<"(=*>+7 (V) ? %<"@!#" (BADCE(=*>+7- F 9 %#"G (T1) ? %#" ( H $ 'ML" 9 ML!" (I) K J ? %8" @ !#" ( @ I7" G 1 9 I." ? @ ML" $ '!N" 9 '!N" ? @ '!N" (II) (T2) Abbildung 6.9 Top-down-Transformation (T1) von Lösungswerten und Bottom-upTransformation (T2) von Werten des Lastvektors zum Austausch zwischen VaterGleichungssystem (V) und Sohn-Gleichungssystemen (I) bzw. (II) Um trotz Wegfalls der Punkte aus dem Gebietsinneren der Söhne ein korrektes Gleichungssystem im Vaterknoten zu erhalten, müssen die Söhne vor der Assemblierung den Einfluss ihres Gebietsinneren auf ihren Rand übertragen. Dazu addiert man zum oberen Block von Gleichung (6.1) den zweiten Zeilenblock, multipliziert mit : OBQSP R Q (6.2) Da die durch Finite-Element-Diskretisierung entstehenden Matrizen symmetrisch und positiv definit sind, kann für die implizite Berechnung von das Cholesky-Verfahren [Stö89] verwendet und durch Vorw¨arts- und Rückw¨artssubstitutionen ermittelt werden. Dies ist die Hauptarbeit, die bei einem direkten Löser anf¨allt. Der Vorteil diese Verfahrens ist in seiner Robustheit und der genauen Absch¨atzbarkeit seines Aufwands zu sehen, weil zur Lösung nur ein einziger Top-down-Durchlauf durch den Baum zur Rücksubstitution erforderlich ist. Ein Problem dieses Verfahrens liegt aber in seiner nicht-optimalen Laufzeit von T bei Gitterpunkten, die die Berechnung wirklich großer, durch feine Gitter entstandener Gleichungssysteme verhindert. Deshalb wird im Folgenden auch das für große Probleme und für die Parallelisierung wesentlich besser geeignete iterative Verfahren von [Hüt96] kurz angegeben. 114 Die rekursive Substrukturierung in der numerischen Simulation (B1) (B2) (D2) 8 , 8 , (D5) (U0) (U1) (U2) (U3) (U4) " 8 Hierarchisierung von Matrix und Lastvektor Aufbringung der Dirichlet-Randwerte GS-HB, Top-down-Schritt (D3) (D4) 8 GS-HB, Build-Schritt Zusammenfassung von Eintr¨agen in Ma trix und Lastvektor aus den Söhnen , 8 (B3) (D1) [Falls nicht Wurzel:] Neue Lösungswerte vom Vaterknoten Beschr¨ankung des Relaxations-Aufwands von (D4) auf die internen Freiheitsgrade , weil die externen vom Vater schon festgelegt sind. Gauß-Seidel-Gl¨attungsschritte Speichern der Änderung von der internen Punkte für den Bottom-up-Schritt Dehierarchisierung 8 Bottom-up-Schritt GS-HB, [Falls Blatt.] [Falls nicht Blatt:] Zusammenfassung der Änderungen aus Söhnen für den Lastvek tor 8 Hierarchisierung 8 Anpassung des Lastvektors 8 Änderungen für den Lastvektor des Vater knotens Abbildung 6.10 Berechnungsschritte beim Gauß-Seidel-Verfahren auf hierarchischer Basis 6.3.1 Ein Gauß-Seidel-L¨ oser auf hierarchischer Basis Grunds¨atzlich ist bei großen Problemen den iterativen Lösern der Vorzug vor direkten Lösern zu geben. Denn direkte Löser verursachen einen Fill-in in den ursprünglich dünnbesiedelten Matrizen und verteuern damit gerade den Wurzelknoten. Dies begrenzt sowohl die Parallelit¨at — ohne Berücksichtigung der Kommunikationskosten ist ein Speedup von höchstens 6 möglich [Hüt96, S. 129] — als auch die maximale Größe der Probleme bei vorgegebenem Speicher. Die wesentliche Rechenzeitersparnis bei einem iterativen Löser wird dadurch erreicht, dass man auf Eliminationsschritte beim Gleichungssystem- 6.3 Bisherige Lösungsvarianten 115 aufbau g¨anzlich verzichtet, also Damit der dann verwendete iterative Löser schneller die gewünschte Lösungsgenauigkeit liefert, ist es wichtig, dass die Kondition der Systemmatrizen durch Transformation auf hierarchische Basis verbessert wird. Als Nebeneffekt der Hierarchisierung ist auch die Entkopplung h¨angender Punkte bei Verwendung hierarchischer multilinearer Basisfunk wie in Abschnitt 6.2.3 dargestellt. tionen sehr günstig, n¨amlich Nach dem Zusammenbau der Gleichungssysteme sind mehrere Baumdurchl¨aufe notwen dig, bis die gewünschte Genauigkeit erreicht ist. In einem Baumdurchlauf wird, ausge hend von der Wurzel des Substrukturierungsbaums und einer N¨aherung für die Lösung, eine neue Iterierte berechnet. Dabei sind die externen Freiheitsgrade schon festgelegt, weil auf Grund der Stetigkeitsforderung der Lösungswert eines Punktes in jedem Baumknoten, der ihn enth¨alt, gleich sein muss. Die N¨aherung für die Werte der internen, nicht durch Dirichlet-Randbedingungen festgelegten Freiheitsgrade werden durch Gauß-Seidel-Iterationsschritte verbessert [Stö89]. auf einem Gleichungssystem Definition 6.2 Ein Gauß-Seidel-Schritt mit -Matrix zur Bestimmung einer neuen Iterierten ist die Abbildung , wobei für alle gilt: % Nachdem die Werte der neuen Iterierten bestimmt sind, müssen derenÄnderungen für die internen Freiheitsgrade auf den Lastvektor des jeweiligen Vaters übertragen werden. Diese Anpassung ist notwendig, weil die internen Freiheitsgrade im Gleichungssystem des Vaters nicht vorkommen. Die einzelnen Schritte des resultierenden Lösungsverfahrens, inklusive Gleichungssystem-Aufbau im Build-Schritt und der Transformationen von Größen für den Vater- bzw. Sohnknoten sind in Abb. 6.10 zusammengefasst. Es sind jeweils die nötigen Operationen " Baumknoten w¨ahrend des angegebenen Schritts aufgelistet. Ein hochgestellter in einem Index bedeutet eine vom Vaterknoten übernommene Größe, mit sind Werte aus den Söhnen des aktuell bearbeiteten Knotens markiert. Dies werden wir auch in den Lösungsverfahren beibehalten, die wir in den n¨achsten Abschnitten darstellen. / In unserem Anwendungsbereich verbessert sich durch die hierarchische Transforma T tion die Konditionszahl der Systemmatrizen von auf T bei Unbekannten [Yse86, JL91, SBG96]. Damit verringert sich die Anzahl der nötigen Iterationsschritte bis zum Erreichen der gleichen Lösungsgenauigkeit. Das iterative Lösungsverfahren GS-HB hat die Zeitkomplexit¨at von nur mehr [Hüt96], im Vergleich zu T des direkten Verfahrens mit statischer KonT densation. / / 116 Die rekursive Substrukturierung in der numerischen Simulation 6.4 Neue Lösungsvarianten Wie im vorigen Abschnitt dargestellt, ist eine Vorkonditionierung der Systemmatrizen — wie die Transformation auf eine hierarchische Basis — notwendig, damit iterative Lösungsverfahren auch in der Praxis schneller sind als direkte Lösungsverfahren. Kombinieren wir diese Vorkonditionierung mit der Elimination weniger, aber betragsm¨aßig großer Eintr¨age in der Systemmatrix, so erhalten wir ein nochmals bezüglich Laufzeit leicht verbessertes Verfahren, das wir in Abschnitt 6.4.1 vorstellen. Es handelt sich dabei um ein basisabh¨angiges Eliminationsverfahren, bei dem nur die st¨arksten Kopplungen vorab eliminiert werden. Nachdem bisher lediglich ein Gauß-Seidel-Löser bei der rekursiven Substrukturierung verwendet wurde, liegt es nahe, die Idee der Transformation auf hierarchische Basis auch mit anderen Standard-Lösungsmethoden für lineare Gleichungssysteme zu kombinieren und auf die rekursive Substrukturierung anzuwenden. Wir pr¨asentieren in Abschnitt 6.4.2 eine Anpassung der Jacobi-Relaxation an die rekursive Substrukturierung, die gegenüber dem Verfahren GS-HB weniger Speicher benötigt und sich deshalb für sehr große Gleichungssysteme eignet. Ähnliche Speichervorteile, aber eine wesentlich schnellere Lösungsmethode als die Jacobi-Variante liefert eine Kombination mit dem Verfahren der konjugierten Gradienten in Abschnitt 6.4.3, falls die Systemmatrizen definit sind. 6.4.1 Ein Gauß-Seidel-L¨ oser mit teilweiser Elimination von starken Kopplungen Der Grund dafür, dass bei Verzicht auf die Elimination der Kopplungen zwischen internen und externen Freiheitsgraden mehrere Durchl¨aufe durch den Substrukturierungsbaum nötig sind, ist die Assemblierung von nicht-vollst¨andigen Gleichungssystemen, denn der Einfluss der internen Freiheitsgrade wird im n¨achsthöheren Knoten weggelassen. Diese Situation können wir verbessern und zum Erreichen der gleiche Lösungsgenauigkeit Iterationen sparen, wenn wenigstens die st¨arksten Einflüsse gleich bei der Assemblierung der Gleichungssysteme berücksichtigt werden. Dies kann dadurch erreicht werden, dass diese st¨arksten Kopplungen aus den Gleichungen der externen Freiheitsgrade eliminiert werden. Betrachten wir Abb. 6.6 auf Seite 108, so stellen wir fest, dass für das Gebiet eines Kno tens sowohl die . -Basisfunktionen mit Spitze über den Eckpunkten des Gebiets als auch die Basisfunktion mit Spitze auf dem Mittelpunkt des Gebiets Tr¨ager besitzen, die sich über dieses gesamte Gebiet erstrecken. Deswegen f¨allt die Kopplung dieser Punkte in der Matrix besonders stark aus. Ein weiterer Grund für die betragsm¨aßig großen Matrixeintr¨age ist, dass die zugehörigen Basisfunktionen zu den Teilr¨aumen der hierar/ chischen Basisfunktionen , gehören und damit ganz oben in der Hierarchie / / 6.4 Neue Lösungsvarianten 117 1 4 0 0 0 0 1 4 s s 0 0 0 0 PSfrag replacements Abbildung 6.11 Links: Auf dem Gebiet sind die beim Verfahren GS-HB-E zu eliminerenden Kopplungen zwischen den betreffenden Punkten mit dicken Pfeilen eingezeichnet. Rechts: In der Matrix sind die zu eliminierende Eintr¨age mit 0 bezeichnet. Die Elimination wird durch geeignete Subtraktion der Zeile und Spalte für den Separator von den übrigen dunkel hinterlegten Zeilen und Spalten bewerkstelligt. der Basisfunktionen angesiedelt sind. Deshalb ist es sinnvoll, diese starken Kopplungen zu eliminieren. In Abb. 6.11 sind die entsprechenden Matrixeintr¨age durch 0 markiert. Algebraisch beschreiben wir die partielle Elimination der vier st¨arksten Kopplungen mit Hilfe von Reihenoperationen der Matrix (vgl. S. 110). Um den Eintrag zu eliminie ren, brauchen wir die Operation Mit dem Index für den Gebietsmittelpunkt ergibt sich dann die Eliminationsmatrix zu Wir und wegen der Symmetrie des ursprünglichen Gleichungssystems gilt: fügen diesen Eliminationsschritt nach der Hierarchisierung des Gleichungssystems in das Verfahren GS-HB aus Abschnitt 6.3.1 ein und transformieren in der Lösungsphase beim Top-down-Schritt mit und im Bottom-up-Schritt mit in jedem Baumknoten. So erhalten wir das Verfahren GS-HB-E, das nochmals in Abb. 6.12 zusammengefasst ist. Das Verfahren GS-HB-E zeichnet sich zwar nicht durch eine Verbesserung der Zeitkomplexit¨at gegenüber dem Verfahren GS-HB aus, eignet sich aber in der Praxis noch besser für die verteilte Bearbeitung. Denn w¨ahrend die Anzahl der Rechenoperationen durch die zus¨atzlichen Eliminationsschritte leicht zunimmt, bleiben die Kosten der Kommunikation in jeder Iteration gleich. Und insgesamt werden Iterationen eingespart, wie sich bei den Tests in Kapitel 7 best¨atigt. Der durch die Elimination entstehende Fill-in des Gleichungssystems und der damit verbundene Speicherbedarf f¨allt dagegen kaum ins Gewicht, wie die experimentellen Versuche ebenfalls zeigen werden. 118 Die rekursive Substrukturierung in der numerischen Simulation GS-HB-E, Build-Schritt (B1) bis (B3) wie Gauß-Seidel (BE) , partielle Elimination starker Kopplungen wie Verfahren GS-HBGS-HB-E, Top-down-Schritt Transformation wegen partieller Elimina- (D1) (DE) tion (D2) bis (D5) wie Verfahren GS-HB GS-HB-E, Bottom-up-Schritt (U0) bis (U2) wie Verfahren GS-HB 8 (UE) 8 Transformation wegen partieller Elimina tion (U3) bis (U4) wie Verfahren GS-HB Abbildung 6.12 Berechnungsschritte beim Gauß-Seidel-Verfahren mit partieller Elimination auf hierarchischer Basis 6.4.2 Ein Jacobi-L¨ oser auf hierarchischer Basis Wenn das Gitter für eine Problemlösung besonders fein und damit der Substrukturierungsbaum sehr tief sind, und wenn es nicht so sehr auf die Rechenzeit ankommt, ist eine Lösungsvariante auf Basis des Jacobi-Verfahrens nützlich. Sie zeichnet sich dadurch aus, dass in jedem Baumknoten außer einer 4 4-Matrix nur die Diagonalelemente der Systemmatrix gehalten werden müssen. Dies beruht auf der Eigenschaft der JacobiRelaxation, dass für alle Multiplikationen mit der Systemmatrix nur Werte aus dem vorigen Iterationsschritt verwendet werden. auf einem Gleichungssystem ist Definition 6.3 Ein Jacobi-Schritt , wobei für alle und einen Relaxadie Abbildung tionsfaktor mit gilt: % Für die Bestimmung eines Wertes in der neuen Iterierten werden nur Werte der vorigen Iterierten verwendet. Damit können in den Bl¨attern schon die Beitr¨age gebildet werden. Diese Beitr¨age werden zum Vaterknoten gereicht. Werden sie im Bottom-up-Schritt aufgesammelt, so bleibt im Top-down-Schritt nur noch für 6.4 Neue Lösungsvarianten 119 1-4 5-8 s 1 4 5 8 s PSfrag replacements Abbildung 6.13 Für das Verfahren J-HB sind die benötigte Matrix-Eintr¨age alle grau unterlegt. Davon können die hellgrauen Eintr¨age nach der Hierarchisierung der Matrix gelöscht werden. zu berechnen. Akzeptable Konvergenz erzielen wir jedoch nur, wenn wir wieder unter Verwendung hierarchischer Basen rechnen. Deshalb genügt es nicht, ausschließlich die Diagonalelemente zu den Vaterknoten zu reichen, sondern es sind alle Eintr¨age notwendig, die an den n¨achsthöheren Hierarchisierungsschritten beteiligt sind. In dem rechten Schema von Abb. 6.7 auf Seite 109 sehen wir, dass dies die Eintr¨age für die Eckpunkte sowie für die Mittelpunkte der Randlinien und des Separators sind. Aus den vier Sohnknoten werden dazu jeweils nur die vier Eckpunkte benötigt. Beim Aufbau können also nach der Trans formation von auf 8 alle Nicht-Diagonal-Elemente 8 mit und oder gelöscht werden (Abb. 6.13). Der Lastvektor wird nur in den Bl¨attern gebraucht, muss also nicht nach oben weitergereicht und transformiert werden. Dagegen müssen wir im Bottom-up-Schritt die Residu enanteile mit und im Top-down-Schritt die Lösungsvektoren mit transformieren, genauso wie beim Verfahren GS-HB. Zur Übersicht fasst Abb. 6.14 die in einem Baumknoten anfallenden Operationen bei dem eben beschriebenen Jacobi-Löser zusammen, den wir Verfahren J-HB nennen. Auf einem Gleichungssystem hat das Jacobi-Verfahren die gleiche Zeitkomplexit¨at wie das Gauß-Seidel-Verfahren [Stö89]. Mit dem Beweis aus [Hüt96, S.113] für das Verfahren GS-HB folgt somit für das Verfahren J-HB ebenfalls die gleiche Zeitkomplexit¨at von T . J-HB benötigt aber in der Praxis (um einen konstanten Faktor) mehr Iterationen. Zweck des Verfahrens J-HB ist die Einsparung von Speicherplatz für die Systemmatrizen in Nicht-Blatt-Knoten, so dass bei gleicher Anzahl verfügbarer Rechner mit feineren Gittern und dadurch mit größerer Genauigkeit der Diskretisierung gerechnet werden kann. / 120 Die rekursive Substrukturierung in der numerischen Simulation (B0) (B1) (B2) (B3) (D1) (D2) (D3) (U0) (U1) (U2) 8 J-HB, Build-Schritt Initialisierung Zusammenfassung von Eintr¨agen der Ma trix aus den Söhnen Hierarchisierung der Matrix Ausdünnung bis auf 4 4-Matrix und Diagonale 8 " % J-HB, Top-down-Schritt [falls nicht Wurzel:] Neue Lösungswerte vom Vaterknoten neue Iterierte aus Residuum und Diago für nalelementen Dehierarchisierung J-HB, Bottom-up-Schritt [falls Blatt:] neue Residuenanteile 8 [falls nicht Blatt:] Zusammenfassung der Residuenanteile aus den Söhnen für den Lastvektor 8 Hierarchisierung Abbildung 6.14 Berechnungsschritte beim Jacobi-Verfahren auf hierarchischer Basis 6.4.3 Ein CG-L¨ oser auf hierarchischer Basis Wenn die bei der elementaren Diskretisierung in den Bl¨attern entstehenden linearen Gleichungssysteme symmetrisch und positiv definit sind — was bei vielen Finite-ElementDiskretisierungen für lineare elliptische partielle Differentialgleichungen der Fall ist —, können wir die Vorteile des Verfahrens GS-HB (Konvergenzgeschwindigkeit) und des Verfahrens J-HB (Speichereinsparung) für die rekursive Substrukturierung kombinieren. Wir verwenden hierfür das Verfahren der konjugierten Gradienten (CG-Verfahren [BBC 94]) zur Lösung der Knotengleichungssysteme. Der sequentielle Grundalgorith mus des CG-Verfahrens mit einer Anfangssch¨atzung des Lösungsvektors ist in Abb. 6.15 angegeben. Bei der rekursiven Substrukturierung können wir allerdings nicht alle Eintr¨age der Vek toren ! und in einem Speicherblock halten, sondern müssen die Werte über die Knoten des Baums verteilen, entsprechend den jeweils vorhandenen Punkten auf dem Rand und dem Separator eines Knotens. Die Skalarprodukte in den Schritten (4) und (8) werden deshalb stufenweise über die internen Punkte aller Baumknoten gebildet. Damit ist dann in Schritt (5) der Wert (bzw. in Schritt (1) der Wert ) verfügbar. In der Wurzel des Baums sind schließlich alle Beitr¨age zu (bzw. zu ) aufgesammelt und (bzw. ) kann berechnet werden. Der Wert (bzw. ) wird im anschließenden 6.4 Neue Lösungsvarianten 121 für (1) + und / solange ! ! (2) ! (3) ! (4) (5) (6) ! (7) (8) Abbildung 6.15 Der sequentielle Grundalgorithmus des CG-Verfahrens Schritt (5) (bzw. (2)) in jedem Baumknoten für die weiteren Berechnungen benötigt, d. h. der Wert muss von oben nach unten an alle Baumknoten verteilt werden. Somit ergeben sich für eine Iteration des CG-Verfahrens zwei Zyklen: Der erste Zyklus umfasst Schritt (1) bis (4), der zweite Zyklus Schritt (5) bis Schritt (8). Die Bildung der Beitr¨age ! kann dagegen vollst¨andig in den Bl¨attern des Baums geschehen; eine Verteilung der Matrix in höhere Baumknoten ist also nicht erforderlich. Auch das CG-Verfahren muss zum Erreichen akzeptabler Konvergenz vorkonditioniert werden [BBC 94, Hac93], bei uns wieder durch Transformation auf hierarchische Basisdarstellung. Wir verwenden die gleichen Basisfunktionen in jedem Baumknoten wie bei den vorigen Verfahren. Deshalb sind Werte, die zu den Sohnknoten gereicht werden, zu dehierarchisieren, und Werte, die von den Sohnknoten übernommen werden, zu hierarchisieren. Die Hierarchsierung ist in jedem Baumknoten nach Schritt (2) zum Herunterreichen des Vektoranteils für die neue Suchrichtung ! und nach Schritt (3) zum Heraufreichen des Residuumanteils notwendig. Im Detail gibt Abb. 6.16 Auskunft über den Ablauf der Berechnung in jedem Baumknoten bei diesem neuen Verfahren CG-HB. Bei der Laufzeit erreichen wir eine asymptotische Verbesserung gegenüber dem ursprünglichen Verfahren GS-HB. Da wir für die Ermittelung der Suchrichtung und für die Bil dung der Faktoren und immer hierarchisierte Vektoren verwenden, ist die Anzahl der Iterationen in der Größenordnung T , im Vergleich zu T der Verfahren GS-HB und J-HB [Yse86]. Wie schon in [Hüt96] beobachtet wurde, steigt in der Praxis jedoch die Anzahl der Iterationen beim Verfahren GS-HB langsamer als in der theoretischen Absch¨atzung. Bei Verfahren CG-HB steigt, wie wir bei den Beispielrechnung in Kapitel 7 sehen werden, die Zahl der Iterationen mit dem erwarteten logarithmischen Faktor. / Bezüglich des Speicherbedarfs hat ist Verfahren CG-HB im Vorteil: In jedem Knoten des Baums müssen zwar vier Vektoren gehalten werden ( ! und ). Dies ist aber weniger als der Speicher, den die Systemmatrizen im Verfahren GS-HB benötigen, da dort jede Matrixzeile außer dem Diagonalelement noch mindestens zwei weitere Eintr¨age hat. 122 Die rekursive Substrukturierung in der numerischen Simulation (B0) (B1) 8 (B2) (B3) CG-HB, Build-Schritt !8 (D1.2) !8 (D2.0) (D2.1) 8 (U1.0) (U1.1) CG-HB, 1.Zyklus, Top-down-Schritt / [falls Wurzel:] Bestimmung von Faktor " " [falls nicht Wurzel:] Neuer Anteil an ! ; Suchrichtung und vom Vaterknoten 8 neue Iterierte aus Residuum und Dia! gonalelementen für interne Freiheits grade 8 ! Dehierarchisierung CG-HB, 1.Zyklus, Bottom-up-Schritt [falls Blatt:] neue Residuenanteile ! 8 [falls nicht Blatt:] Zusammenfassung der Residuenanteile aus den Söhnen für den Lastvektor Hierarchisierung Bestimmung des Anteils für Faktor !8 8 CG-HB, 2.Zyklus, Top-down-Schritt [falls Wurzel:] Bestimmung des Fak tors " [falls nicht Wurzel:] vom Vaterkno ten neue Iterierte für Lösungswerte neue Iterierte für ! (D1.3) [falls Blatt:] Bestimmung des StartResiduums [falls nicht Blatt:] Zusammenfassung der Residuenanteile der Söhne Summand für Hierarchisierung (D1.1) (U2.0) (U2.1) (D1.0) (D2.2) (D2.3) (U1.2) (U1.3) CG-HB, 2.Zyklus, Bottom-up-Schritt [falls Blatt:] Anteil an Faktor [falls nicht Blatt:] Bestimmung des An teils für Faktor Abbildung 6.16 Berechnungsschritte beim CG-Verfahren auf hierarchischer Basis 6.5 Das Koordinationsprogramm in FASAN 123 Wir haben nun drei neue Lösungsvarianten für das Verfahren der rekursiven Substrukturierung vorgestellt, die sich alle in das Ablaufschema von Abb. 6.2 einfügen lassen. 6.5 Das Koordinationsprogramm in FASAN Wünschenswert ist jetzt ein Rahmenprogramm für die rekursive Substrukturierung, in dem diese vier Löser einfach integriert werden können. Das Ablaufdiagramm von Abb. 6.2 auf Seite 102 liefert einen groben Anhaltspunkt für die Koordinationsschicht des zu erstellenden FASAN-Programms ARESO (Arbeiten mit rekursiv substrukturierten Objekten) function Areso (T:Tree; d:int[]) -> T’:Tree; { // Baum erweitern oder aus Startknoten aufbauen T_new, W_new = ExtendTree (T, d); // neuen und alten Baum vergleichen und ggf. abbrechen if stopAreso (selectNode(T_new), selectNode(T)) then T’ = T; else // Verteilung berechnen d_ = balance (W_new, numHosts); // Gleichungssysteme aufbauen T_ = Build (nonode(), T_new, d_); // Löser-Iterationen T_sol = IterSolve (T_, d_); // neue äußere Schleife T’ = recycle Areso (T_sol, d_); fi } Zur Lastverteilung, auf die wir erst im folgenden Abschnitt genauer eingehen, liefert die Funktion ExtendTree einen WeightTree, der die vorausgesagte Rechenlast für jeden Knoten enth¨alt. Mit dem Ergebnis der Funktion balance kann die Funktion Build die Gleichungssysteme bereits auf dem richtigen Zielrechner erstellen, damit keine unnötige Migration von Node-Objekten anf¨allt. Aber zun¨achst zur Verfeinerung des Substrukturierungsbaums durch Bisektion: type Tree = Leaf (leafNode:Node) | Branch (branchNode:Node; leftTree,rightTree:Tree) function selectNode (T:Tree) -> nd:Node { if isLeaf(T) then nd = T.leafNode; else nd = T.branchNode; fi } 124 Die rekursive Substrukturierung in der numerischen Simulation function ExtendTree (T:Tree; d:int[]) -> T’:Tree; W’:WeightTree { nd = selectNode(T); if stopExtend (nd) then // Abbruch der Verfeinerung T’ = T; W’ = weightLeaf(nd); else if isLeaf (T) then // neue Blätter anlegen T1 = Leaf (extendTreeDown (nd, 0)); T2 = Leaf (extendTreeDown (nd, 1)); else T1 = T.leftTree; // bestehende Unterbäume verwenden T2 = T.rightTree; fi T1’, W1’ = ExtendTree (T1, d) on dest_left (nd, myHost, numHosts, d); T2’, W2’ = ExtendTree (T2, d) on dest_right(nd, myHost, numHosts, d); // Punkte in neuem Knoten klassifizeren: nd’ = extendTreeUp (nd, selectNode(T1’), selectNode(T2’)); T’ = Branch (nd’,T1’,T2’); W’ = weightTree (nd’, W1’.getLTree, W2’.getLTree)); fi } Für eine effiziente Ausführung ist es wichtig, dass die Argument- und Ergebnisparameter von Funktionsaufrufen mit Lokationsattributen möglichst wenig Kommunikationsaufwand verursachen. Dazu bietet es sich z. B. an, die Variable für die Systemmatrix in der Java-Klasse Node als transient zu markieren, denn dann wird sie von RMI standardm¨aßig nicht zusammen mit dem Node-Objekt serialisiert und verschickt. Die Matrix wird n¨amlich nur im Bottom-up-Schritt von der Funktion Build zwischen Knoten transferiert und kann hier explizit als Parameter angegeben werden. function Build (par:Node; T:Tree; d:int[]) -> T’:Tree; S’:SparseMat { nd = selectNode(T); if isRoot(nd) then nd_ = buildRoot (nd); else nd_ = buildDown (par, nd); fi if isLeaf (T) then T’ = Leaf (buildLeaf (nd_)); S’ = getMatrix(nd_); else T1’,S1’ = Build (nd_, T.leftTree, d) 6.5 Das Koordinationsprogramm in FASAN 125 on dest_left(nd, myHost, numHosts, d); T2’,S2’ = Build (nd_, T.rightTree, d) on dest_right(nd, myHost, numHosts, d); nd’ = buildUp (nd_, selectNode(T1’), S1’ selectNode(T2’), S2’); T’ = Branch (nd’,T1’,T2’); S’ = getMatrix(nd’); fi } Die Berechnungsschritte in den Knoten des Substrukturierungsbaums, die sich aus den Beschreibungen der Lösungsverfahren GS-HB (Abb. 6.10), GS-HB-E(6.12), J-HB(6.14) und CG-HB(6.16) ergeben, sind in externen Funktion zusammengefasst: function buildRoot (nd:Node) -> nd’:Node extern Java function buildDown (par:Node; nd:Node) -> nd’:Node extern Java function buildLeaf (nd:Node;) -> nd’:Node extern Java function buildUp (nd:Node; n1:Node; s1:SparseMat; n2:Node; s2:SparseMat) -> nd’:Node; s’:SparseMat extern Java function solveRoot (nd:Node) -> nd’:Node extern Java function solveDown (par:Node; nd:Node) -> nd’:Node extern Java function solveLeaf (nd:Node) -> nd’:Node extern Java funvtion solveUp (nd:Node; n1:Node; n2:Node) -> nd’:Node extern Java Für jeden Baumknoten werden jeweils entweder die Root- oder die Down-Funktion aufgerufen und entweder die Leaf- oder die Up-Funktion. Die Programmierung dieser externen Funktionen in Java bereitet keine größeren Schwierigkeiten, deshalb verzichten wir auf einen vollst¨andigen Abdruck des Quelltextes. Die Funktion Solve ist — bis auf die Parameter für die Matrizen und Ersetzung des Funktionsnamens — identisch mit Build. Die Funktion IterSolve zur Konstruktion einer Schleife über die Solve-Funktion haben wir im Prinzip schon in Beispiel 3.5 auf Seite 34 angegeben. Falls ein Knoten im Substrukturierungsbaum vier Söhne haben soll, oder allgemein eine flexiblere Gebietszerlegung implementiert wird, ist eine Formulierung für beliebigen Verzweigungsgrad durch den hierarchischen Datentyp type Forest = forest (first:Tree; rest:Forest) | empty() type Tree = branch (node:Node; children:Forest) | leaf (nd:Node) 126 Die rekursive Substrukturierung in der numerischen Simulation nötig. Anstatt zu den rekursiven Aufrufen für jede Funktion einen eigenen Iterator über Forest zu schreiben (also buildForst, solveForest etc.), verwenden wir dann kürzer eine Funktion höherer Ordnung, der wir die benötigten Down- und Up-Funktionen mit folgenden Funktionstypen übergeben: type NodeFun = Node->Node type DownFun = (Node,Node)->Node type UpFun = (Node,Forest)->Node Die Funktion für den Abstieg im Baum, mapDown, haben wir bereits in Beispiel 3.10 auf Seite 38 behandelt. Die Funktion für den Aufstieg lautet: function mapUp (leaf_f:NodeFun; up_f:UpFun; t:Tree) -> t’:Tree { if isleaf(t) then t’ = leaf (leaf_f(t.leafVal)); else branch(x, sons) = t; sons’ = map (mapUp(leaf_f, up_f, _), sons); t’ = branch (Node(up_f(x, sons’), sons’); fi } Dass die externe Funktion up_f die Sohnknoten in einem Parameter vom Typ Forest bekommt, ist weiter kein Problem. Denn Konstruktor- und Selektorfunktionen für hierarchische Datentypen sind auch in den externen Funktionen als Methoden der Verpackungsklasse Tree verwendbar (vgl. hierzu die Übersetzung von FASAN nach Java, Abschnitt 5.3.2). Somit wird die Funktion Solve sehr kurz: function Solve (par:Node; t:Tree) -> t’:Tree { t_ = mapDown (solveRoot(_), solveDown(_,_), t); t’ = mapUp (solveLeaf(_), solveUp(_,_), t_); } Die Kommunikationsbeziehungen zwischen den Baumknoten folgen den Kanten des Baums zwischen Vater- und Sohnknoten. Trotzdem ist der Aufwand für die zeitlich und inhaltlich korrekte Übertragung der Knoteninhalte eines unbalancierten Baums bei verteilter Berechnung nicht zu untersch¨atzen, wenn diese von Hand programmiert werden muss. Je komplizierter die betrachteten Gebiete und die verwendeten Löser werden, desto komplizierter werden auch die verwendeten Datenstrukturen und die Migration der entstehenden Baumstrukturen. Hier erleichtert der Einsatz von FASAN die Programmierung erheblich, weil speziell Kommunikation und Synchronisation der parallelen Kontrollflüsse, die die Unterb¨aume bearbeiten, automatisch generiert werden. 6.6 Lastverteilung 127 Das folgende Kapitel wird experimentelle Ergebnisse liefern, die mit diesem Rahmenprogramm (für Bisektion) gewonnen wurden und die demonstrieren, wie effizient sich FASAN für diese Anwendung in der Praxis verh¨alt. Zun¨achst fehlen uns aber noch Konzepte und die Implementierung der Lastverteilung, die dieses Anwendungskapitel abschließen. 6.6 Lastverteilung Dieser Abschnitt stellt Verfahren vor, die für die Lastverteilung bei der verteilten rekursiven Substrukturierung zum Einsatz kommen. Nach einer allgemeinen Beschreibung geben wir auch die Lokationsfunktionen an, die zur Lastverteilung im FASAN-Programm eingesetzt wurden. Grunds¨atzlich ist zu unterscheiden, ob der Substrukturierungsbaum balanciert ist oder nicht. Im ersten Fall genügt ein statisches Lastverteilungsverfahren (Abschnitt 6.6.1). Im adaptiven Fall ist es wichtig, die Kostenabsch¨atzungen für die zu verteilenden Einheiten zu bestimmen. In Abschnitt 6.6.2 überlegen wir uns, welche Berechnungs- und Kommunikationskosten in den Knoten des Substrukturierungsbaums anfallen. Das dynamische Lastverteilungsverfahren (Abschnitt 6.6.3) beruht darauf, nach jeder Erweiterung des Baums in jedem Knoten die zukünftigen Kosten zu vermerken und danach die Knoten noch vor der Bildung der Knotengleichungssysteme neu auf die Rechner zu verteilen. 6.6.1 Ein statisches Verfahren Eine einfache statische Verteilungsstrategie für balancierte B¨aume nutzt die mögliche Parallelit¨at in den oberen Baumebenen, angefangen von der Wurzel. Jeder Knoten einer Baumebene wird einem neuen Rechner zugeteilt, solange noch Rechner ohne Baumknoten sind. Wenn alle Rechner mit parallelen Funktionsaufrufen besch¨aftigt sind, werden Knoten in tieferen Baumebenen dem Rechner zugeteilt, auf dem ihr Vaterknoten liegt. Eine Verteilungsstrategie aus [Hüt96], die auch jeweils den Vaterknoten einem eigenen Rechner zuweist, ist nur bei Hauptspeichermangel sinnvoll. Die zugehörige Lokationsfunktion lautet dann für den linken Sohn eines Knotens in Tiefe depth int static_left (int depth, int myHost, int numHosts) { return myHost; } und für den rechten Sohn, der gegebenenfalls auf einen neuen Rechner migriert wird int static_right (int depth, int myHost, int numHosts) { int newHost = myHost + numHosts / pow (NUM_SONS, depth+1); return (newHost > numHosts) ? myHost : newHost; } 128 Die rekursive Substrukturierung in der numerischen Simulation 0 0 8 0 0 0 0 2 1 8 4 2 3 4 4 6 5 6 8 7 8 12 10 12 0 14 9 10 11 12 13 14 15 0 1 4 2 4 3 5 8 6 7 8 12 9 10 11 12 13 14 15 Abbildung 6.17 Verteilung der Knoten balancierter Substrukturierungsbäume auf 16 Rechner: links bei rekursiver Gebietszerlegung in jeweils zwei Teilgebiete, rechts bei Zerlegung in vier Teilgebiete. Die Nummern bei den Knoten geben den Zielrechner an. In Abb. 6.17 ist die Verteilung der Knoten auf 16 Rechner dargestellt, bei rekursiver Zweiteilung des Gebiets (Bisektion) und bei rekursiver Vierteilung. 6.6.2 Kostenabsch¨ atzung der L¨ osungsverfahren Sowohl der Build-Schritt zum Aufbau der Knotengleichungssysteme als auch die anschließenden Solve-Schritte vollziehen in der Iteration zur Lösung der Gleichungssysteme komplette Baumdurchl¨aufe. Deshalb ist es sinnvoll, bei nutzbaren Rechnern disjunkte Teilb¨aume zu finden, die möglichst gleiche Rechenlast erzeugen und zusammen alle Knoten enthalten. Dazu ist aber eine Vorhersage der Rechenlast nötig, die jeder Baumknoten verursacht. Den größten Anteil an den Berechnungskosten haben der Build-Schritt zur Erstellung der Gleichungssysteme und die Top-down- und Bottom-up-Schritte für die iterative Lösung der Gleichungssysteme. In modernen numerischen Verfahren ist allerdings die Zahl der Gleitpunktoperationen allein kein ausreichendes Maß für die Berechnungskosten, weil etwa bei dünnbesiedelten Matrizen für die Verfolgung von Zeigerstrukturen oft mehr Aufwand entsteht als durch die arithmetischen Operationen. Hinzu kommt noch das dynamisch adaptive Verhalten vieler Algorithmen (so auch der rekursiven Substrukturierung), das selten statisch genau bestimmt werden kann. Erleichternd für unsere Aufgabe der Lastverteilung ist aber, dass wir nicht die absoluten Rechenkosten benötigen, sondern nur die Kosten-Verhältnisse der einzelnen Knoten. Bei effizienter Implementierung vor allem der Datenstruktur für die dünnbesiedelten Matrizen im Substrukturierungsbaum ist dann der Gesamtaufwand etwa proportional zu der Anzahl der Gleitpunktoperationen. Deshalb genügt es, wenn wir uns hier auf die Gleitpunktoperationen beschr¨anken. 6.6 Lastverteilung 129 In den folgenden Kostenabsch¨atzungen benutzen wir die Größen: Anzahl der externen Freiheitsgrade eines Knotens Anzahl der internen Freiheitsgrade eines Knotens Anzahl der Söhne eines Knotens (= Verzweigungsgrad ) die durchschnittliche Anzahl von Matrixeintr¨agen in einer Matrixzeile Die Anzahl aller Matrixeintr¨age beim Verfahren GS-HB bei gleichm¨aßiger Verfeinerung ist nach [Hüt96, S.103ff] / + / mit Baumtiefe bei Bisektion (6.3) wobei ’ ’ nur in den F¨allen gilt, in denen der Knoten einen Teil des Gebietsrands oder (bei adaptiver Verfeinerung) h¨angende Punkte auf dem Separator enth¨alt. der Gleitpunktoperationen in den einzelnen Schritten der Für die nötige Anzahl Lösungsverfahren überlegen wir uns zun¨achst die Kosten folgender elementarer Operationen: + . Skalarprodukt zweier Vektoren der L¨ange : Pro Vektorkomponente ist eine Addition und eine Multiplikation erforderlich. Multiplikation einer Matrix mit einem Vektor der L¨ange : + . Pro Vektorkomponente sind für alle Eintr¨age der entsprechenden Matrixzeile eine Addition und eine Multiplikation nötig. Hierarchisierung eines Vektors: + . Für jeden der 12 Eintr¨age in der Hierarchisierungstabelle muss eine Addition und eine Multiplikation erfolgen. Hierarchisierung einer Marix: . Für jeden Eintrag der Hierarchisierungstabelle ist eine Zeilenoperation auf Zei leneintr¨agen auszuführen. Dieselben Operationen sind wegen der Multiplikati on der Hierarchisierungsmatrix von links und von rechts ( ) nochmals nötig. Assemblierung eines Vektors mit Komponenten: . Jeder Vektoreintrag wird aus den Summanden der Sohnknoten zusammengesetzt. Assemblierung einer Matrix mit Zeilen: . In jeder Matrixzeile werden für jeden der Eintr¨age aus jedem der Sohnknoten Eintr¨age kombiniert. Da sich die einzelnen Schritte der Lösungsverfahren aus diesen elementaren Operationen zusammensetzen, können wir sofort die gen¨aherten Kosten pro Baumknoten und pro Durchlauf bei den beschriebenen iterativen Verfahren angeben. 130 Die rekursive Substrukturierung in der numerischen Simulation Verfahren GS-HB Build-Schritt: (B1) (Abb. 6.10) (B2) + + (B3) Top-down- und Bottom-up-Schritt: (D1) (D2) (D3) + + + + Verfahren GS-HB-E (U1) (D4) (D5) + (U2) + (U3) (U4) (Abb. 6.12) Die Kosten für den Gauß-Seidel-Löser mit partieller Elimination umfassen die obigen Tabellen, und zus¨atzlich die Kosten der Multiplikation mit der Eliminationsmatrix bzw. : (BE) + (DE) (UE) Verfahren J-HB (Abb. 6.14) % Beim Jacobi-Verfahren wird (bis auf einen 4 4-Block) nur die Diagonale der Systemmatrix zum Vaterknoten gereicht, daher ist hier außer in den Bl¨attern = 1. Der Ausdünnungsschritt (B3) erfordert keine Gleitkomma-Operationen, löscht 60 Eintr¨age (vgl. Abb. 6.11) und kann bei geeigneter Datenstruktur der Matrix mit diesem konstanten Aufwand erfolgen. Build-Schritt: (B1) (B2) (B3) + Top-down- und Bottom-up-Schritt: (D1) (D2) (D3) (U0) (U1) + (U2) + Verfahren CG-HB (Abb. 6.16) Hier kommt die Systemmatrix (und damit die Zahl Build-Schritt: (B0) (B1) (B2) + ) nur noch in den Bl¨attern vor: (B3) + Top-down- und Bottom-up-Schritt im 1. Zyklus: (D1.0) (D1.1) (D1.2) (D1.3) (U1.0) (U1.1) + + (U1.2) + (U1.3) + 6.6 Lastverteilung 131 Top-down-und Bottom-up-Schritt im 2. Zyklus: (D2.0) (D2.1) (D2.2) (D2.3) (U2.0) (U2.1) + + + Mit diesen Tabellen haben wir jetzt die Anzahl der nötigen Gleitpunktoperationen pro Baumknoten erfasst, und in guter N¨aherung proportional dazu den Gesamtaufwand an Rechenoperationen. Bei parallelen Berechnungen übersteigen die Kosten einer Kommunikationsoperation die einer Rechenoperation um mehrere Größenordnungen, und selbst bei Berechnungen auf gemeinsamem Speicher macht die Erzeugung und Verwaltung neuer Tasks oder Threads einen nicht unerheblichen Teil der Laufzeit aus. Bei verteilten Berechnungen kommt noch der Unsicherheitsfaktor dazu, dass die Verzögerungen auf dem Verbindungsnetzwerk schwanken. Allerdings können diese Kosten bei den drei neuen Lösungsvarianten ebenfalls leicht abgesch¨atzt werden. In der Beschreibung der Algorithmen (Abb. 6.12, 6.14 und 6.16 in Abschnitt 6.4) haben wir immer Werte, die vom Vaterknoten bezogen werden, mit einem " oberen Index versehen, und Werte aus den Söhnen mit . Dort ist also unmittelbar ersichtlich, welche Werte, Vektoren und Matrizen im Fall, dass Vater- und Sohnknoten auf verschiedenen Rechnern liegen, verschickt werden müssen. Bei Vektoren und Matrizen müssen dabei nur die Werte versandt werden, die zu externen Punkten gehören, die in beiden Knoten vorkommen. Bei der RMI-Implementierung verteilter FASAN-Programme ist diese Beschr¨ankung besonders wichtig, da ab einer Feldl¨ange von etwa 500 Eintr¨agen die Übertragungszeiten der Argumente von RMI-Aufrufen sprunghaft und dann leicht überproportional zur Feldl¨ange ansteigen (vgl. Abschnitt 5.2.1). 6.6.3 Ein dynamisches Verfahren Ein unausgeglichener Substrukturierungsbaum kann entstehen, wenn auf Basis eines Fehlersch¨atzers das zugrundeliegende Gitter und damit der Baum nur lokal verfeinert und nicht alle Bl¨atter weiter unterteilt werden. Um in diesen F¨allen mehrere Rechner effizient für die Berechnung nutzen zu können, ist es wichtig, die Zuteilung der Baumknoten an die Rechner ebenso adaptiv vornehmen zu können. Ein einfaches, aber wirkungsvolles Verfahren basiert auf der Anwendung des HF-Algorithmus [BEE98b, BEE98a]. Dazu muss zun¨achst jedem Baumknoten eine Last zugewiesen werden, die sich aus der Kostenabsch¨atzung aus Abschnitt 6.6.2 ergibt. Zus¨atzlich wird in jedem Baumknoten auch das Gewicht des gesamten Unterbaums, dessen Wurzel ist, akkumuliert: / falls Blatt ist, falls Knoten mit Söhnen und / ist. 132 Die rekursive Substrukturierung in der numerischen Simulation Algorithmus HF, als Grundlage von Lokationsfunktionen1 angewandt auf diesen Substrukturierungsbaum, lautet dann: import java.util.TreeSet; import areso.WeightTree; TreeSet hf (WeightTree t, int numHosts) { { TreeSet subproblems = new TreeSet(); subproblems.add(t); while (subproblems.size() < numHosts) { Tree q = (Tree) getElemWithMaxWeight(subproblems); subproblems.remove(q); subproblems.add(q.left()); subproblems.add(q.right()); } return subproblems; } Damit erhalten wir numHosts Baumknoten, die wir auf die numHosts Rechner verteilen können. Knoten in Unterb¨aumen dieser Knoten werden demselben Rechner wie die Teilbaum-Wurzel zugeteilt. Für die höheren Knoten kann jeweils einer der Rechner der Sohnknoten ausgew¨ahlt werden, am besten der mit dem kleineren Gewicht, um den Speicherverbrauch besser auszugleichen. Für einen Baum mit hypothetischen Gewichten ist in Abb. 6.18 angegeben, welche Knoten Algorithmus HF bei Verteilung auf 8 und auf 16 Rechner liefert. Ein Vorteil dieser Verteilungsstrategie ist seine theoretisch nachweisbare PerformanceGarantie bezüglich einer (praktisch meist nicht erreichbaren) optimalen Verteilung. Lemma 6.4 Algorithmus HF liefert einen Schnitt von strukturierungsbaum mit Wurzel , so dass gilt: / / " Dabei ist / Knoten in einem Sub- aus allen Nicht-Bl¨attern p des Substruk- turierungsbaums. Für den Beweis siehe [BEE98b]. Für den Einsatz im FASAN-Programm verwenden wir die Nummern der Wurzeln der ermittelten disjunkten Teilb¨aume als Indizes in einem int[]-Array. Eine Methode 1 für die Dokumentation der Set-Klasse siehe z.B. http://java.sun.com/products/jdk/1.2/docs/api/ 6.6 Lastverteilung 133 153 70 64 48 27 8 15 12 4 0 8 5 1 48 9 2 4 10 3 3 2 16 2 4 3 11 22 4 10 4 4 3 12 3 5 5 6 6 7 Abbildung 6.18 Dynamische Lastverteilung durch Algorithmus HF für 8 Rechner (die grau hinterlegten Knoten) und 16 Rechner (die Knoten unmittelbar über den schematisierten Unterb¨aumen). Die Zahlen in den Knoten geben hypothetische Gewichte an. distribute hat dann dafür zu sorgen, dass jeder dieser Indizes eine andere Rechnernummer zugewiesen bekommt. Damit ist die in Abschnitt 6.5 benötigte Funktion die folgende: int[] balance (WeightTree w) { return distribute(hf(w)); } Die passenden Lokationsfunktionen sehen so aus: int hf_left (Node nd, int actHost, int[] dest) { int nodeId = 2 * nd.ident; return idx = (nodeId < dest[0]) ? dest[nodeId] : actHost; } int hf_right (Node nd, int actHost, int[] dest) { int nodeId = 2 * nd.ident + 1; return idx = (nodeId < dest[0]) ? dest[nodeId] : actHost; } Wir haben jetzt also alle Teile beisammen, die für unsere FASAN/Java-Anwendung notwendig sind: den sequentiellen Grundalgorithmus der rekursiven Substrukturierung mit verschiedenen Lösungsvarianten, das FASAN-Programm zur Koordination des parallelen Ablaufs sowie zwei Strategien zur Lastverteilung, die wir für die Lokationsfunktionen dest_left und dest_right im FASAN-Programm verwenden können. Bleibt uns noch, das entstandene Programm zu testen. 134 Die rekursive Substrukturierung in der numerischen Simulation 135 Kapitel 7 Beispielrechnungen und Leistungsmessungen Als Abschluss der Arbeit demonstrieren wir in diesem Kapitel den praktischen Einsatz von FASAN auf Grundlage der Java-Implementierung der rekursiven Substrukturierung. Wir geben zum einen numerische Resultate an, die vorführen, dass die neuen Lösungsvarianten für den praktischen Einsatz geeignet sind. Zum anderen belegen Laufzeitmessungen auf einer SMP-Maschine und in einem Rechnernetz, dass sich FASAN als Werkzeug eignet, um mit wenig Programmieraufwand effiziente verteilte numerische Programme zu erstellen. 7.1 Testbeispiele für die Simulation Wir betrachten zun¨achst einfache Randwertaufgaben, die zwar vom Standpunkt der numerischen Simulation leicht zu behandeln sind, dafür sich aber bei verteilter Ausführung zum Testen von FASAN besonders eignen, weil durch die kurzen Rechenzeit in den Knoten des Substrukturierungsbaums h¨aufig Datenaustausch zwischen den Knoten stattfindet. Dass die mit FASAN und Java implementierte Version der rekursiven Substrukturierung auch für praktische Probleme einsetzbar ist, soll an einem Beispiel aus der Strukturmechanik vorgeführt werden. % In allen Beispielen w¨ahlen wir als Gebiet, auf dem das Problem gelöst werden soll, das Einheitsquadrat ( . 136 Beispielrechnungen und Leistungsmessungen 8 0.12 7 0.1 6 0.08 5 0.06 4 0.04 3 1 2 0.02 0.8 10 0 0.6 0.2 0.4 0.4 0.6 0.8 0.5 0 0.5 0.2 0 1 0 Abbildung 7.1 f(x,y) (links) und die numerisch berechnete Lösung (rechts) im Beispiel POISSON auf dem Einheitsquadrat. Bei der Lösung ist die Verschiebung des Maximums aus dem Mittelpunkt in Richtung positiver x- und y-Achse zu erkennen. / / / (/ Poisson-Gleichung Die zweidimensionale Poisson-Gleichung ist die Randwertaufgabe im Gebiet ( auf dem Rand ( (7.1) ( ( ( Dabei bezeichnet die gesuchte Lösungsfunktion . Zur Diskretisierung des Differentialoperators verwenden wir den 5-Punkte-Stern % bzw. einen 3-Punkte-Stern am Rand des Gebiets und in den Bl¨attern mit einem 2 2 Gitter, + / beispielsweise für den linken oberen Punkt . Für unsere Beispielrechnungen verwenden wir speziell die Funktionen ( ( (POISSON) ( Die Funktion sowie die mit dem FASAN/Java-Programm berechnete Lösung sind in Abb. 7.1 dargestellt. ( Stationäre Wärmeleitung Mit der Gleichung (7.1) lassen sich auch station¨are W¨armeleitprobleme beschreiben [Hüt96]. Hierzu w¨ahlen wir als konkretes Beispiel (HEAT1) ( ( 0 0 0 ( 7.1 Testbeispiele für die Simulation 137 1 0.8 0.6 0.4 0.2 1 0.8 00 0.6 0.2 0.4 0.4 0.6 0.8 0.2 1 0 Abbildung 7.2 Lösung von Beispiel HEAT1 auf dem Einheitsquadrat 1 0.8 0.6 0.4 0.2 0 0 0 0.5 0.5 Abbildung 7.3 Lösung von HEAT2 auf dem Einheitsquadrat. Die Sprungstellen der Beispiel und führen zu einer stark adaptiven Verfeinerung des Gitters. Lösung bei ( bei dem gleichzeitig auch die analytisch ermittelbare Lösung im Gebietsinneren ist (Abb. 7.2). Vor allem zum Test paralleler Berechnungen mit adaptiver Verfeinerung verwenden wir weiterhin das Beispiel mit ( ( falls falls ( und oder ( (HEAT2) In den vorgegebenen Randwerten sind zwei Sprungstellen (Abb. 7.3), in deren Umgebung das Gitter für die numerische Berechnung besonders fein aufgelöst werden muss, wenn man den Diskretisierungsfehler verkleinern will. Ebene Elastizität Als Beispielanwendung aus der Strukturmechanik betrachten wir eine kurze Kragscheibe unter gleichm¨aßiger Belastung, also ein ebenes Elastizit¨atsproblem. 138 Beispielrechnungen und Leistungsmessungen Die quadratische Scheibe wird auf ihrer Oberseite gleichm¨aßig belastet. Die linke Seite der Scheibe ist fixiert. Die physikalischen Eigenschaften des Materials sind durch den E-Modul sowie die Querdehnzahl gegeben. Die folgenden Differentialgleichungen beschreiben dann die Reaktion des Objekts auf die ¨außere Last: + + (PANEL) / / / / / / ( + / / ( ( / (/ / / + 4 wobei und die unbekannten Verschiebungen und und die externen Kr¨afte in - bzw. -Richtung sind. Die Diskretisierung erfolgt hier mittels des Ritz-GalerkinVerfahrens für Finite Elemente und der Verschiebungsmethode [Bat90], bei der für die gesuchten Verschiebungen und in jedem Element die Ansatzfunktionen ( ( / ( / ( ( gew¨ahlt werden. Mit den Verzerrungen die Matrix durch ( ( ( , und bilden wir . Wenn wir für unser Beiund damit die Verzerrungs-Verschiebungs-Matrix spiel mit isotropem linear-elastischen Material in der Matrix / / und w¨ahlen, erhalten wir schließlich mit für ein Blatt des Substrukturierungsbaums mit Teilgebiet der Größe + + die Eintr¨age der Steifigkeitsmatrix als ! !( % 7.2 Numerische Ergebnisse der rekursiven Substrukturierung 139 7.2 Numerische Ergebnisse der rekursiven Substrukturierung Bevor wir Laufzeiten der parallelen Ausführung messen, vergewissern wir uns, dass die entwickelten numerischen Verfahren effizient sind. Denn wir wollen kein Verfahren verwenden, das zwar gute Parallelisierungseigenschaften hat, aber praktisch durch zu langsame Konvergenz keinen Sinn machen würde. Mit jeder Iteration eines Lösungsverfahrens erhalten wir eine verbesserte N¨aherung an die tats¨achliche Lösung . Da — außer in speziellen F¨allen wie dem Beispiel (HEAT1) — nicht bekannt ist, wird stattdessen das Residuum / / berechnet. Als Maß für die Güte eines Iterationsverfahren wird die Konvergenzrate mit der euklidischen Norm / / / verwendet, für die wir die experimentelle N¨aherung bei Abbruch mit angeben. Weiterhin bezeichnen in den Tabellen M FHG die Tiefe des Substrukturierungsbaums (bei Bisektion), die Anzahl der Freiheitsgrade, die N¨aherung der Konvergenzrate (die wir bei CG-HB auslassen, weil sie naturgem¨aß bei Konjugierten-Gradienten-Lösern st¨arkeren Schwankungen unterworfen ist), die Anzahl der Iterationen (Baumtraversierungen) des Lösers bis zum Erreichen der Genauigkeit , MFlop die Anzahl der benötigten Gleitpunkt-Operationen. Fehlen für diesen Wert die Eintr¨age in der Tabelle, liegt das an der Berechnung mit dem verteilten Programm, das dafür nicht instrumentiert war. Alle Versuche wurden mit Transformation auf hierarchische Basis durchgeführt, so wie sie bei den einzelnen Lösungsverfahren in Kapitel 6 beschrieben wurde. Als Abbruch schranke für die Norm des Residuums des Gesamtsystems wurde durchwegs verwendet, für die Anzahl der Gauß-Seidel-Iterationen bei den Verfahren GS , und für das Vefahren J-HB der Relaxationsfaktor HB und GS-HB-E , . außer bei der Kragscheibe / 140 Beispielrechnungen und Leistungsmessungen GS-HB GS-HB-E 1 2 3 1 2 3 0.11 0.15 0.15 0.11 0.18 0.18 0.08 0.15 0.09 0.09 0.15 0.12 Knoten HEAT1/POISSON PANEL Tabelle 7.1 Füllungsgrad der Systemmatrizen beim Lösungsverfahren GS-HB-E M 4 6 8 10 12 14 16 FHG 25 81 289 1089 4225 16641 66049 M 4 6 8 10 12 14 16 GS-HB MFlop 11 0.024225 16 0.194424 19 1.154745 24 6.697520 31 37.641437 38 194.385290 45 0.333 0.515 0.598 0.689 0.760 0.820 0.867 FHG 25 81 289 1089 4225 16641 66049 0.139 0.182 0.553 0.578 0.672 0.748 0.816 J-HB 0.719 30 0.785 36 0.848 46 0.889 59 0.915 74 0.934 90 0.947 107 MFlop 0.009808 0.078580 0.505456 2.884306 15.195640 75.540768 11 14 20 25 31 43 51 GS-HB-E MFlop 6 0.015610 8 0.117400 12 0.878348 18 6.079950 23 34.226693 29 182.819437 35 CG-HB MFlop 0.009615 0.068940 0.474762 2.628993 13.774611 78.633231 Tabelle 7.2 Berechnungen des Beispiels POISSON 7.2.1 Gleichm¨ aßige Verfeinerung Vor der Betrachtung der Konvergenzraten und Iterationszahlen wollen wir untersuchen, ob die evtl. eingesparte Rechenzeit durch das neue Verfahren GS-HB-E gegenüber GSHB nicht etwa durch stark erhöhten Speicherbedarf erkauft wurde. Da die Systemmatrizen den Hauptteil des Speicherbedarfs bei beiden Verfahren ausmachen, betrachten wir in Tabelle 7.1 ihren Füllgrad (= Anzahl Nicht-Null-Eintr¨age geteilt durch Matrixgröße) für Tiefe + in den obersten Baumknoten (1=Wurzel, 2=linker Sohn der Wurzel, 3=rechter Sohn der Wurzel). Der durch die Elimination verursachte Fill-in ist also nur geringfügig und hat praktisch keine Auswirkung. Poisson-Gleichung: (Beispiel POISSON, Tabelle 7.2) In dieser Tabelle erkennen wir das logarithmische Ansteigen der Iterationszahlen, sowohl beim bisherigen Verfahren GS-HB als auch bei GS-HB-E mit partieller Elimination star- 7.2 Numerische Ergebnisse der rekursiven Substrukturierung M 4 6 8 10 12 14 FHG 25 81 289 1089 4225 16641 M 4 6 8 10 12 14 GS-HB MFlop 8 0.018576 13 0.160959 16 0.983544 21 5.898215 28 34.121900 35 179.453777 0.333 0.522 0.512 0.575 0.657 0.732 FHG 25 81 289 1089 4225 16641 0.719 0.780 0.845 0.887 0.914 0.932 0.148 0.264 0.395 0.504 0.608 0.690 J-HB MFlop 34 0.011032 41 0.088670 54 0.586624 71 3.429610 90 18.276376 111 92.202378 141 GS-HB-E MFlop 6 0.015610 9 0.129499 13 0.943543 18 6.079950 23 34.226693 29 182.819437 12 15 24 34 50 70 CG-HB MFlop 0.010458 0.073755 0.568374 3.565200 22.145004 127.256074 Tabelle 7.3 Berechnungen des Beispiels HEAT1 ker Kopplungen in der Systemmatrix. W¨ahrend die Anzahl der Gleitpunktoperationen bei GS-HB-E nicht wesentlich geringer ist als bei GS-HB, ist die eingesparte Zahl von Iterationen für die Praxis bedeutsam. Bei den beiden speichersparenden Lösungsvarianten J-HB und CG-HB stellen wir für CG-HB eine etwas höhere, bei J-HB eine bedeutend höhere Anzahl von Iterationen bis zum Erreichen der gewünschten Genauigkeit fest. Allerdings ist bei beiden Verfahren die Zahl der Gleitpunktoperationen deutlich niedriger als bei GS-HB und GS-HB-E. Bei sequentieller Berechnung ist CG-HB dadurch effektiv schneller. Stationäre Wärmeleitung: (Beispiel HEAT1, Tabelle 7.3) Mit den Verfahren GS-HB und GS-HB-E erfordert dieses Beispiel bei den meisten Baumtiefen etwas weniger Iterationen und Gleitpunktoperationen als im Beispiel POISSON. Die Löser J-HB und CG-HB benötigen dagegen etwas mehr Iterationen als im vorigen Beispiel. Dennoch w¨achst bei ihnen die Zahl der Iterationen auch hier logarithmisch mit der Anzahl der Freiheitsgrade. Wärmeleitung mit unstetigen Randwerten: (Beispiel HEAT2, Tabelle 7.4) Unstetige Randbedingungen haben kaum Einfluss auf die Konvergenzgeschwindigkeit des Lösers, die theoretisch nur von der Systemmatrix abh¨angt. So l¨asst sich auch dieses Beispiel problemlos mit allen Lösern durchrechnen. 142 Beispielrechnungen und Leistungsmessungen M FHG 4 25 6 81 8 289 10 1089 12 4225 GS-HB MFlop 11 0.024225 17 0.205579 21 1.268879 27 7.496825 34 41.160974 0.333 0.519 0.599 0.683 0.761 M FHG 4 25 6 81 8 289 10 1089 12 4225 0.136 0.223 0.583 0.602 0.673 J-HB 0.719 0.780 0.845 0.887 0.914 34 42 55 73 93 MFlop 0.011032 0.090688 0.596770 3.520494 18.854014 12 16 23 32 48 GS-HB-E MFlop 7 0.017521 9 0.129499 13 0.943543 19 6.394105 25 37.045131 CG-HB MFlop 0.010458 0.078570 0.544971 3.357154 21.263910 Tabelle 7.4 Berechnungen des Beispiels HEAT2 Die Unstetigkeitsstellen machen sich in einem starken Diskretisierungsfehler bemerkbar, sodass in ihrer N¨ahe die numerische N¨aherungslösung große Abweichungen von der analytischen Lösung aufweist. Diese Ungenauigkeit kann nur mit einem feineren Gitter bzw. größerer Baumtiefe reduziert werden. Deshalb greifen wir dieses Beispiel weiter unten bei der adaptiven Verfeinerung nochmals auf. Kragscheibe: (Beispiel PANEL, Tabelle 7.5) Für die Rechnung der Kragscheibe bringt die partielle Elimination (GS-HB-E) noch mehr Gewinn als bei den vorigen Beispielen: Zwischen 25% und 30% der Iterationen werden eingespart, um die gleiche Lösungsgenauigkeit zu erreichen. Die anderen beiden Verfahren schneiden bei den Iterationszahlen dagegen wesentlich schlechter ab. Die Konvergenzraten für das Verfahren J-HB scheinen schlecht zu sein. Allerdings muss beachtet werden, dass die Verfahren GS-HB und GS-HB-E in jedem Relaxationsschritte vornehmen, J-HB dagegen Baumdurchlauf pro Freiheitsgrad nur einen. Dadurch erkl¨art sich auch die Tatsache, dass die Anzahl der Gleitpunktoperationen von J-HB gegenüber GS-HB nicht bedeutend schlechter ist. Deshalb ist J-HB durchaus eine Alternative zu GS-HB und sogar die bessere Wahl, wenn es auf maximal möglich Genauigkeit der diskreten Lösung bezüglich der physikalischen Realit¨at ankommt. Denn dann soll der Substrukturierungsbaum möglichst tief sein, und J-HB spart wesentlich Speicherplatz, weil nur die Diagonale der Systemmatrix in den Nicht-BlattKnoten gehalten werden muss. Das CG-HB-Verfahren scheidet hier als Lösungsmöglichkeit aus, denn die Systemmatri zen sind in diesem Beispiel nicht definit, und damit konvergiert nicht mehr gegen die Lösung . Hier müssten also Verfahren eingesetzt werden wie beispielsweise die Verfah- 7.2 Numerische Ergebnisse der rekursiven Substrukturierung M 4 6 8 10 12 FHG 50 162 578 2168 8450 0.551 0.704 0.792 0.845 0.880 GS-HB MFlop 21 0.567638 35 4.476298 48 26.881052 64 150.718268 81 784.879558 M FHG 4 50 6 162 8 578 10 2168 12 8450 0.213 0.536 0.686 0.775 0.831 J-HB 143 GS-HB-E MFlop 9 0.277466 21 3.071590 32 20.743748 44 120.950068 59 670.448900 MFlop 0.980 596 1.362608 0.984 748 7.719184 0.987 901 39.396192 0.990 1107 199.113728 0.992 1356 989.090960 Tabelle 7.5 Berechnungen des Beispiels PANEL ren BiCG und QMR [BBC 94, S.21ff], die nicht nur auf einer orthogonalen Sequenz von Residuenvektoren aufbauen, sondern zwei wechselweise orthogonale Sequenzen von Residuenvektoren verwenden. Solche Verfahren w¨aren angesichts der Speicherersparnis in den Nicht-Blatt-Knoten des Substrukturierungsbaums lohnend. Mit indefiniten Systemen ist man h¨aufig bei numerischen Simulationen konfrontiert, z. B. mit gewissen Parametern der Konvektions-Diffusions-Gleichung [Fun97] oder der Helmholtz-Gleichung [BSZ99]. 7.2.2 Adaptive Verfeinerung Das Beispiel (HEAT2) benötigt wegen der Unstetigkeit seiner Randbedingungen in der Umgebung der Sprungstellen eine st¨arkere Verfeinerung des Gitters. Deshalb eignet es sich, um Berechnungen der Löser auf adaptiven, unbalancierten Substrukturierungsb¨aumen zu testen. Wir haben die vier Lösungsverfahren auf unbalancierte B¨aume der Tiefe 14, 20 und 26 (was bei uniform verfeinertem Gitter 16 641, 1 050 625 bzw. 67 125 249 Freiheitsgraden entsprechen würde) ausprobiert (Tabelle 7.6). Dabei f¨allt bei allen vier Lösern auf, dass die Konvergenzraten und die Anzahl der Iterationen mit zunehmender Tiefe kaum ansteigen. Dies kommt u. a. daher, dass h¨angende Punkte des Gitters bei Rechnung in hierarchischer Basis auf die Lösung keinen Einfluss haben, da sie den konstanten hierarchischen Überschuss 0 beitragen. Somit haben wir uns überzeugt, dass auch die neuen Lösungsvarianten praxistauglich sind und es sich lohnt, sie für die verteilte Berechnung zu nutzen. 144 Beispielrechnungen und Leistungsmessungen M 14 20 26 FHG 2191 0.631 4327 0.641 6259 0.644 M 14 20 26 GS-HB GS-HB-E MFlop MFlop 24 34.787538 0.568 20 35.915592 25 86.402437 0.592 21 89.721049 26 145.528414 0.597 22 154.360172 FHG 2191 0.861 4327 0.857 6259 0.856 J-HB 60 60 60 MFlop 8.961530 21.659811 CG-HB MFlop 35 11.199667 38 29.365765 39 47.662939 Tabelle 7.6 Adaptive Berechnungen des Beispiels HEAT2 7.3 Paralleler Ablauf mit FASAN Jetzt wollen wir noch prüfen, wie effizient das Substrukturierungsverfahren mit der in Abschnitt 6.5 eingeführten FASAN-Koordinationsschicht auf einem Rechnernetz arbeitet. Wir haben oben erw¨ahnt, dass speziell die Poisson-Gleichung wegen der einfachen Differentialoperatoren wenig Rechenzeit in den Knoten des Substrukturierungsbaums erfordert. Wir beschr¨anken uns deshalb in den folgenden Abschnitten, wo wir die Effizienz der FASAN-Koordinationsschicht testen wollen und nicht die Berechnungen in den Lösern, auf das Beispielproblem POISSON bzw. für die adaptiven Berechnungen auf Beispiel HEAT2. Zur Zeit der Fertigstellung dieser Arbeit war das aktuelle JDK 1.2 (Java 2, [Sun98]), das u. a. Just-in-Time-Übersetzung, Threads des Betriebssystems und etwas effizientere Serialisierungsmechanismen unterstützt, nur für Solaris- und Windows-Plattformen verfügbar [Nef99]. Deshalb fiel die Wahl der Testplattform auf ein Netz von 16 Workstations vom Typ Sun Ultra-60 mit 384 MB Hauptspeicher und je 2 Prozessoren. Alle Messungen wurden mindestens dreimal durchgeführt und das Minimum der Laufzeiten verwendet. Dies rechtfertigt sich mit der schwankenden Last der Rechner und des Verbindungsnetzes durch andere Benutzer. Denn diese Last ist in den beschriebenen Lastverteilungsmechanismen noch nicht berücksichtigt und soll deshalb in die Messungen nicht mitgerechnet werden. Außerdem wurde die Setup-Zeit zum Start und zur Vernetzung der FASAN-Server nicht mit einbezogen, da diese erstens in der aktuellen Implementierung großen Schwankungen unterworfen ist und zweitens bei mehreren Berechnungen nur einmal anf¨allt. 7.3 Paralleler Ablauf mit FASAN 145 Verfahren GS-HB, Beispiel POISSON, M=12, it=20 140 Thread-Programm auf 1 Prozessor Sequentielles Programm Thread-Programm auf 2 Prozessoren 120 Dauer [sec] 100 80 60 40 20 0 8 / 289 10 / 1089 12 / 4225 Baumtiefe / Anzahl Freiheitsgrade 14 / 16641 Abbildung 7.4 Laufzeiten auf einer 2-Prozessor-Maschine 7.3.1 Einsatz von Threads Wir wollen zun¨achst beobachten, wieviel Overhead die Ausnutzung der vom JDK 1.2 unterstützten POSIX-Threads auf einer 2-Prozessor-Maschine erfordert, und inwieweit sich die Nutzung beider Prozessoren lohnt. Hierzu übersetzen wir das FASAN-Programm in ein rein sequentielles Programm, wie es in Abschnitt 5.3 beschrieben wurde. Wir vergleichen dasselbe FASAN-Programm, aber übersetzt mit der Instrumentierung für Threads liefern, so(Abschnitt 5.4) und ¨andern die Lokationsfunktionen so ab, dass sie immer dass nur ein Prozessor genutzt werden kann. Abb. 7.4 zeigt, dass der Overhead, den das Thread-Programm durch Ausführung aller Selektor-Aufrufe in einem Monitor erzeugt, etwa 20% mehr Laufzeit kostet. Die Entscheidung, jeden Knoten einer hierarchischen Datenstruktur (der als Verpackungsobjekt übersetzt wird) einzeln mit seinem Monitor zu synchronisieren, bis sein Inhalt vorhanden ist, führt also mit dem JDK 1.2 zu leichten Effizienzeinbußen. Der massive Einsatz von Monitoren zur Synchronisation ist aber vertretbar, weil stark optimierte virtuelle Maschinen für Java wie z. B. HotSpot [Sun99] bedeutend effizienter bezüglich der Synchronisationsimplementierung sein werden. Außerdem l¨auft das Programm auf zwei Prozessoren schneller: Die untere Kurve in Abb. 7.4 gibt die Ausführungszeit an, die wir bei Nutzung beider Prozessoren und Erzeugung von Threads für die Berechnung von Baumknoten bis in Tiefe 3 erhalten. Wir kommen auf einen Speed-up von 1.625 gegenüber dem sequentiellen Programm, der zum einen durch den sequentiellen Berechnungsteil in der Wurzel des Substrukturierungsbaums und zum anderen durch den Overhead, den die Synchronisationsmechanismen verursachen, begrenzt ist. 146 Beispielrechnungen und Leistungsmessungen Verfahren GS-HB-E, Beispiel POISSON 1800 1600 Dauer [sec] 1400 1200 1 Rechner 2 Rechner 4 Rechner 8 Rechner 16 Rechner 1000 800 600 400 200 10 / 1089 / 18 12 / 4225 / 23 14 / 16641 / 29 16 / 66049 / 35 Baumtiefe / Anzahl Freiheitsgrade / Anzahl Iterationen Abbildung 7.5 Laufzeiten des verteilten FASAN-Programms mit Verfahren GS-HB-E 7.3.2 Verteilte Berechnung bei gleichm¨ aßiger Verfeinerung Für die Laufzeitmessungen beschr¨anken wir uns auf die Lösungsvarianten GS-HB-E und CG-HB. Das Verfahren GS-HB unterscheidet sich nur insofern von GS-HB-E, als es den Schritt der partiellen Elimination nicht enth¨alt. Diese Elimination verursacht aber nur Kosten in der Größenordnung eines Hierarchisierungsschritts, wie wir in Abschnitt 6.6.2 angegeben haben; bei gleicher Iterationszahl unterscheiden sich die Laufzeiten der beiden Verfahren daher so gut wie nicht. Ähnlich sieht es mit den Verfahren J-HB und CGHB aus. Sie erfordern beide sehr wenig Rechenaufwand pro Iterationsschritt, und auch die Speicheranforderungen sind sehr ¨ahnlich: W¨ahrend J-HB die Diagonalelemente der Matrix in jedem Knoten halten muss, braucht CG-HB in jedem Knoten zus¨atzlich einen Vektor für die Suchrichtung. Weiterhin können Dirichlet-Randwerte bei beiden Verfahren bereits in den Bl¨attern eliminiert werden. Betrachten wir die Laufzeiten der beiden Verfahren GS-HB-E (Abb. 7.5) und CG-HB (Abb. 7.6), so f¨allt auf, dass CG-HB nicht schneller ist als GS-HB-E, obwohl es etwa nur halb so viele Gleitpunkt-Operationen ausführen muss. Andererseits hat CG-HB mehr Iterationen auszuführen, und mit jeder Iteration den vollen Kommunikationsaufwand eines Baumdurchlaufs. Ein Vorteil von CG-HB gegenüber GS-HB-E liegt aber klar im Speicherbedarf: Mit der verwendeten prototypischen Implementierung des Java-Teils für das Substrukturierungsprogramm konnte Tiefe 18 (263 169 Freiheitsgrade) bei uniformem Gitter nur mit dem CG-HB-Löser auf 16 Rechnern erreicht werden. 7.3 Paralleler Ablauf mit FASAN 147 Verfahren CG_HB, Beispiel POISSON 3827 ////// 1800 1600 Dauer [sec] 1400 1200 1 Rechner 2 Rechner 4 Rechner 8 Rechner 16 Rechner 1000 800 600 400 200 10 / 1089 / 25 12 / 4225 / 31 14 / 16641 / 43 16 / 66049 / 51 18 / 263169 / 60 Baumtiefe / Anzahl Freiheitsgrade / Anzahl Iterationen Abbildung 7.6 Laufzeiten des verteilten FASAN-Programms mit Verfahren CG-HB Erfreulich ist, dass gerade bei großen Berechnungen eine deutliche Beschleunigung eintritt. Dies haben wir unter anderem zur Ermittlung der Iterationszahlen und Konvergenzraten in Abschnitt 7.2 ausgenützt. Ein Vorteil der rekursiven Substrukturierung ist, dass sie ganz automatisch eine Partitionierung der Freiheitsgrade, Separator für Separator, vornimmt, und dadurch nie wirklich lange Vektoren verschickt werden müssen, für die dann die Serialisierung von RMI unertr¨aglich lang dauern würde (vgl. Abschnitt 5.2.1). 7.3.3 Verteilte Berechnung bei adaptiver Verfeinerung Mit dem Lastverteilungsalgorithmus HF haben wir die Möglichkeit, auch adaptive Berechnungen mit besserer Effizienz verteilt durchzuführen. Wir betrachten das Beispielproblem HEAT2, bei dem der Substrukturierungsbaum wegen der Sprungstellen in den Randbedingungen eine größere Tiefe aufweist und unbalanciert ist. Wir w¨ahlen das Verfahren CG-HB, weil es die speichersparendste unserer Lösungsvarianten ist und deshalb für adaptive Rechnungen, wo man an besonders feiner Auflösung interessiert ist, sich am besten eignet. Mit der Speicherersparnis geht auch die Reduzierung der Kommunikationskosten einher. Für die Versuchsrechnungen haben wir die Baumtiefe auf maximal + begrenzt; die Laufzeitergebnisse zeigt Abb. 7.7. Bei zwei Rechnern ist die Verteilung durch Algorithmus HF mit der statischen Verteilung noch identisch: Es wird nur der Wurzelknoten geteilt. Ab drei Rechnern ergeben sich dann Unterschiede: Wie schon bei früheren Untersuchungen mit Algorithmus HF [BEE98b] auf Basis des GS-HB-Verfahrens festgestellt 148 Beispielrechnungen und Leistungsmessungen Verfahren CG-HB, Beispiel HEAT2, M=20, it=10 350 statische Lastverteilung dynamische Lastverteilung (HF) 300 Dauer [sec] 250 200 150 100 50 0 1 2 3 4 5 6 7 8 9 10 Anzahl Prozessoren 11 12 13 14 15 16 Abbildung 7.7 Laufzeiten mit CG-HB für das adaptive Beispiel HEAT2 wurde, erlaubt diese Art der Lastverteilung die bessere Ausnutzung der verfügbaren Rechner. W¨ahrend die Laufzeiten bei statischer Lastverteilung mit einem zus¨atzlichen Rechner nicht oder nur geringfügig verbessert werden, wenn ein Knoten aufgeteilt wird, der nicht die größte Rechenlast verursacht hat, bekommt man mit Algorithmus HF einen kontinuierlicheren Abfall der Rechenzeiten. Denn Algorithmus HF teilt ja gerade immer den Knoten mit der größten Last auf, wenn ein zus¨atzlicher Rechner hinzugenommen wird. Die Laufzeiten zeigen aber auch eine gewisse S¨attigung“, hier etwa bei acht Rechnern. ” Dies ist auf den kritischen Pfad des Substrukturierungsbaums zurückzuführen, also desjenigen Pfades zwischen Wurzel und einem Blatt, der die größte Last verursacht und nicht weiter parallelisiert werden kann. Dieser kritische Pfad w¨achst natürlich mit der Tiefe des Baums und macht sich umso st¨arker bemerkbar, je unbalancierter der Baum ist, d. h. je größer die Lastdifferenz je zweier Teilb¨aume einer Ebene ist. Insgesamt können wir jedoch feststellen, dass sich der Einsatz dieser sehr einfachen Methode der dynamischen Lastverteilung lohnt: Bei einer Verteilung auf mindestens drei Rechner sind die Laufzeiten immer etwas niedriger als bei statischer Verteilung; in unserem Beispiel bei 3 und 7 Rechnern sogar deutlich niedriger. 149 Kapitel 8 Schlussbetrachtungen und Ausblick Ausgangspunkt für die Entwicklung der funktionalen Koordinationssprache FASAN war die Erkenntnis, dass sich funktionale Programme zum Erstellen paralleler und verteilter Anwendungen wegen des Fehlens von Seiteneffekten gut eignen, wie die Vielzahl von Forschungsarbeiten auf diesem Gebiet dokumentiert. Speziell im Bereich des technisch-wissenschaftlichen Rechnens ist man bei numerischen Simulationen, die erhebliche Berechnungszeiten erfordern, sehr an deren Parallelisierung interessiert. Zudem gewinnen hier rekursive, auf hierarchischen Datenstrukturen aufbauende Verfahren zunehmend an Bedeutung, weil sie den einfachen Übergang zu adaptiven Berechnungen ermöglichen. Allerdings ergeben sich in diesem Anwendungsbereich wegen der Größe der Datenstrukturen und der verwendeten Algorithmen besondere Anforderungen: Es genügt nicht, nur mögliche parallel ausführbare Teilberechnungen aus einem Algorithmus herauszufinden. Mindestens ebenso wichtig ist, dass die großen Datenmengen der Simulation so weit wie möglich lokal gehalten werden. Zus¨atzlich enthalten die hier üblichen, an den Dimensionsrichtungen der Problemstellungen orientierten Algorithmen oft komplexe Datenabh¨angigkeiten, die für eine effiziente pipeline-artige parallele Ausführung erst umorganisiert werden müssen. Die Konzeption von FASAN greift genau diese Forderungen auf. Sie ermöglicht mit den bekannten Techniken funktionaler Programmierung — Einfach-Zuweisungen, Rekursion, hierarchische Datenstrukturen und Funktionen höherer Ordnung — die Formulierung der Koordinationsschicht für diese Algorithmen. Dabei wird dem Programmierer oder auch einem Lastbalancierungssystem die Möglichkeit gelassen, das grobe parallele Ablaufverhalten zu bestimmen. Die Feinheiten der verteilten Ausführung — Datenlokalit¨at, direkte Kommunikation und Abschw¨achung von Synchronisationspunkten —, die für die Effizienz entscheidend sind, werden von FASAN automatisch organisiert. Die beiden wesentlichen Beitr¨age dieser Arbeit zu dem schon l¨anger existierenden FASAN-Konzept (z. B. [EP96, Pfa97]) sind die Entwicklung einer parallelen Semantik und die neue programmierfreundliche Implementierung in Java. Zur Angabe einer parallelen operationellen Semantik haben wir Datenflussgraphen, ei- 150 Schlussbetrachtungen und Ausblick ne besondere Art von Prozessnetzen, verwendet. Mit ihrer Hilfe konnte das KabelbaumPrinzip der hierarchischen Datenstrukturen in FASAN anschaulich dargestellt werden. Außerdem wurden auf recht einfache Weise wesentliche Eigenschaften der Programme gezeigt: FASAN-Programme sind verklemmungsfrei und enthalten keine Laufbedingungen. Darüber hinaus wurden auch die geforderten Effizienz-Aspekte der hierarchischen Datenstrukturen — Datenlokalit¨at, direkte Kommunikation, verringerte Synchronisation und höherer Parallelit¨atsgrad — nachgewiesen. Diese Eigenschaften können durch explizite prozedurale parallele Programmierung oft nicht oder nur sehr mühsam erreicht werden. Für die Implementierung haben wir uns für die Sprache Java entschieden, weil sie einige Konzepte vereint, die in dieser Zusammenstellung sonst nicht verfügbar sind. Außerdem stellt Java einen guten Mittelweg zwischen effizienter Ausführung und Portabilit¨at dar. Bei der Übersetzung eines FASAN-Programms sind wir stufenweise vorgegangen: Ausgehend von einem sequentiellen Grundprogramm haben wir Erweiterungen eingefügt, mit deren Hilfe das Thread-System von Java genutzt werden kann. Die n¨achste Stufe brachte dann die Erg¨anzung um Mechanismen für die verteilte Ausführung auf Basis von Java-RMI sowie eine auf FASAN angepasste Implementierung globaler Referenzen und verteilter Speicherverwaltung. Den praktischen Nutzen von FASAN konnten wir mit einer größeren Anwendung aus dem Bereich der numerischen Simulation unter Beweis stellen. Wir haben das Verfahren der rekursiven Substrukturierung gew¨ahlt, weil es ein wichtiges und universelles Verfahren zur numerischen Lösung großer linearer Gleichungssysteme ist, wie sie beispielsweise bei der Modellierung technischer und physikalischer Probleme mit der Finite-ElementMethode entstehen [Sch95, Hüt96]. Für die Lösung der Gleichungssysteme haben wir in dieser Arbeit drei neue Varianten vorgeschlagen und praktisch getestet. Hauptvorteil dieser Löser ist der reduzierte Speicherbedarf, wodurch genauere Simulationen auf feineren Gittern möglich sind. Außerdem wird das Datenvolumen für die Kommunikation bei verteilter Ausführung verringert. Die neuen Verfahren konnten ohne große Schwierigkeiten sofort verteilt getestet werden, weil ein FASAN-Programm als Rahmen diente und kein zus¨atzlicher Programmieraufwand für die Parallelisierung anfiel. Mit Hilfe des Lokationskonzepts von FASAN konnten wir auch rasch einen ersten Ansatz eines dynamischen Lastverteilungsverfahren für adaptive Berechnungen integrieren. Schließlich zeigten Beispielrechnungen die Leistungsf¨ahigkeit der von FASAN erzeugten parallelen Programme. Für aufbauende Arbeiten besteht vor allem im Bereich der Lastverteilung noch Forschungsbedarf. Sehr nützlich w¨are eine Bibliothek von Parallelisierungsschemata, die entweder direkt als Lokationsfunktionen oder gekapselt in Funktionen höherer Ordnung (als Skeletons) mit FASAN programmiert werden könnten. Auch müsste die Kombination mit automatischen Lastverteilungssystemen untersucht werden. Die Java-Implementierung könnte verbessert werden, indem schnellere Verfahren zum verteilten Methodenaufruf und zur Serialisierung in die übersetzten FASAN-Programme integriert werden [Pan98]. Ohne besonders großen Aufwand könnten die übersetzten FASAN-Programme auch tole- 151 rant gegenüber dem Ausfall oder größerer Verzögerung einzelner Rechner gemacht werden. Dafür müsste ein verteilter Funktionsaufruf, der eine Fehlermeldung zurückliefert oder zu lange nicht zurückkehrt, erneut auf einem anderen Rechner erfolgen. Auf der Seite der Anwendung können weitere Lösungsmethoden für die rekursive Substrukturierung entwickelt, implementiert und mit Hilfe des FASAN-Rahmenprogramms auf einem Rechnernetz getestet werden. Dass dies in kurzer Zeit realisierbar ist, haben die drei neuen Lösungsvarianten aus dieser Arbeit gezeigt. Speziell w¨are der Einbau eines Lösungsverfahrens interessant, das so effizient und speichersparend wie der CG-Löser arbeitet, aber keine definiten Systemmatrizen voraussetzt und deshalb für allgemeinere Problemstellung einsetzbar ist. Hier k¨amen Methoden wie Bi-CG oder QMR (Quasi Minimal Residual, [BBC 94]) in Frage. 152 Schlussbetrachtungen und Ausblick Literaturverzeichnis 153 Literaturverzeichnis [AG94] G. S. A LMASI und A. G OTTLIEB: Highly Parallel Computing. Benjamin/Cummings Publishing Company, Redwood City (CA) et al., 1994. [AG98] K. A RNOLD und J. G OSLING: The Java programming language. AddisonWesley Publishing Company, Reading (MA) et. al., 2 Auflage, 1998. [AMR92] S. E. A BDULLAHI, E. E. M IRANDA und G. A. R INGWOOD: Distributed Garbage Collection. In: International Workshop on Memory Management (IWMM92), St. Malo, France., Nummer 637 in LNCS, Seiten 43–81, Berlin, September 1992. Springer Verlag. [ANP89] A RVIND, R. S. N IKHIL und K. K. P INGALI: I-Structures: Data Structures For Parallel Computing. TOPLAS, 4(11):598–632, 1989. [Bac78] J. BACKUS: Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs. Communications of the ACM, 21(8):613–640, 1978. [Bac98] M. BACKSCHAT : A Novel Economic-Based Approach to Dynamic Load Distribution on Large Heterogeneous Computer Networks. In: E. D’H OLLANDER, G. J OUBERT, F. P ETERS und U. T ROTTENBERG (Herausgeber): Parallel Computing: Fundamentals, Applications and New Directions, Band 12 der Reihe Advances in Parallel Computing, Seiten 739–742, Amsterdam, 1998. Elsevier Science Publishers. [Bar84] H. P. BARENDREGT: The Lambda Calculus. Elsevier Science Publishers, Amsterdam, 1984. [Bat90] K. BATHE: Finite-Elemente-Methoden. Springer Verlag, Berlin, 1990. [Bat96] K. BATHE: Finite element procedures. Prentice-Hall Int., Eaglewood Cliffs (NJ), 1996. [BBC 94] R. BARRETT, M. B ERRY, T. F. C HAN, J. D EMMEL, J. D ONATO, J. D ON GARRA, V. E IJKHOUT, R. P OZO, C. ROMINE und H. VAN DER VORST: Templates for the Solution of Linear Systems: Building Blocks for Iterative Methods, 2nd Edition. SIAM, Philadelphia, 1994. 154 Literaturverzeichnis [BCH 94] G. E. B LELLOCH, S. C HATTERJEE, J. C. H ARDWICK, J. S IPELSTEIN und M. Z AGHA: Implementation of a Portable Nested Data-Parallel Language. Journal of Parallel and Distributed Computing, 21(1):4–14, April 1994. [BDS92] M. B ROY, C. D ENDORFER und K. S TOLEN: HOPSA – a High-level Programming Language for Parallel Computations. interner Bericht, Technische Universit¨at München, 1992. [BEE98a] S. B ISCHOF, R. E BNER und T. E RLEBACH: Load Balancing for Problems with Good Bisectors, and Applications in Finite Element Simulations. In: Proceedings of the 4th International Euro-Par Conference on Parallel Processing, Euro-Par’98, Nummer 1470 in LNCS, Seiten 383–389, Berlin, 1998. Springer Verlag. [BEE98b] S. B ISCHOF, R. E BNER und T. E RLEBACH: Load Balancing for Problems with Good Bisectors, and Applications in Finite Element Simulations: Worst-case Analysis and Practical Results. SFB-Bericht 342/05/98, Technische Universit¨at München, 1998. [Bes95] E. B EST: Semantik: Theorie sequentieller und paralleler Programmierung. Vieweg Verlag, 1995. [BES97] H.-J. B UNGARTZ, R. E BNER und S. S CHULTE: Hierarchische Basen zur effizienten Kopplung substrukturierter Probleme der Strukturmechanik. SFB-Bericht 324/05/97 A, Technische Universit¨at München, 1997. [BG96] G. E. B LELLOCH und J. G REINER: A Provable Time and Space Efficient Implementation of NESL. In: ACM SIGPLAN International Conference on Functional Programming, Seiten 213–225, New York, Mai 1996. ACM Press. [BGG 96] M. B ROY, E. G EISBERGER , R. G ROSU, F. H UBER, B. RUMPE, B. S CH ÄTZ, A. S CHMIDT, O. S LOTOSCH und K. S PIES: Das AutoFocusBilderbuch — eine anwendungsorientierte Einführung. Technischer Bericht Technische Universit¨at München, 1996. [Bir88] B. S. B IRD: Lectures on constructive functional programming. In: M. B ROY (Herausgeber): Constructive Mehtods in Computing Science, Band 55 der Reihe NATO ASO Series F: Computer and Systems Sciences, Seiten 151–216, Berlin, 1988. Springer Verlag. [Bis98] J. B ISHOP: Java Gently. Addison-Wesley Publishing Company, Reading (MA) et. al., 2 Auflage, 1998. [BJK 96] R. B LUMOFE, C. J OERG, B. K USZMAUL, C. L EISERSON, K. R ANDALL und Y. Z HOU: Cilk: An Efficient Multithread Runtime System. The Journal of Parallel and Distributed Computing, 37(1):55–69, 1996. Literaturverzeichnis [Ble96] 155 G. E. B LELLOCH: Pragramming Parallel Algorithms. Comm. of the ACM, 3(39):85–97, 1996. [BLOMP98] S. B REITINGER, R. L OOGEN, Y. O RGA -M ALL ÉN und R. P E ÑA: Eden, Language Definition and Operational Semantics. Technischer Bericht 10(1996), Phillips-Universit¨at Marburg, 1998. [Bon95] T. B ONK: Ein rekursiver Algorithmus zur adaptiven numerischen Quadratur mehrdimensionaler Funktionen. Dissertation, Technische Universit¨at München, 1995. [BOSW98] G. B RACHA, M. O DERSKY, D. S TOUTAMIRE und P. WADLER: Making the future safe for the past: Adding Genericity to the Java Programming Language. In: The 13th annual ACM SIGPLAN Conferences on ObjectOriented Programming Systems, Languages, and Applications OOPSLA 98, Vancouver, New York, 1998. ACM Press. [BPZ96] M. BACKSCHAT, A. P FAFFINGER und C. Z ENGER: Economic Based Dynamic Load Distribution In Large Workstation Networks. In: P. E . A . F RAIGNIAUD (Herausgeber): Euro-Par’96 Parallel Processing: Proceedings of the 2nd international Euro-Par conference, Lyon, France, August 26-29, 1996, Vol. 2, Nummer 1124 in Lecture Notes in Computer Science, Seiten 631–634, Berlin, 1996. Springer Verlag. [Bra97] D. B RAESS: Finite Elemente: Theorie, schnelle Löser und Anwendungen in der Elastizitätstheorie. Springer Verlag, Berlin, 1997. 2. überarbeitete Auflage. [Bro92] M. B ROY: Informatik, eine grundlegende Einführung, Teil 1. Springer Verlag, Berlin, 1992. [BSZ99] M. BADER, M. S CHIMPER und C. Z ENGER: Hierarchical Bases for the Indefinite Helmholtz Equation. Technischer BerichtSFB 342/03/99 A, Technische Universit¨at München, 1999. [Bun92] H.-J. B UNGARTZ: Dünne Gitter und deren Anwendung bei der adaptiven Lösung der drei-dimensionalen Poisson-Gleichung. Dissertation, Technische Universit¨at München, 1992. [Bur91] D. S. B URNETT: Finite Element Analysis. Addison-Wesley Publishing Company, Reading (MA) et. al., 1991. [CFBO92] D. C ANN, J. F EO, A. B OHOEM und R. O LDEHOEFT: SISAL Reference Manual: language version 2.0. Technischer Bericht Lawrence Livermore National Laboratory, Livermore, California, 1992. [Col98] M. I. C OLE: Algorithmic Skeletons. Pitman/MIT-Press, 1998. [DGTY95] J. DARLINGTON, Y.- K . G UO, H. W. TO und J. YANG: Functional Skeletons for Parallel Coordination. In: P. F RAIGNIAUD ET AL . (Herausge- Literaturverzeichnis 156 ber): Euro-Par’95 Parallel Processing: Proceedings of the first international Euro-Par conference, Stockholm, Sweden, August 29–31, 1995, Lecture Notes in Computer Science, 966, Berlin, 1995. Springer Verlag. [Ebn94] R. E BNER: Neuimplementierung einer funktionalen Sprache zur Parallelisierung numerischer Algorithmen. Diplomarbeit, Technische Universit¨at München, 1994. [EP96] R. E BNER und A. P FAFFINGER : Transformation of Functional Programs into Data Flow Graphs Implemented with PVM. In: A. B ODE ET. AL . (Herausgeber): Proceedings of the Third Euro PVM Users’ Group Meeting, 7–9 October 1996, Munich, Berlin, 1996. Springer Verlag. [EP98] R. E BNER und A. P FAFFINGER : Higher Level Programming and Efficient Automatic Parallelization: A Functional Data Flow Approach with FASAN. In: E. D’H OLLANDER , G. J OUBERT, F. P ETERS und U. T ROTTENBERG (Herausgeber): Parallel Computing: Fundamentals, Applications and New Directions, Band 12 der Reihe Advances in Parallel Computing, Seiten 377–384, Amsterdam, 1998. Elsevier Science Publishers. [EPZ96] R. E BNER, A. P FAFFINGER und C. Z ENGER: FASAN — eine funktionale Agenten-Sprache zur Parallelisierung von Algorithmen in der Numerik. In: W. M ACKENS (Herausgeber): Software Engineering im Scientific Computing, Tagungsband zur SESC 1995, Kiel, Braunschweig, 1996. Vieweg Verlag. [Far98] J. FARLEY: Java Distributed Computing. 1998. [FMS 95] J. F EO, P. M ILLER, S. S KEDZIELEWSKI, S. D ENTON und C. S OLOMON: Sisal 90. In: A. P. W. B OHM und J. T. F EO (Herausgeber): High Performance Functional Computing, Seiten 35–47, Livermore, California, April 1995. [FOT92] I. F OSTER, R. O LSON und S. T UECKE: Productive Parallel Programming: The PCN Approach. Scientific Programming, 1(1):51–66, 1992. [FT90] I. F OSTER und S. TAYLOR: Strand, New Concepts in Parallel Programming. Prentice-Hall Int., Eaglewood Cliffs (NJ), 1990. [FT93] I. F OSTER und S. T UECKE: Parallel Programming with PCN. Technischer Bericht Argonne National Laboritory, 1993. [FT94] I. F OSTER und S. TAYLOR: A Compiler Approach to Scalable ConcurrentProgram Design. ACM TOPLAS, 3(16):577–604, 1994. [Fun97] K. F UNK: Anwendung der algebraischen Mehrgittermethode auf konvektionsdominierte Strömungen. Diplomarbeit, Technische Universit¨at München, 1997. O’Reilly, Cambridge (MA), Literaturverzeichnis 157 [GC92] D. G ELERNTER und N. C ARRIERO: Coordination languages and their significance. Comm. of the ACM, 2(35):97–107, 1992. [Geo73] A. G EORGE: Nested Dissection of a Regular Finite Element Mesh. SIAM J. Numer. Anal., 10(2), 1973. [GG98] R. G OLDMAN und R. P. G ABRIEL: Qlisp: Experience and New Directions. In: Proceedings of the ACM/SIGPLAN PPEALS 1988, New Haven, CT, Seiten 111–123, 1998. [Gil96] A. G ILL: Cheap Deforestation for Non-strict Functional Languages. Dissertation, Department of Computing Science, Glasgow University, Januar 1996. [GL78] A. G EORGE und J. L IU: An Automatic Nested Dissection Algorithm for Irregular Finite Element Problems. SIAM J. Numer. Anal., 15(5), 1978. [GL95] S. G ORLATCH und C. L ENGAUER: Parallelization of Divide-and-Conquer in the Bird-Meertens Formalism. Formal Aspects of Computing, 7(6):663– 682, 1995. [GLP93] A. J. G ILL, J. L AUNCHBURY und S. L. P EYTON J ONES: A Short Cut To Deforestation. In: A RVIND (Herausgeber): Functional Programming and Computer Architecture, Seiten 223–232, New York, Juni 1993. ACM SIGPLAN/SIGARCH, ACM Press. [Gri87] M. G RIEBEL: On the combination of the ideas of multilevel solvers using hierarchical bases and the substructuring technique for the finite element method. Technischer BerichtI1987, Technische Universit¨at München, 1987. [Gri94] M. G RIEBEL: Multilevelmethoden als Iterationsverfahren über Erzeugendensystemen. Teubner Verlag, Stuttgart, 1994. [Hac93] W. H ACKBUSCH: Iterative Lösung großer schwachbesetzter Gleichungssysteme. Teubner Verlag, Stuttgart, 1993. [Hal85] R. H. H ALSTEAD: MultiLisp: A Language for Concurrent Symbolic Computation. ACM TOPLAS, 4(7):501–538, 1985. [Hal90] R. H. H ALSTEAD: New Ideas in Parallel Lisp: Language Design, Implementation, and Programming Tools. In: G. G OOS und J. H ARTMA NIS (Herausgeber): Parallel Lisp: Languages and Systems, Proceedings of the U.S./Japan Workshop on Parallel Lisp, Band 441, Seiten 2–57, Berlin, 1990. Springer Verlag. [Ham94] K. H AMMOND: Parallel Functional Programming: An Introduction. Technischer Bericht University of Glasgow, 1994. [HF93] P. H UDAK und J. H. FASEL: A Gentle Introduction to Haskell. Technischer Bericht Yale University and Los Alamos National Laboritories, 1993. 158 Literaturverzeichnis [HG89] R. H ERRTWICH und H. G.: Kooperation und Konkurrenz: nebenl äufige, verteilte und echtzeitabhängige Programmsysteme. Springer Verlag, Berlin, 1989. [Hil93] J. M. D. H ILL: Data Parallel Haskell: Mixing old and new glue. Technischer Bericht University of London, 1993. [HL92] P. H AMMERLUND und B. L ISPER: On the Relation between Functional and Data Parallel Programming Languages. Technischer Bericht Royal Institute of Technology, Stockholm, 1992. [HLPT98] K. H AMMOND, H.-W. L OIDL, S. P EYTON J ONES und P. W. T RINDER: Algorithm + Strategy = Parallelism. Journal of Functional Programming JFP, 8(1), 1998. [HS94] R. H ÜTTL und M. S CHNEIDER : Parallel Adaptive Numerical Simulation. SFB-Bericht 342/01/94 A, Technische Universit¨at München, 1994. [HS97a] U. H INKEL und K. S PIES: Spezifikationsmethodik für mobile, dynamische FOCUS-Netze. In: A. W OLISZ, I. S CHIEFERDECKER und A. R EN NOCH (Herausgeber): Formale Beschreibungstechniken f ür verteilte Systeme, GI/ITG-Fachgespräch 1997, St. Augustin, 1997. GMD Verlag. [HS97b] F. H UBER und B. S CH ÄTZ: Rapid Prototyping with AutoFocus. In: A. W O LISZ, I. S CHIEFERDECKER und A. R ENNOCH (Herausgeber): Formale Beschreibungstechniken für verteilte Systeme, GI/ITG-Fachgespräch 1997, Seiten 343–352, St. Augustin, 1997. GMD Verlag. [HTI99] Z. H U, M. T KEICHI und H. I WASAKI: Diffusion: Calculating Efficient Parallel Programs. In: Proceedings of the ACM SIGPLAN Workshop on Partial Evaluation and Semantics-Based Program Manipulation PEPM’99, San Antonio, Texas, January 22–23, 1999, New York, 1999. ACM Press. [Hud88] P. H UDAK: Exploring Para-Functional Programming: Separating the what from the how. IEEE Software, 1(5):54–61, 1988. [Hud91] P. H UDAK: Para-Functional Programming in Haskell, Seiten 159–196. Addison-Wesley Publishing Company, Reading (MA) et. al., 1991. [Hüt96] R. HÜTTL: Ein iteratives Lösungsverfahren bei der Finite-ElementMethode unter Verwendung von rekursiver Substrukturierung und hierarchischen Basen. Dissertation, Technische Universit¨at München, 1996. [Jen97] K. J ENSEN: Coloured petri nets: basic concepts, analysis methods and results, Band 1–3. Springer Verlag, Berlin, 1997. [JH93] M. J ONES und P. H UDAK: Implicit and Explicit Parallel Programming in Haskell. Technischer Bericht YALEU/DCS/RR-982, Department of Computer Science, Yale University, August 1993. Literaturverzeichnis 159 [JL91] M. J UNG und U. L ANGER: Applications of Multilevel Methods to Practical Problems. Surv. Math. Ind., 1:217–257, 1991. [Joe96] C. F. J OERG: The Cilk System for Parallel Multithreaded Computing. Dissertation, Department of Electrical Engineering and Computer Science, Massachusetts Institute of Technology, 1996. [KCR98] R. K ELSEY, W. C LINGER und J. R EES: Revised Report on the Algorithmic Language Scheme. Technischer Bericht Computer Science Department, Cornell University, 1998. [Kel89] P. H. J. K ELLY: Functional Programming for Loosely-Coupled Multiprocessors. Pitman/MIT Press, 1989. [KPS94] H. K UCHEN, R. P LASMEIJER und H. S TOLTZE: Distributed Implementation of a Data Parallel Functional Language. In: Parallel Architectures & Languages Europe, Seiten 464–477. LNCS 817, Springer Verlag, 1994. [Kru93] C. D. K RUMVIEDA: Distributed ML: Abstractions for Efficient and FaultTolerant Programming. Technical report TR93-1376, Cornell University, Computer Science Department, August 1993. [Leb98] M. L EBERECHT : An Efficient Runtime System Combining Dataflow, Multithreading, and Distributed Shared Memory. Dissertation, Technische Universit¨at München, 1998. [LT97] H.-W. L OIDL und P. W. T RINDER: Engineering Large Parallel Functional Programs. In: Implementation of Functional Languages, St. Andrews, Scotland, September 1997, LNCS, Berlin, 1997. Springer Verlag. [Mae96] J. W. M AESSEN: pH – a parallel Haskell. Technischer Bericht MIT, 1996. [MH91] E. M OHR und R. H. H ALSTEAD: Lazy Task Creation – a Technique for Increasing the Granularity of Parallel Programs. IEEE Transactions on Parallel and Distributed Systems, 2(3):264–280, Juli 1991. [MS96] M. M ÜLLER und G. S MOLKA: Oz: Nebenläufige Programmierung mit Constraints. KI-Künstliche Intelligenz, Themenheft Logische Programmierung, Seiten 55–61, 1996. [Nef99] J. N EFFINGER: The Volano Report: Which Java platform is fastest, most scalable? JavaWorld, M¨arz 1999. [Nik90] R. S. N IKHIL: ID Version 90.0 Reference Manual. Technischer Bericht Computation Structures Group Memo 284-1, Laboratory for Computer Science, Massachusetts Institute of Technology, Juli 1990. [NSvP93] E. N ÖCKER, S. S METSERS, M. VAN E EKELEN und R. P LASMEIJER : Concurrent Clean. Technischer Bericht University of Nijmegen, 1993. [OW97a] S. OAKS und H. W ONG: Java Threads. O’Reilly , Cambridge, 1997. 160 Literaturverzeichnis [OW97b] M. O DERSKY und P. WADLER: Pizza into Java: translating theory into practice. In: POPL ’97, proceedings of the 24th ACM SIGPLAN-SIGACT symposium on Principles of programming languages, Seiten 146–159, New York, 1997. ACM Press. [Pan98] J. G. F. PANEL: Java Grande Forum Report: Making Java Work for HighEnd Computing. Technischer Bericht SC 98, Orlando, Florida, 1998. [Pau91] L. C. PAULSON: ML for the Working Programmer. Cambridge University Press, Cambridge (MA), 1991. [Pey89] S. L. P EYTON J ONES: Parallel Implementations of Functional Programming Languages. The Computer Journal, 32(2):175–186, 1989. [Pfa97] A. P FAFFINGER: Funktionale Beschreibung numerischer Algorithmen auf dünnen Gittern. Dissertation, Technische Universit¨at München, 1997. [PH97] J. P ETERSON und K. H AMMOND: Report on the programming language Haskell: a non-strict, purely functional language, version 1.4. Technischer Bericht Yale University, 1997. [PT94] G. M. PAPADOPOULOS und K. R. T RAUB: Multithreading: A Revisionist View of Dataflow Architectures. CSG Memo 330, Laboratory for Computer Science, Computation Structures Group, MIT, 1994. [Qui94] D. Q UINN: Parallel Computing — Theory and Practice. McGraw Hill, Hamburg, 1994. [Rau92] H. R AUSCHMAYER : Werkzeuge zur Bearbeitung funktionaler Programme auf Parallelrechnern. Diplomarbeit, Technische Universit¨at München, 1992. [Rei84] J. R EID: TREESOLVE, a Fortran Package for Solving Large Sets of Linear Finite-Element Equations. In: B. E NGQUIST und T. S MEDSASS (Herausgeber): Proceedings of the IFIP TC 2 Working Conference on PDE Software: Modules, Interfaces and Systems, Söderköping, Sweden, 22–26 August, 1983, Amsterdam, 1984. North Holland. [Rei85] W. R EISIG: Systementwurf mit Netzen. Springer Compass. Springer Verlag, Berlin, 1985. [Rei98] W. R EISIG: Elements of distributed algorithms: modeling and analysis with Petri Nets. Springer Verlag, Berlin, 1998. [Rep91] J. H. R EPPY: CML: a Higher-Order Concurrent Language. In: Proc. PLDI’91, Toronto, Canada, June 26–28, 1991, Seiten 293–305, 1991. [Rep98] J. H. R EPPY: Concurrent Programming in ML. Cambrige University Press, 1998. Literaturverzeichnis 161 [Rou97] M. ROULO: Java’s three types of portability. Technischer Bericht Web Publishing Inc., 1997. [RW91] B. ROSENSTENGEL und U. W INAND: Petri-Netze: eine anwendungsorientierte Einführung. Vieweg Verlag, Braunschweig, 1991. [Saa96] Y. S AAD: Iterative methods for sparse linear systems. PWS Publishing Company, Boston et. al., 1996. [SBG96] B. E. S MITH, P. E. B JØRSTAD und W. D. G ROPP: Domain Decomposition: Parallel Multilevel Methods for Elliptic Partial Differential Equations. Cambridge University Press, Cambridge UK., 1996. [Sch95] M. S CHNEIDER : Verteilte adaptive numerische Simulation auf der Basis der Finite-Elemente-Methode. Dissertation, Technische Universit¨at München, 1995. [Sch98] B. S CH ÄTZ: Ein methodischer Übergang von asynchron zu synchron kommunizierenden Systemen. Dissertation, Technische Universit¨at München, 1998. [SG86] H. S TOYAN und G. G ÖRZ: LISP, eine Einführung in die Programmierung. Springer Verlag, Berlin, 1986. [SHW93] G. S MOLKA, M. H ENZ und J. W ÜRTZ: Object-Oriented Concurrent Constraint Programming in Oz. Technischer BerichtRR-93-16, DFKI Saarbrücken, 1993. [Ski95] D. B. S KILLIKORN: Parallel Implementation of Tree Skeletons. Technischer Bericht Queen’s University, Kingston, Ontario, 1995. [SKL90] V. A. S ARASWAT, K . K AHN und J. L EVY: Janus: A step towards distributed constraint programming. In: Proc. of the North American Conference on Logic Programming, Austin, Texas, 1990. [SRP90] V. A. S ARASWAT, M. R INARD und P. PANANGADEN: Semantic foundations of concurrent constraint programming. Technischer Bericht ACM Press, New York, 1990. [SS95] B. S CH ÄTZ und K. S PIES: Formale Syntax der logischen Kernsprache der FOCUS-Entwicklungsmethodik. Technischer BerichtTUM-I6529, Technische Universit¨at München, 1995. [Stö89] J. S TÖR: Numerische Mathematik. Springer Verlag, Berlin, 1989. [Sun98] S UN MICROSYSTEMS I NC .: Java 2 Platform API Specification, 1998. [Sun99] S UN MICROSYSTEMS I NC .: Java HotSpot Performance Engine, 1999. [Sup98] S UPERCOMPUTING T ECHNOLOGY G ROUP, MIT: Cilk 5.2 Reference Manual. Technischer Bericht Massachusetts Institute of Technology, 1998. 162 Literaturverzeichnis [Thi94] P. T HIEMANN: Grundlagen der funktionalen Programmierung. Teubner Verlag, Stuttgart, 1994. [THM 96] P. W. T RINDER, K. H AMMOND, J. S. M ATTSON J R ., A. S. PARTRIDGE und S. L. P EYTON J ONES: GUM: a portable implementation of Haskell. In: Proceedings of Programming Language Design and Implementation, Philadephia, USA, Mai 1996. [Tho96] S. T HOMPSON: Haskell: The Craft of Functional Programming. AddisonWesley Publishing Company, Reading (MA) et. al., 1996. [Tur79] D. A. T URNER: A New Implementation Technique for Applicative Languages. Software–Practice and Experience, 9:31–49, 1979. [Vei90] J. V EIT: Entwurf und Implementierung eines Sprachkonzepts zur parallelen Konfiguration numerischer Aufgabenstellungen. Diplomarbeit, Technische Universit¨at München, 1990. [Vin96] S. V INOSKI: CORBA: Integrating Diverse Applications Within Distributed Heterogeneous Environments. Technischer Bericht IONA Technologies, Inc., Cambridge, MA, 1996. [VvM 98] R. V ELDEMA, R. VAN N IEUWPOORT, J. M AASSEN, H. E. BAAL und A. P LAAT: Efficient Remote Method Invocation. Technischer Bericht Dept. of Computer Science, Vrjie Universiteit Amsterdam, 1998. [Wad90] P. WADLER: Deforestation: transforming programs to eliminate trees. In: Special issue of selected papers from 2nd European Symposium on Programming, Band 73 der Reihe Theoretical Computer Science, Seiten 231– 248, 1990. [Wes92] M. D. W ESTHEAD: Linda and the paradigms of parallelisation. DAI research paper 744, University of Edinburgh, 1992. [Yse86] H. Y SERENTANT: On the multi-level splitting of finite element spaces. Numerische Mathematik, 49(3):379–412, 1986. [Zen90] C. Z ENGER: Sparse Grids. SFB-Bericht 342/18/90, Technische Universit¨at München, 1990. [Zen92] C. Z ENGER: Funktionale Programmierung paralleler Algorithmen f ür numerische und technisch-wissenschaftliche Anwendungen. In: R. R. G. BA DER , G. W ITTUM (Herausgeber): GAMM-Seminar über numerische Algorithmen auf Transputer-Systemen, Heidelberg, 31.5.–1.6. 1991, TeubnerSkripten zur Numerik, Stuttgart, 1992. Teubner Verlag. Index 163 Index " (Folgemarkierungen) . . . . . . . . . . . . 23 (Sequenzen-Konkatenation) . . . . . . . 19 (Konkatenation) . . . . . . . . . . . . . . . . . 40 (leere Sequenz) . . . . . . . . . . . . . . . . . 19 [] (optionaler Teil) . . . . . . . . . . . . . . . . 28 [] (ein- oder mehrmalige Wiederholung) . . . . . . . . . . . . . . . . . . . . . 28 [] (null- oder mehrmalige Wiederholung) . . . . . . . . . . . . . . . . . . . . . 28 !" . . . . . . . . . siehe Assemblierungsmatrix . . . . . . . . . . . . . . . . . siehe Tr¨agermenge (Interpretation) . . . . . . . . . . . . . . . . . . 20 . . . . . . . . . . . . . siehe -Algebra (transponierte Matrix) . . . . . . . . . 106 (Sequenz mit Elementen aus A) . . 19 abh¨angige Transitionen . . . . . . . . . . . . 24 Abh¨angigkeitsbaum . . . . . . . . . . . . . . . 99 Abschluss . . . . . . . . . . . . . . . . . . 37, 43, 48 Übersetzung in Java . . . . . . . . . . . . 83 Abschluss-Objekt . . . . . . . . . . . . . . . . . 84 abstrakte Syntax . . . . . . . . . . . . . . . . . . . 39 adaptive Verfeinerung . . . . . . . . 143, 147 aktivieren . . . . . . . . . . . . . . . . . . . . . . . . . 23 algorithm. Skeletons . . . siehe Skeletons applikative Sprachen . . . . . . . . . . . . . . . . 1 ARESO . . . . . . . . . . . . . . . . . . . . . . . . . 123 Argument (eines Funktionsknotens) . 48 Array . . . . . . . . . . . . . . . . . . . . . . . . . 28, 75 Assemblierung Kosten . . . . . . . . . . . . . . . . . . . . . . 129 Assemblierungsmatrix . . . . . . . . . . . . 106 Ausdruck . . . . . . . . . . . . . . . . . . . . . . 18, 40 Ausführung (eines Netzes) parallel . . . . . . . . . . . . . . . . . . . . . . . 25 sequentiell . . . . . . . . . . . . . . . . . . . . 23 Ausg¨ange (eines Datenflussgraphen) 49 & * Auswerten . . . . . . . . . . . . . . . . . siehe - eines Abschlusses . . . . . . . . . . . . . 56 eines Funktionsknotens . . . . . . . . . 55 Auswertungsstrategie . . . . . . . . . . . . . . 17 , . . . . . . . . . . . . . . . . . . . . siehe Belegung . . . . . . . . . . siehe Knotenpunktbasis . . . . . . . . . . . . . . . . . . . . siehe Lastvektor bedingter Ausdruck . . . . . . . . . . . . . . . . 59 Belegung einer FASAN-Variablen . . . . . . . . 31 eines Datenflussgraphen . . . . . . . . 48 Bird-Meertens-Formalismus . . . . . . . . 83 BNF-Notation . . . . . . . . . . . . . . . . . . . . . 28 . . . . siehe Vorkonditionierungsmatrix call by reference . . . . . . . . . . . . . . . . . . . 91 call by value . . . . . . . . . . . . . . . . . . . 17, 91 CG-HB . . . . . . . . . . . siehe CG-Verfahren CG-Verfahren . . . . . . . . . . . . . . . . . . . . 122 Grundalgorithmus . . . . . . . . . . . . 121 Kosten . . . . . . . . . . . . . . . . . . . . . . 130 Cholesky-Verfahren . . . . . . . . . . . . . . 113 . . . . . . . . . . . . . . siehe Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . 39, 78 CORBA . . . . . . . . . . . . . . . . . . . . . . . . . . 77 create . . . . . . . . . . . . . . . . . . . . . . . . . . 82 Currying . . . . . . . . . . . . . . . . . . . . . . 37, 83 (Flussrelation) . . . . . . . . . . . . . . . . . . . 22 (transitive Hülle von ) . . . . . . . . . . 22 Datenabh¨angigkeit . . . . . . . . . . . . . . 3, 27 Datenelement . . . . . . . . . . . 23, 25, 48, 53 Datenfluss-Sprachen . . . . . . . . . . . . . . . . 7 Datenflussgraph . . . . . . . . . . . . . . . . 22, 48 einer bedingten Zuweisung . . . . . 51 einer rekursiven Funktion . . . . . . . 50 eines FASAN-Programms siehe ('& # Index 164 eines Konstruktors . . . . . . . . . . . . . 50 eines Selektors . . . . . . . . . . . . . . . . 50 parallel . . . . . . . . . . . . . . . . . . . . . . . 54 Datenlokalit¨at . . . . . . . . . . . . . . . . . . . . . 65 Datenparallelismus . . . . . . . . . . . . . . . . . 8 Datenstruktur adaptiv . . . . . . . . . . . . . . . 11, 99, 103 regul¨ar . . . . . . . . . . . . . . . . . . . . . . . . . 5 Debugging . . . . . . . . . . . . . . . . . . . . 72, 74 Dehierarchisierung . . . . . . . . . . . . . . . 108 deklarative Sprachen . . . . . . . . . . . . . . . . 1 denotationelle Semantik . . . . . . . . . . . . 20 Differentialgleichung . . . . . . . . . . . . . . 99 direkte Kommunikation . . . . . . . . . . . . 65 Diskretisierung . . . . . . . . . . . . . . 105, 119 Fehler . . . . . . . . . . . . . . . . . . . . . 5, 137 . . . . . . . . . . siehe Entkopplungsmatrix (leere Belegung) . . . . . . . . . . . . . . 48, 53 eager evaluation . . . . . . . . . . . . . . . . . . . 17 ebene Elastizit¨at . . . . . . . . . . . . . . . . . . 137 Eing¨ange (eines Datenflussgraphen) . 49 Elimination starker Kopplungen . . . . . . . . . . . 117 von Freiheitsgraden . . . . . . 112, 117 endliche Sequenz . . . . . . . . . . . . . . . . . . 19 Endrekursion . . . . . . . . . . . . . . . . . . 33, 79 Entfalten . . . . . . . . . . . . . . . siehe eines -Knotens . . . . . . . . . . . 57 eines Funktionsknotens . . . . . . . . . 58 eines Konstruktorknotens . . . . . . . 60 eines parallelen Knotens . . . . . . . . 63 eines Selektorknotens . . . . . . . . . . 61 Entkopplungsmatrix . . . . . . . . . . . . . . 111 Ergebnis (eines Funktionsknotens) . . 48 erreichbar . . . . . . . . . . . . . . . . . . . . . . . . . 24 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Erzeuger (eines Puffers) . . . . . . . . . . . . 48 - . . . . . . . . . . . . . . . . . . . . . . . . . . 55–57 explizite Parallelisierung . . . . . . . . . . . 10 externe Funktion . . . . . . . . . . . . . . . . . . 29 externer Typ . . . . . . . . . . . . . . . . . . . . . . 30 ('! * & * Füllgrad . . . . . . . . . . . . . . . . . . . . . . . . . 140 Factory-Methode . . . . . . . . . . . . . . . . . . 82 Fallunterscheidung . . . . . . . . . . . . . . . . 79 FASAN . . . . . . . . . . . . . . . 11, 22, 27, 123 Abbildung auf abstrakte Syntax siehe Abbildung auf Datenflussgraphen siehe abstrakte Syntax . . . . . . . . . . . 39–46 Datenflussgraph . . . . . . . . . . . . 48–49 operationelle Semantik . . . . . . . . . 47 Sprachbeschreibung . . . . . . . . 27–39 FASAN-Server . . . . . . . . . . . . . . . . 78, 88 Fehlersch¨atzer . . . . . . . . . . . . . . . . . . . 100 feuern . . . . . . . . . . . . . . . . . . . . . . . . . 23, 54 Fill-in . . . . . . . . . . . . . . . . . . . . . . 112, 114 Finite-Element-Methode . . 99, 105, 138 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 FOCUS . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Folgemarkierung . . . . . . . . . . . . . . . . . . 23 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 FRAME . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 Instantiierung . . . . . . . . . . . . . . . . . 83 Freiheitsgrad . . . . . . . . . . . . . . . . . . . . . 101 extern . . . . . . . . . . . . . . . . . . . . . . . 104 intern . . . . . . . . . . . . . . . . . . . . . . . . 104 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 Funktion extern . . . . . . . . . . . . . . . . . . . . . 29, 82 höherer Ordnung . . . . . . 16, 38, 126 rekursiv . . . . . . . . . . . . . . . . . . . . . . . 31 funktionale Sprachen . . . . . . . . . . . . . . . . 1 Kennzeichen . . . . . . . . . . . . . . . . . . 15 Funktionsaufruf . . . . . . . . . 17, 31, 40, 50 parallel . . . . . . . . . . . . . . . . . . . . 34, 52 Funktionsknoten . . . . . . . . . . . . . . . . . . 48 argumentlos . . . . . . . . . . . . . . . . . . . 56 parallel . . . . . . . . . . . . . . . . . . . . . . . 52 Funktionstyp . . . . . . . . . . . . . . . . . . . . . . 18 # ! (&# & / (ausgelassenes Argument) . . . . . 48, 53 Gauß-Seidel-Schritt . . . . . . . . . . . . . . 115 Gauß-Seidel-Verfahren . . . . . . . . . . . . 114 Kosten . . . . . . . . . . . . . . . . . . . . . . 130 mit partieller Elimination . . . . . . 118 Gebietszerlegung . . . . . . . . . . . . . 99, 100 Index Verfeinerung . . . . . . . . . . . . . . . . . 102 Gent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Gleitpunktoperation . . . . . . . . . . 128, 129 globale Referenz . . . . . . . . . . . . . . . 76, 91 GlobalRef . . . . . . . . . . . . . . . . . . 92–96 Granularit¨at . . . . . . . . . . . . . . . . . . . . . 6, 27 Grundterm . . . . . . . . . . . . . . . . . . . . . . . . 18 Grundtyp . . . . . . . . . . . . . . . . . . . . . . . . . 30 GS-HB . . . siehe Gauß-Seidel-Verfahren GS-HB-E . siehe Gauß-Seidel-Verfahren mit partieller Elimination . . . . . . . . . . siehe hierarchische Basis h¨angende Punkte . . . . . . . . . . . . . . . . . 104 Entkopplung . . . . . . . . . . . . . 110, 115 hashCode . . . . . . . . . . . . . . . . . . . . . . . 92 HEAT1 (Testbeispiel) . . . . . . . . . . . . . 137 HEAT2 (Testbeispiel) . . . . . . . . . . . . . 137 HF-Algorithmus . . . . . . . . . . . . . . . . . 131 hierarchische Basis . . . . . . . . . . . . . . . 107 hierarchischer Datentyp . . . . . 32, 44, 59 Übersetzung in Java . . . . . . . . . . . . 86 verteilte Ausführung . . . . . . . . . . . 91 hierarchischer Nachbarpunkt . . . . . . 108 hierarchischer Überschuss . . . . . . . . . 108 Hierarchisierung . . . . . . . . . . . . . 106, 108 Kosten . . . . . . . . . . . . . . . . . . . . . . 129 Hierarchisierungstabelle . . . . . . . . . . 109 Hostsprache . . . . . . . . . . . . . . . . . . . . 9, 29 IDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 IIOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 implizite Parallelisierung . . . . . . . . . . . . 7 inkompatible Punkte . . . siehe h¨angende Punkte Interpretation . . . . . . . . . . . . . . . . . . . . . 20 Jacobi-Schritt . . . . . . . . . . . . . . . . . . . . 118 Jacobi-Verfahren . . . . . . . . . . . . . . . . . 120 Kosten . . . . . . . . . . . . . . . . . . . . . . 130 Java . . . . . . . . . . . . . . . . . . 16, 29, 73, 144 Java Grande Forum Panel . . . . . . . 74, 78 Java Native Interface . . . . . . . . . . . 29, 74 JavaSpaces . . . . . . . . . . . . . . . . . . . . . . . . 77 J-HB . . . . . . . . . . siehe Jacobi-Verfahren 165 JNI . . . . . . . . siehe Java Native Interface . . . . . . . . . . . . . . . . siehe Systemmatrix Kabelbaum . . . . . . . . . . . . . . . . . . . . . . . 59 Prinzip . . . . . . . . . . . . . . . . . . . . 12, 39 Knotengleichungssystem . . . . . . . . . . 101 Knotenpunktbasis . . . . . . . . . . . . . . . . 107 Kommentare (in FASAN) . . . . . . . . . . 29 Kommunikation . . . . . . . . . . . . . . . . . . . . 5 asynchron . . . . . . . . . . . . . . . . . . . . . 64 direkt . . . . . . . . . . . . . . . . . . . . . . . . . 65 konfliktfrei . . . . . . . . . . . . . . . . . . . . . . . . 24 Konjugierte-Gradienten-Verfahren siehe CG-Verfahren Konstanten (in FASAN) . . . . . . . . . . . . 28 Konstruktor . . . . . . . . . . . . . . . . . . . . . . . 32 Übersetzung in Java . . . . . . . . . . . . 86 Definition in FASAN . . . . . . . . . . . 44 Kontrollfluss . . . . . . . . . . . . . 6, 34, 74, 85 Konvergenz . . . . . . . . . . . . . . . . . . . . . . 106 Konvergenzrate . . . . . . . . . . . . . . . . . . 139 Koordinationssprache . . . . . . . . . . . . . 7, 9 Kostenabsch¨atzung . . . . . . . . . . . . . . . 129 Kragscheibe . . . . . . . . . . . . . . . . . 100, 137 kritischer Pfad . . . . . . . . . . . . . . . . . . . 148 & . . . . . . . . . . . . . . . . . . . . . siehe Lokation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Lösungsvektor . . . . . . . . . . . . . . . . . . . 104 Transformation . . . . . . . . . . . . . . . 112 Lastvektor . . . . . . . . . . . . . . . . . . . . . . . 104 Transformation . . . . . . . . . . . . . . . 112 Lastverteilung . . . . . . . . . . . . . . . . . . . . . 12 durch Lokationen . . . . . . . . . . . . . . 36 dynamisch . . . . . . . . . . . . . . . . 35, 131 statisch . . . . . . . . . . . . . . . . . . . . . . 127 Laufbedingung . . . . . . . . . . . . . . . . . . 6, 66 Ausschluss . . . . . . . . . . . . . . . . . . . . 66 lazy evaluation . . . . . . . . . . . . . . . . . . . . 17 lenient evaluation . . . . . . . . . . . . . . . . . . . 7 Linienbaum . . . . . . . . . . . . . . . . . . . . . . 102 & . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Lokation . . . . . . . . . . . . . 34, 54, 127, 133 im Datenflussgraphen . . . . . . . . . . 52 Lokationsfunktion . . . . . . siehe Lokation Index 166 . . . . . . . . . . . . . . . . . siehe Markierung Markierung . . . . . . . . . . . . . . . . . . . . . . . 23 eines Datenflussgraphen . . . . . . . . 49 Matrix dünnbesiedelt . . . . . . . . . . . . . . . . 114 Reihenoperation . . . . . . . . . . . . . . 110 Matrixmultiplikation Kosten . . . . . . . . . . . . . . . . . . . . . . 129 Methodenaufruf . . . . . . . . . . . . . . . . . . . 29 remote . . . . . . . . . . . . . . . . . . . . . . . . 74 Monitor . . . . . . . . . . . . . . . . . . . . . . 87, 145 MPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 (Matrix-Reihenoperation) . . . . . 110 Nachbereich . . . . . . . . . . . . . . . . . . . . . . 22 natürliche Semantik . . . . . . . . . . . . . . . . 20 n-beschr¨ankt . . . . . . . . . . . . . . . . . . . . . . 23 nested dissection . . . . . . . . . . . . . . . . . 112 . . . . . . . . . . . . . . . . . . . . . . . . . . . 49–53 Netz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 nicht-strikt . . . . . . . . . . . . . . . . . . . . . . . . 21 Nonterminal . . . . . . . . . . . . . . . . . . . . . . 28 (&# (Relaxationsfaktor) . . . . . . . . . . . . . 118 Objektorientierung . . . . . . . . . . . . . . . . . . 2 operationelle Semantik . . . . . . . . . 20, 47 (Potenzmenge) . . . . . . . . . . . . . . . . . . 19 PANEL (Testbeispiel) . . . . . . . . . . . . . 138 para-funktionale Programmierung . . . . 9 parallele Transitionen . . . . . . . . . . . . . . 24 Parallelisierung explizit . . . . . . . . . . . . . . . . . . . . . . . 10 implizit . . . . . . . . . . . . . . . . . . . . . . . . 7 Parallelit¨atsgrad . . . . . . . . . . . . . . . . . . . 66 partielle Elimination . . . . . . . . . . . . . . 112 passieren . . . . . . . . . . . . . . . . . . . . . . . . . 65 Performance-Garantie . . . . . . . . . . . . 132 Petri-Netz . . . . . . . . . . . . . . . . . . . . . . . . 22 Pfad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Pizza . . . . . . . . . . . . . . . . . . . 16, 17, 30, 83 Platz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 POISSON (Testbeispiel) . . . . . . . . . . 136 Poisson-Gleichung . . . . . . . . . . . . . . . 136 Polymorphismus . . . . . . . . . . . . . . . 15, 30 Portabilit¨at . . . . . . . . . . . . . . . . . . . . 12, 73 Pr¨adikat . . . . . . . . . . . . . . . . . . . . . . . 32, 95 Übersetzung in Java . . . . . . . . . . . . 86 Definition in FASAN . . . . . . . . . . . 44 prozedurale Sprachen . . . . . . . . . . . . . . . 1 Prozessnetz . . . . . . . . . . . . . . . . . . . . 24, 47 Puffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 PVM . . . . . . . . . . . . . . . . . . . . . . . . 72, 100 . . . . . . . . . . . . . . . siehe Konvergenzrate . . . . . . . . . . . . . . . . . . . . . siehe Residuum race condition . . . . siehe Laufbedingung Randbedingung Dirichlet . . . . . . . . . . . . . . . . 101, 104 . . . . . . . . . . . . . . . . . . . . . . . . . . . 39, 78 rechenbereit . . . . . . . . . . . . . . . . . . . . . . . 54 Rechnergrenze . . . . . . . . . . . . . . . . . . . . 64 recycle . . . . . . . . . . . . . . . . . . . . . 33, 79 referentielle Transparenz . . . . . . . . 15, 91 Referenztabelle . . . . . . . . . . . . . . . . . . . 92 Eintrag . . . . . . . . . . . . . . . . . . . . . . . 95 Referenzz¨ahler . . . . . . . . . . . . . . . . . 69, 95 Reihenoperation . . . . . . . . . . . . . 110, 117 rekursive Funktion . . . . . . . . . . . . . . . . . 31 rekursive Programmdefinition . . . . . . . 20 rekursives Programm . . . . . . . . . . . . . . 21 Relaxationsfaktor . . . . . . . . . . . . 118, 139 RemoteObject . . . . . . . . . . . . . . . . . 88 Replikationsknoten . . . . . . . . . . . . . . . . 61 Residuum . . . . . . . . . . . . . . . . . . . 118, 139 Transformation . . . . . . . . . . . . . . . 112 Ritz-Galerkin-Verfahren . . . . . . . . . . 138 RMI . . . . . . . . . . . . . . . . . . . . . . . . . . 71, 74 rmic . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 . . . . . . . . . . . . . . . . siehe Signatur -Algebra . . . . . . . . . . . . . . . . . . . . . . . . 20 schalten . . . . . . . . . . . . . . . . . . . . . . . 23, 54 Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Schnitt . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Seiteneffektfreiheit . . . . . . . . . . . . . 15, 29 . . . . . . . . . . . . . . . . . . . . . . . . . . . 39, 78 Selektor . . . . . . . . . . . . . . . . . . . . . . . 32, 95 Übersetzung in Java . . . . . . . . . . . . 86 Index Definition in FASAN . . . . . . . . . . . 44 sequentielle Semantik . . . . . . . . . . . . . . 23 Sequenz . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Serialisierung . . . . . . . . . . . . . . . . . . 74, 75 Signatur . . . . . . . . . . . . . . . . . . . . . . . 18, 39 algorithmisch . . . . . . . . . . . . . . . . . 18 Skalarprodukt Kosten . . . . . . . . . . . . . . . . . . . . . . 129 Skeleton . . . . . . . . . . . . . . . . . . . . 9, 38, 83 Java-Klasse . . . . . . . . . . . . . . . . . . . 89 Socket Factory . . . . . . . . . . . . . . . . . . . . 76 Speicherverwaltung . . . . . . . . . 16, 68, 74 verteilt . . . . . . . . . . . . . . . . . . . . . . . . 92 Spracherweiterung . . . . . . . . . . . . . . . 6, 8 station¨are Warmeleitung . . . . . . . . . . 136 statische Analyse . . . . . . . . . . . . . . . . . . 10 Steifigkeitsmatrix . . . . . . . . . . . . . . . . 138 strikt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Stub . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Substrukturierung (Beschreibung) . . 102 Substrukturierungsbaum . . . . . . . . . . 101 Aufbau . . . . . . . . . . . . . . . . . . . . . . 102 balanciert . . . . . . . . . . . . . . . . . . . . 127 unbalanciert . . . . . . . . . . . . . 143, 147 Verzweigungsgrad . . . . . . . . . . . . 125 SWITCH-Methode . . . . . . . . . . . . . . . . . 91 Synchronisation . . . . . . . . . . . . . . . . . 5, 25 synchronized . . . . . . . . . . . . . . . . . 87 Systemmatrix . . . . . . . . . . . . . . . . . . . . 104 Tensorprodukt . . . . . . . . . . . . . . . . . . . 107 Term . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Thread . . . . . . . . . . . . . . . . . . . 34, 74, 145 native . . . . . . . . . . . . . . . . . . . . . . . . . 85 . . . . . . . . . . . . . . . . . . . . . . . . . 41–46 Tr¨agermenge . . . . . . . . . . . . . . . . . . . . . . 20 Transition . . . . . . . . . . . . . . . . . . . . . . . . 22 . . . . . . . . . . . . . . . . . . . . . . . . . . 39, 78 Typausdruck . . . . . . . . . . . . . . . . . . . . . . 18 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Typisierung . . . . . . . . . . . . . . . . . . . . . . . 15 . . . . . . . . . . . . . . . . siehe Lösungsvektor unabh¨angige Transitionen . . . . . . . . . . 24 . . . . . . . . . . . . . . . . . . . . . . . 55–64 # ! .( 0 (! * 167 unidirektional . . . . . . . . . . . . . . . . . . . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 Variable (in FASAN) belegen . . . . . . . . . . . . . . . . . . . . . . . 31 lesen . . . . . . . . . . . . . . . . . . . . . . . . . 31 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Verbindungsrelation . . . . . . . . . . . . . . . 22 Verbraucher (eines Puffers) . . . . . . . . . 48 Verfeinerung adaptiv . . . . . . . . . . . . . . . . . . 143, 147 gleichm¨aßig . . . . . . . . . . . . . . . . . . 140 Verklemmung . . . . . . . . . . . . . . . . . . . 6, 68 Verklemmungsfreiheit . . . . . . . . . . . . . 68 Verpackung . . . . . . . . . . . . . . . . . . . . . . . 86 Verpackungsklasse . . . . . . . . . . . . . . . . 81 verteilte Berechnung . . . . . . . . . . . . . . 147 Verzweigungsgrad . . . . . . . . . . . . . . . . 125 Vorbereich . . . . . . . . . . . . . . . . . . . . . . . . 22 Vorkonditionierung . . . . . . . . . . . . . . . 112 . . . . . . . . . . . . . . . . . siehe Term Wiederverwendung . . . . . . . . . . . . . . . . 12 0 Zielsprache . . . . . . . . . . . . . . . . . . . . . . . 73 Zusammenfügen von Teilgebieten . . 104 Zuweisung . . . . . . . . . . . . . . . . . . . . 31, 40 bedingt . . . . . . . . . . . . . . . . . . . . . . . 31