[Informatik-Fachberichte] Parallele Implementierung funktionaler Programmiersprachen Volume 232 ||...
Transcript of [Informatik-Fachberichte] Parallele Implementierung funktionaler Programmiersprachen Volume 232 ||...
Einleitung
Funktionale Programmiersprachen haben eine Reihe von interessanten Vorteilen gegeniiber imperativen Sprachen. Ein funktionales Programm entspricht im wesent lichen einer Funktion im mathematischen Sinne, die auf die Eingabewerte angewendet wird und den Funktionswert als Ausgabe liefert. Der A-Kalkiil von Church und seine Theorie bilden die mathematische Basis der funktionalen Programmierung. Auf Grund dieser mathematischen Tradition haben funktionale Programmiersprachen eine verhaltnismafiig einfache und klare Semantik, die eine wichtige Grundlage fUr Korrektheitsbeweise, Verifikation von Programmeigenschaften sowie fUr Programmtransformationen bildet. In diesem Zusammenhang ist auch die Eigenschaft der "referential transparency" zu nennen: Der Wert eines Ausdruckes hangt nur von seiner Umgebung, nicht aber vom Zeitpunkt seiner Auswertung abo In einem funktionalen Programm kann ein Teilausdruck also immer durch einen anderen Ausdruck, der denselben Wert hat, ersetzt werden. Die Semantik des Programms bleibt unverandert.
Der Programmentwurf erfolgt in einer funktionalen Sprache auf einer sehr viel h6heren Abstraktionsstufe als in einer imperativen Sprache. Auf Grund dessen sind funktionale Programme ausdrucksstarker und kiirzer als entsprechende imperative. In Bild 1 ist als Beispiel der Quicksortalgorithmus zur Sortierung einer Folge von ganzen Zahlen in PASCAL und MIRANDA * angegeben.
Die PASCAL-Prozedur erwartet die zu sortierenden Zahlen in einem Array, in dem dann in geschickter Weise Elemente vertauscht werden. Das MIRANDAProgramm arbeitet auf einer Liste von ganzen Zahlen, indem die Teillisten der Elemente, die gr6fier bzw. kleiner als das erste Element der Liste sind, sortiert werden und in geeigneter Weise konkateniert werden.
Die besondere Ausdrucksstarke funktionaler Sprachen ist groBtenteils darauf zuriickzufUhren, daB Funktionen wie andere Werte behandelt werden, also sowohl
*Miranda ist eine funktionale Sprache, die von David Turner [Turner 85J entwickelt wurde. Das Miranda-System ist ein Warenzeichen der Firma Research Software Limited.
R. Loogen, Parallele Implementierung funktionaler Programmiersprachen© Springer-Verlag Berlin Heidelberg 1990
2
a) Standard-Quicksortalgorithmus in PASCAL:
procedure quicksort (I,r : integer); var x, i, j, tmp : integer; begin
end
ifr> 1 then begin
end
x := a[I]; i := 1; j := r+1; repeat
repeat i:=i+1 until a[ikx; repeat j:=j-1 until a[j]sx; tmp := a[j]; a[j] := ali]; ali] := tmp;
until jsi; . ali] := a[j]; a[j] := a[I]; a[I] := tmp; quicksort (1, j-1); quicksort (j+1, r);
b) Standard-Quicksortalgorithmus in MIRANDA:
quicksort [] = quicksort (x:I) =
[] quicksort (filter « x) 1) ++ [x] ++ quicksort (filter (>= x) 1)
(filter bezeichnet eine Bibliotheksfunktion, deren Definition in Bild 2 angegeben ist. ++ ist ein Infixoperator zur Listenkonkatenation. « x) und (>= x) bezeichnen einstellige Priidikate, die genau dann den Wert 'true' ergeben, wenn ihr Argument kleiner bzw. grosser als der Wert von x ist.)
Bild 1: Gegeniiberstellung eines imperativen und eines funktionalen Programms
3
als Parameter als auch als Ergebnis einer Funktionsapplikation auftreten konnen. Funktionen, bei denen Funktionen als Ein- undjoder Ausgabewerte zugelassen sind, heiBen Funktionen haherer Ordnung oder Funktionale. Sie erlauben etwa die Beschreibung allgemeiner Algorithmen oder Verfahren, die durch die Funktionsparameter an verschiedene Kontexte oder Erfordemisse angepaBt werden konnen.
Die in dem MIRANDA-Quicksortprogramm in Bild Ib verwendete Bibliotheksfunktion filter ist ein Funktional, das aus einer Liste eine Teilliste von Elementen herausfiltert, die eine Eigenschaft besitzen, die durch eine als Parameter iibergebene Testfunktion 'test' spezifiziert wird. Die MIRANDA-Spezifikation dieses Funktionals ist in Bild 2 angegeben.
filter test [] filter test (x:l)
= [] = x: (filter test 1), = filter test 1,
test x '" (test x)
('" bezeichnet die logische Negation. Die zweite Definitionsgleichung beschreibt eine Fallunterscheidung ('guarded expression') nach dem Ergebnis der Applikation der Testfunktion auf das Kopfelement der Argumentliste.)
Bild 2: Beispiel einer Funktion hoherer Ordnung
Durch Aufrufe der Funktion filter mit geeigneten Testfunktionen kann aus einer beliebigen Liste von ganzen Zahlen z.B. die Teilliste der geraden oder ungeraden Zahlen oder, wie im Quicksortprogramm, die Teilliste der Zahlen, die ober- oder unterhalb einer bestimmten Schranke liegen, erzeugt werden.
Funktionen hoherer Ordnung sind ein wichtiges Konzept zur Entwicklung modularer Programme, da sie die Definition allgemeiner, wiederverwendbarer Module ermoglichen. Wie in [Hughes 84] durch interessante Beispiele gezeigt wird, unterstiitzt auch die Verwendung von 'lazy evaluation' als Auswertungsstrategie die Modularisierung von Programmen. 'Lazy evaluation' bedeutet verzogerte Auswertung und bezieht sich auf die Auswertung der Argumente einer Funktionsapplikation. Die Auswertung eines Funktionsargumentes wird solange verzogert, bis sein Wert zur Bestimmung des Funktionsresultates benotigt wird. Dies hat den Vorteil, daB Argumente, die zur Bestimmung des Ergebnisses nicht notig sind, auch nicht ausgewertet werden. Insbesondere kann der Wert einer Funktionsapplikation somit definiert sein, obwohl der Wert eines Argumentes undefiniert ist. Auch strukturierte Datenobjekte werden nur insoweit ausgewertet, wie sie benotigt werden, so daB auch unendliche Datenstrukturen wie andere Werte behandelt werden
4
konnen. 'Lazy evaluation' erlaubt demnach die Verwendung von Teilprogrammen (Modulen), die eine unendliche Ausgabe generieren. Die Termination der Gesamtberechnung wird nicht geHihrdet, da die Auswertung der Teilprogramme durch die umgebenden Programmteile gesteuert wird. Bild 3 zeigt ein MIRANDA-Programm zur Bestimmung von Fibonacci-Zahlen.
fib i = get (genfib 1 1) i where get [] i = get (x:D 1 = get (x:l) (HI) = genfib x y
o x get Ii x: (genfib y (x+y))
Bild 3: Beispiel zur Programmierung mit unendlichen Datenstrukturen
Die in diesem Programm verwendete Funktion genfib erzeugt die unendliche Liste aller Fibonacci-Zahlen.
Alle diese Besonderheiten funktionaler Programmiersprachen erweisen sich leider als problematisch, wenn man sie von der Seite der Implementierung von Programmiersprachen auf von Neumann-Rechnern betrachtet. Imperative Sprachen sind auf die Programmierung von von Neumann-Rechnern ausgerichtet. Sie sind gewissermafien Abstraktionen dieser Computer und daher aufierst effizient auf denselben zu implementieren. Beim Entwurf funktionaler Sprachen stehen die mathematischen Eigenschaften sowie die Ausdrucksstarke der Programme im Vordergrund. Dadurch sind diese Sprachen unabhangig von Rechnerarchitekturen.
Die bisher effizienteste Implementierung einer funktionalen Sprache mit 'lazy evaluation' als Auswertungsstrategie auf einem von Neumann-Rechner erfolgte mittels der Technik der 'programmierten Graphreduktion' auf der Basis einer abstrakten Maschine - der sogenannten G-Maschine [Johnsson 87]. Leider ist selbst diese Implementierung im allgemeinen langsamer als die imperativer Sprachen. Dies zeigt, daB von Neumann-Rechner als Zielarchitekturen zur Implementierung funktionaler Sprachen nicht vorteilhaft sind.
In den letzten Jahren wurden daher - meist in Verbindung mit neuen Implementierungstechniken - eine ganze Reihe innovativer Rechnerarchitekturen speziell fUr funktionale Sprachen entworfen, unter anderem etwa in [Turner 79], [Clarke, Gladstone, MacLean, Norman 80], [Johnsson 84, 87], [Fairbairn, Wray 87], [Burn, Peyton-Jones, Robson 88], [Meijer 88], [Peyton-Jones, Saskild 88]. Nur
5
einige dieser Entwiirfe wurden bisher tatsachlich in Hardware realisiert. Dazu zahlt die abstrakte G-Maschine, deren Hardwareversion von der Leistungsfahigkeit durchaus mit von Neumann-Rechnern vergleichbar ist [Kieburtz 87].
Die durch technologische Fortschritte stark vorangetriebene Entwicklung von Parallelrechnerarchitekturen offenbart vor allem fUr funktionale Sprachen neue Moglichkeiten. Denn auf Grund der Eigenschaft der 'referential transparency' enthalten funktionale Programme implizite Parallelitat, die darin besteht, dafi unabhangige Teilausdriicke in beliebiger Reihenfolge, also insbesondere parallel ausgewertet werden konnen. In dem in Bild 4 angegebenen MIRANDA-Programm zur Berechnung der Fakultatsfunktion konnten etwa im Fall l#h die rekursiven Funktionsaufrufe (pfac 1 m) und (pfac (m+1) h) parallel ausgewertet werden.
pfac 1 h = 1, = l*h, = (pfac 1 m) * (pfac (m+1) h),
where m = (1+h)/2
1 = h 1+1 = h rv(1=h) & rv(1+1=h)
Bild 4: Programm mit impliziter ParallelWit
Prinzipiell konnen funktionale Sprachen auf Parallelrechnern implementiert werden, ohne dafi die Sprache urn syntaktische Konstrukte zur Spezifikation von Parallelitat erweitert werden muB. Der Programmierer braucht sich also nicht urn die Organisation der Parallelausfiihrung seines Programmes, zu der Kommunikationen zwischen parallelen Prozessen und die Synchronisation von Prozessen gehoren, zu kiimmern.
Ein sogenannter parallelisierender Compiler kann die in einem Programm enthaltene implizite Parallelitat entdecken und das Programm in parallele Prozesse zerlegen, so daB eine Auswertung auf einem Parallelrechner durchgefiihrt werden kann, ohne dafi der Programmierer irgendwelche zusatzlichen Angaben machen muB.
Ziel dieses Buches ist die konzeptionelle Entwicklung eines solchen parallelisierenden Compilers fiir funktionale Sprachen und damit verbunden der sprachorientierte Entwurf einer Parallelrechnerarchitektur, die die Ausfiihrung funktionaler Programme in besonderer Weise unterstiitzt. Bild 5 zeigt schematisch die Vorgehensweise, die wir zur Implementierung funktionaler Sprachen, die 'lazy evaluation' unterstiitzen, auf Multicomputersystemen gewahlt haben. Vnter Multicomputersystemen verstehen wir dabei Parallelrechner, die aus mehreren un-
6
abhangigen Prozessorelementen bestehen, die iiber ein Netzwerk kommunizieren konnen.
Urn von den Besonderheiten der verschiedenen funktionalen Sprachen mit 'lazy evaluation' zu abstrahieren, gehen wir zunachst zu einer Zwischensprache tiber, die wir SAL (Simple Applicative Language) nennen und die im wesentlichen einer erweiterten Form des A-Kalkiils entspricht. Ein parallelisierender Compiler iibersetzt die Programme dieser Zwischensprache, in eine parallele Zwischensprache, in der Parallelitat explizit durch ein spezielles syntaktisches Konstrukt, das let parKonstrukt, angezeigt wird. Bild 6 zeigt eine solche Transformation fUr das in Bild 4 angegebene MIRANDA-Programm.
Anhand dieses Beispiels gehen wir kurz auf einige Merkmale von SAL und der parallelen Zwischensprache ein.
In SAL gehort der auszuwertende Ausdruck zum Programm, damit er bei der Parallelisierung beriicksichtigt werden kann. Rekursive Funktionen werden innerhalb eines letrec-Konstruktes definiert, welches ahnlich zu dem where-Konstrukt in MIRANDA ist, also insbesondere geschachtelt auftreten kann. Geschachtelte letrec-Ausdriicke werden bei der Parallelisierung allerdings eliminiert. Zur Definition von Funktionen wird in SAL die A-Notation benutzt. Fiir Fallunterscheidungen steht das if - then - else-Konstrukt zur Verfiigung. AIle Ausdriicke werden in Prafixnotation geschrieben. SAL lafit lokale Definitionen von nicht rekursiven Objekten mittels des let-Konstruktes zu. 1m Gegensatz zu den letrec-Konstrukten werden let-Konstrukte bei der Parallelisierung nicht eliminiert.
Ein Programm der parallelen Zwischensprache besteht im allgemeinen aus einem System von globalen Funktionsdefinitionen, d.h. Funktionsdefinitionen, in denen keine letrec- oder A-Konstrukte als echte Teilausdriicke auftreten.
{main
Fl(Xl, ...... ,xmJ )
Fr(Xl, ... ,XmJ
= exp } = eXPl
= eXPr
In Bild 6c betsteht dieses System aus zwei Gleichungen. Applikationen in dieser Sprache werden zur Erleichterung der Implementierung
in sogenannter flacher oder first-order Form:
Funktionssymbol (Argument l , ... , Argumentn )
notiert. Dies hat implementierungstechnische Griinde und wird spater naher erlautert.
Parallelitat wird durch das letpar-Konstrukt angezeigt, das im wesentlichen folgende Form hat:
Funktionale Sprache
(Z.B.: MIRANDA, LAZyML etc.)
Erweiterter A-Kalkiil (SAL)
parallelisierender Compiler
Parallele Zwischensprache
Parallele Abstrakte Maschine
M ulticomputersystem
Bild 5: Organisation der parallelen Implementierung
7
8
a) MIRANDA-Programm
pfac 1 h = I, = l*h, = (pfac 1 m) * (pfac (m+l) h),
where m = (1+h)/2
mit auszuwertendem Ausdruck (pfac 1 11)
.Jj. Transformation in SAL .Jj.
b) SAL-Programm
letrec pfac = A (I, h). if (=,I,h) then 1
1 = h 1+1 = h fV(l=h) & fV(l+l=h)
else if (=,(+,I,I),h) then (*,I,h) else let m = (j,( +,I,h),2)
in (*, (pfac, I, m), (pfac, (+,m,I), h)) titi
in (pfac, 1, 11)
.Jj. Parallelisierung .Jj.
c) Parallelisiertes Programm
main = pfac(l, 11) pfac (I, h) = if =(I,h) then 1
else if=(+(I,I),h) then *(I,h) else let m = /( +(I,h),2)
titi
in let par y = pfac(l, m) in *(y, pfac( +(m,I), h))
Bild 6: Parallelisierung eines MIRANDA-Programms
let par Yl = F{(ell, ... ,el n1 )
and and YP = F;(epl, ... ,epnp ) in e[Yl, ... , YpJ
9
Die Auswertung eines Ausdruckes dieser Form erfolgt derart, daB die Ausdriicke FI( eil, ... , ein.) (1 ~ i ~ p) parallel, d.h. auf anderen Prozessorelementen ausgewertet werden konnen, wahrend der Ausdruck e lokal ausgewertet wird. In diesem Ausdruck werden die parallel auswertbaren Teilausdriicke mittels der Variablen Yi referenziert. Bei der Parallelisierung wird sichergestellt, daB die parallel auswertbaren Ausdriicke immer als Applikationen von definierten Funktionen dargestellt werden. Dies vereinfacht den Transfer von solchen Ausdriicken zu anderen Prozessoren.
Durch das let par-Konstrukt wird ein hierarchisches ProzeBsystem beschrieben. Synchronisation ist nur zwischen der Hauptrechnung, d.h. der Auswertung von e und den parallelen Prozessen zur Auswertung der Teilausdriicke
des Ausdruckes e notwendig. In dem sehr einfachen Fall der Parallelisierung des pfac-Programms in Bild
6 wird jeweils ein rekursiver Aufruf von pfac zur Parallelauswertung freigegeben. Dadurch ergibt sich das in Bild 7 skizzierte ProzeBsystem.
Ausgehend von der parallelen Zwischensprache erfolgt der Entwurf einer parallelen abstrakten Maschine, auf deren Basis die Organisation der parallelen Programmausfiihrung und die Verwaltung paralleler Prozesse spezifiziert wird. Die Maschine besteht aus einer endlichen Anzahl von Prozessorelementen, die iiber ein Verbindungsnetzwerk Nachrichten austauschen konnen. Jedes Prozessorelement enthalt zwei autonom arbeitende Prozessoreinheiten - eine Kommunikationseinheit und eine Reduktionseinheit. In den Reduktionseinheiten erfolgt die sequentielle AusfUhrung von Prozessen. In den Kommunikationseinheiten erfolgt die Verwaltung der Parallelitat. Diese dezentrale Organisation der abstrakten Maschine ermoglicht eine optimale Ausnutzung von Parallelitat auch innerhalb der verschiedenen Maschinenkomponenten und erleichtert ihre formale Spezifikation.
Ais Implementierungstechnik haben wir die programmierte Graphreduktion gewahlt, da diese sich in sequentiellen Implementierungen, wie bereits erwahnt, bewahrt hat und daher, wie sich zeigen wird, auch fUr eine parallele Implementierung eine gute Grundlage bildet. Bei der programmierten Graphreduktion wird das auszufiihrende Programm als Graph repdisentiert, der wiihrend der Ausfiihrung transformiert wird. Die Graphtransformationen werden durch Maschinencode gesteuert.
10
[3 - rekursiver Aufruf
~ - Prozefi
Bild 7: Rekursive Aufrufe und parallele Prozesse im pfac-Programm
11
Die in der parallelen Zwischensprache benutzte Hache Notation fur Applikationen fuhrt zu einer besonders kompakten Graphreprasentation von auszuwertenden Ausdrucken, bei der sichergestellt ist, dafi der nachste durchzufuhrende Transformationsschritt immer bereits durch die Wurzel des zu reduzierenden Graphen bestimmt ist. Dadurch werden aufwendige Graphtraversierungen, wie sie etwa in der G-Maschine notwendig sind, vermieden.
Die parallele abstrakte Maschine kann auf realen Multicomputersystemen implementiert werden oder als Ausgangspunkt einer Hardwareentwicklung dienen. Wir werden am Ende dieses Buches nur die erste Moglichkeit diskutieren und einige Aspekte zur Implementierung der Maschine auf einem OccAM/Transputersystem angeben.
Entsprechend der in Bild 5 enthaltenen Ubersicht ist das Buch in folgende drei Teile gegliedert:
I. Grundlagen und Beschreibung der Ausgangs'sprache II. Parallelisierung funktionaler Programme
III. Entwurf einer parallelen Graphreduktionsmaschine.
1m ersten Teil wird zunachst die Sprache SAL eingefiihrt, die wir als Ausgangssprache der parallelen lmplementierung wahlen. Diese einfache funktionale Sprache wird durch die Angabe der Syntax, Fixpunkt- und Reduktionssemantik definiert. Sie umfafit die wesentlichen Konzepte, die allen funktionalen Sprachen zugrundeliegen. Anhand der Sprache SAL werden die wichtigsten sequentiellen Reduktionsstrategien fur funktionale Sprachen vorgestellt.
Aufierdem geben wir einen Uberblick iiber die wichtigsten in der Literatur beschriebenen lmplementierungstechniken fiir funktionale Sprachen. Den Abschlufi des erst en Teils bildet eine kurze Ubersicht uber Architekturformen fiir Parallelrechner.
1m zweiten Teil beschreiben wir die Techniken und Verfahren, die im parallelisierenden Compiler benutzt werden, urn die in einem funktionalen Programm enthaltene implizite Parallelitat zu entdecken und das Programm in parallel ausfiihrbare Teile zu zerlegen. Fur Sprachen mit 'lazy evaluation' erweist sich vor allem die Entdeckung der impliziten Parallelitat als schwierige und aufwendige Aufgabe, wenn nur solche Teilausdriicke ausgewertet werden sollen, deren Wert zur Bestimmung des Gesamtresultates benotigt wird. Man spricht in diesem Fall von konservativer Parallelitat.
Eine alternative Methode besteht darin, beliebige Teilausdriicke parallel auszuwerten und parallele Prozesse, fiir die sich herausstellt, dafi ihr Resultat nicht benotigt wird, abzubrechen. Man bezeichnet dies als spekulative Parallelitat, da Berechnungen durchgefiihrt werden, deren Ergebnis moglicherweise nicht benotigt
12
wird. Die Entdeckung von spekulativer Parallelitat ist zwar sehr einfach, aber die Verwaltung der parallelen Prozesse stellt ein groBes Problem bei dieser Methode dar. Eine Verschwendung von Ressourcen kann nieht vollends ausgeschlossen werden. Aus diesem Grunde werden wir in diesem Buch nur konservative Parallelitat behandeln.
In dem in Bild 3 angegebenen Programm durfen also z.B. Aufrufe der Funktion genfib immer nur dann ausgewertet werden, wenn ein weiteres Listenelement zur Bestimmung des Gesamtergebnisses benotigt wird. Auf diese Weise wird eine vollstandige Auswertung der durch Aufrufe von genfib erzeugten unendlichen Liste verhindert. Die Ausnutzung von Parallelitat ist in diesem Programm nur begrenzt moglich.
Der zweite Teil des Buches greift zum Teil auf in der Literatur vorgeschlagene Techniken und Algorithmen zuruck und zeigt, wie diese Verfahren in dem parallelisierenden Compiler eingesetzt werden. Alle zur Parallelisierung erforderlichen Programmtransformationen werden formal spezifiziert.
Zum AbschluB von Teil II geben wir eine Graphreduktionssemantik fur parallelisierte Programme an, die die Darstellung des Programms als Graphen berucksichtigt und wiedergibt, welche Teilgraphen parallel reduziert werden konnen. Diese Reduktionssemantik bildet die Schnittstelle zwischen der rein sprachliehen Ebene, auf der Programmtransformationen zur Parallelisierung durchgefiihrt werden, und der technischen Ebene der parallelen abstrakten Graphreduktionsmaschine, auf der die parallelisierten Programme ausgefiihrt werden sollen.
Den Entwurf der parallelen abstrakten Graphreduktionsmaschine beschreiben wir im dritten Teil des Buches. Neben der formalen Spezifikation, die wir auf der Basis nichtdeterministischer Transitionssysteme vornehmen, wird die Ubersetzung parallelisierter Programme in Code der parallelen Maschine vollstandig spezifiziert. Anhand eines ausfiihrlichen Beispieles wird die Arbeitsweise der Maschine verdeutlieht. lnsgesamt zeigt sieh, daB die Technik der programmierten Graphreduktion in naturlicher Weise an eine parallele Umgebung angepaBt werden kann. 1m AnschluB an die formale Spezifikation der Maschine diskutieren wir kurz einige Aspekte der Implementierung der abstrakten Maschine auf einem realen Multiprozessorsystem (einem OccAM/Transputersystem).
Den AbschluB bildet ein Vergleich des hier beschriebenen Ansatzes zur parallelen lmplementierung funktionaler Sprachen mit anderen Projekten gleicher Zielsetzung.
1m Anhang sind mathematische Grundlagen zusammengestellt, die beim Verstandnis des erst en Kapitels hilfreich sein konnen.