[informatik-fachberichte] parallele implementierung funktionaler programmiersprachen volume 232 ||...
TRANSCRIPT
Kapitel2
1m plementierungstechniken
Ein wesentliches Problem bei der Implementierung der Reduktionsregeln einer funktionalen Sprache ist die Behandlung der Substitution von Variablen durch Ausdriicke, wie sie etwa bei der /3-, let-, case- und letrec-Reduktion auftritt. 1m wesentlichen gibt es drei Vorgehensweisen :
1. direkte textuelle Ersetzung (string reduction),
2. indirekte Ersetzung durch Verwaltung von Umgebungen, in denen die Bindung von Variablen an Werte bzw. unausgewertete Teilausdriicke vermerkt wird (engl.: closure technique) und
3. direkte Ersetzung durch Umsetzen von Zeigern in einer Darstellung des zu reduzierenden Ausdruckes als Graphen (graph reduction).
2.1 Direkte textuelle Ersetzung
Die einfachste Methode zur Implementierung funktionaler Sprachen ist sicherlich die direkte textuelle Ersetzung oder "string reduction". Der zu reduzierende Ausdruck wird als Zeichenkette (string) dargestellt. Wahrend einer Reduktion wird die Substitution von Variablennamen durch Ausdriicke explizit durchgefUhrt. Dabei werden natiirlich die Ausdriicke sooft kopiert wie der jeweilige Variablenname auftritt. Dies fiihrt zu einem hohen Zeit- und Platzaufwand, da die substituierten Ausdriicke sehr komplex sein konnen. Bei einer call-by-name Auswertung kommt es zudem zur Mehrfachauswertung von kopierten, nicht ausgewerteten Ausdriicken, wie wir bereits in Beispiel 1.4.5 gesehen haben. Aufgrund dieser Nachteile und Probleme ist diese Methode fUr die Praxis uninteressant. In der Literatur existieren aber Vorschlage fUr Maschinen, die dieses einfache Prinzip zugrundelegen,
R. Loogen, Parallele Implementierung funktionaler Programmiersprachen© Springer-Verlag Berlin Heidelberg 1990
2.2. UMGEBUNGSBASIERTE REDUKTION 59
so z.B. die Reduktionsmaschine von Berkling [Berkling 75] sowie Magos parallele Reduktionsmaschine [Mago 80]. Mago versucht durch massive ParaIleliUit die Ineffizienz der Stringreduktion auszugleichen. In einer parallelen Weiterentwicklung der Berklingschen Reduktionsmaschine [Kluge 83] weicht man zur Vermeidung von Mehrfachauswertungen vom Prinzip der direkten textuellen Ersetzung abo Letztendlich kann man feststeIlen, daB keine Realisierung einer Maschine, die nach dem Prinzip direkter textueller Ersetzung arbeitet, existiert.
2.2 Umgebungsbasierte Reduktion
Bei der umgebungsbasierten Reduktion erfolgt keine explizite Ersetzung von Variablennamen durch Ausdriicke. Stattdessen wird in einer separaten Struktur -der Umgebung - die Bindung der Variablennamen an Ausdriicke vermerkt . Die Berechnungsausdrucke werden durch sogenannte Closures reprasentiert. Eine Closure ist ein Paar
C=( u ,[varl/cI,"" vark/Ckj) , ~, "
"" Berechnungsausdruck Umgebung
wobei u ein Berechnungsausdruck ist, fiir den gilt:
und CI, ... , Ck wiederum Closures sind, die die Ausdriicke reprasentieren, durch die varl, ... , vark in u substituiert werden sollen. Die Closure C ist eine Darstellung des Ausdrucks
U = u[varl/uI,"" vark/uk],
sofern UI,"" Uk die Ausdriicke sind, die durch CI, .•. , Ck reprasentiert werden. Closures reprasentieren geschlossene Berechnungsausdriicke.
Die zweite Komponente einer Closure ist die Umgebung, in der die Bindungen von Variablen an Ausdriicke, welche wiederum durch Closures reprasentiert sind, vermerkt sind. Gegeniiber der Stringreduktion hat die umgebungsbasierte Reduktion natiirlich den Vorteil, daB keine Ausdriicke kopiert werden. AuBerdem kann die Mehrfachauswertung von Argumentausdriicken im FaIle einer call-byname Strategie vermieden werden, indem man nach der erst en Auswertung eines solchen Teilausdruckes denselben in der Umgebung durch seinen Wert ersetzt. Schwierigkeiten macht allenfalls eine geeignete Verwaltung und Speicherung der Umgebungen wahrend eines Reduktionsprozesses. Bevor wir auf diese Problematik naher eingehen, geben wir eine umgebungsbasierte normal order Reduktion fUr das in Beispiel 1.4.5 gegebene SAL-Programm an.
60 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
2.2.1 Beispiel Sei wie in Beispiel 1.4.5:
P = letrec get = A(lintlist, iint). case I of NIL: 0; CONS (YI, Y2): if (=, i, 1) then YI
else (get, Y2, (pred,i)) fi esac
and genfib = A(xint,xknt). (CONS, Xl, (genfib, X2, (+, XI,X2))) in (get, (genfib, 1, 1), 3)
Zur einfacheren Beschreibung der Reduktionen fUhren wir wieder folgende Bezeichnungen ein:
E(get) := letrec get = A(lintlist, iint). case I of ... esac and genfib = A(XI,X2). (CONS, Xl, (genfib, X2, (+, XI,X2))) in A(lintlist, iint). case I of ... esac ,
E(genfib) := letrec get = A(lintlist, iint). case I of ... esac and genfib = A(XI,X2). (CONS, Xl, (genfib, X2, (+, XI,X2))) in A(XI,X2). (CONS, Xl, (genfib, X2, (+, XI,X2)))
Die Reduktion startet mit der Closure (P, [J), in der die Umgebung leer ist, da P ein geschlossener Berechnungsausdruck ist. Zur Vereinfachung notieren wir in der Umgebungskomponente nur die Bindungen der im Berechnungsausdruck der Closure tatsachlich frei vorkommenden Variablen.
( P, []) =?cl ( (get, (genfib, 1, 1),3), [get/(E(get), []), genfib/( E(genfib), [] ) ] ) ------
(E(get), []) =?cl ( (A (1, i).case I of··· esac, [get/( E(get), [] )] )
=?cl (case I of NIL: 0, CONS(YI,Y2): if(=,i,l) then YI else ... fl., [1/ ((genfib, 1, 1), [genfib/(E(genfib), [])]), i/3, get/(E(get),[])] ) , ..".. ,
( (genfib, 1, 1), [genfib / (E(genfib), [J}]) ~cl ((A(XI,X2).(CONS, Xl, (genfib, X2, (+, Xt,X2))), 1, 1),
[ genfib/ (E(genfib),[J) ] )
2.2. UMGEBUNGSBASIERTE REDUKTION
=*cl ( CONS (Xl, (genfib, X2, (+, Xl,X2»), [xl/I, x2/1, genfib/ (E(genfib), []}] ) , v '
=: U(l)
~cl ( if(=, i, 1) then Yl else (get, Y2, (pred, i)) ii, [yl/(Xl,U(1»), Y2/((genfib,x2, (+,Xl,X2)), U(1») ,i/3,
61
get / (E(get), [])] )
~cl ( (get, Y2, (pred, i)), [Y2/((genfib,x2, (+,Xl,X2», U(1») ,i/3, get / (E(get), []}] ) ------...... ( (E(get), [] ) ~cl ( (-X(I, i).case 1 of··· esac, [get/(E(get),[])] )
~cl (case I of NIL: 0; CONS(Yl,Y2): if (=,i, 1) then Yl'else ... ii, [ 1/ (Y2, [Y2/((genfib, X2, (+, Xl, X2)), U(1)}]), i/((pred, i), [i/3]),
" ¥ '
(Y2, [Y2/ ((genfib, X2, (+, Xl, X2)), U(1)}]) =*cl ((genfib, X2, (+, Xl, X2», U(1»)
get / (E(get), [])] )
=*cl ( (-X(Xl,X2).(CONS, Xl, (genfib, X2, (+, Xl,X2)), X2, (+, Xl, X2)), U(l)}
=*cl (CONS (Xl, (genfib, X2, (+, Xl,X2))), [xl/ (X2' U(1»), X2/ (( +, Xl, X2), U(l»), genfib/ (E(genfib), [])]) " ",.. .,
=*cl ( if (=, i, 1) then Yl else (get, Y2, ( pred, i)) ii, [yl/ (Xl, U(2»), Y2/ ((genfib, X2, (+, Xl, X2», U(2»), i/2,
get / ((E(get), []))] )
=*cl ( (get, Y2, ( pred, i», [Y2/ ((genfib, X2, (+, Xl, X2)), U(2)}, i/2, get/ ((E(get), []))]}
=*cl (case I of NIL: 0; CONS(yl,Y2): if(=,i,l) then Yl else ... ii esac, [1/ (Y2, [Y2/ ((genfib, X2, (+, Xl, X2)), U(2)}]) },
" y ,
i/ ((pred, i), [i/2]) , get/((E(get), [])}]} " '¥" '
62 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
(Y2, [Y2/ ((genfib, X2, (+, Xl, X2)), U(2»))) ~c1 ((genfib, X2, (+, Xl, X2)), U(1») ~c1 ((A(Xl, X2).(CONS, Xl, (genfib, X2, (+, Xl, X2))),
X2,(+,Xl,X2)), U(2») ~c1 (CONS (Xl, (genfib, X2, (+, Xl,X2))),
[xl/ (X2, U(2»), X2/ (( +, Xl, X2), U(2»), genfib/ (E(genfib), [])])
~c1 (Yl,[yl/(Xl,[Xl/(X2,U(2»)])])
~c12
Die Schachtelungstiefe der Closures in der Umgebung entspricht der Abstraktionstiefe des jeweiligen Ausdruckes. Bei obiger normal order Reduktion treten die Umgebungen U(l) und U(2) durch die Schachtelung der Closures mehrfach als Teilumgebungen auf. In einer realen Implementierung muB natiirlich ein Kopieren von Umgebungen vermieden werden, da dies wiederum zu Mehrfachauswertungen fiihren kann. I.a. werden bei der Abspeicherung von Closures Zeigertechniken verwendet, auf die wir spater noch eingehen werden.
Die erste abstrakte Maschine, die zur Ausfiihrung funktionaler Sprachen entworfen wurde, war die SECD-Maschine von Landin [Landin 64]. Diese Maschine implementiert eine applicative-order Reduktionsstrategie auf der Basis der ClosureTechnik. Auf Grund der applicative-order Strategie sind Closures lediglich zur Reprasentierung von Funk~ionsausdriicken (Ausdriicken von funktionalem Typ) notwendig. Alle anderen Ausdriicke werden ja vor der Bindung an Variable vollstandig zu Konstanten aus AUTr(A) reduziert. Die Berechnungsausdriicke werden in elementaren Maschinencode iibersetzt. Umgebungen werden durch Listen von ( Variablen, Wert oder Closure )-Paaren realisiert. Das Kopieren von Umgebungen hat zwar keine Mehrfachauswertungen zur Folge, sollte aber wegen des Kopieraufwands trotzdem umgangen werden. Dies geschieht im allgemeinen, indem die zweite Komponente einer Closure einen Zeiger auf die Umgebung enthiilt und lediglich solche Zeiger auf Umgebungen kopiert werden.
Die "Functional Abstract Machine" (FAM) von Cardelli [Cardelli 83], auf der ein Compiler fiir die Sprache ML beruht [Cardelli 84]' ist eine stark optimierte
2.2. UMGEBUNGSBASIERTE REDUKTION 63
SECD-Maschine. Die Optimierungen bestehen im wesentlichen darin, daB die Berechnungsausdriicke in einen sehr miichtigen FAM-Maschinencode iibersetzt werden, der wiederum in Zielmaschinencode iiberfiihrt wird. Die Closures bestehen aus einem Zeiger auf die Ubersetzung des Rumpfes des Berechnungsausdruckes und einem Feld, in dem die Werte der Variablen vermerkt sind, die frei in dem Berechnungsausdruck auftreten. Umgebungen werden also nicht als Listen sondern als Felder dargestellt und auf die Variablen eingeschriinkt, die tatsachlich im Berechnungsausdruck frei auftreten. Letztendlich werden sooft wie moglich Stacks eingesetzt, die direkt auf die Hardwarestacks der Zielmaschine abgebildet werden konnen.
Die SECD-Maschine kann in einfacher Weise so modifiziert werden, daB eine call-by-need-Strategie implementiert wird. Die Umgebungen miissen dazu, wie aus obiger Beispielreduktion ersichtlich, Closures enthalten konnen, die unausgewertete Teilausdriicke repriisentieren. Wichtig ist dabei, daB diese Closures nach einer eventuellen Auswertung in der Umgebung durch das Ergebnis dieser Auswertung ersetzt werden konnen. Dies geschieht im allgemeinen wieder durch den Einsatz von Zeigern. Alle Closures werden in der Umgebung indirekt durch Zeiger auf die eigentlichen Closures repriisentiert. Dies ermoglicht eine einfache Ersetzung der Closure durch das Ergebnis ihrer Auswertung an allen Stellen, an denen sie referenziert wird. In [Burge 75] ist eine in dieser Weise modifizierte SECD-Maschine beschrieben. Burge bezeichnet die Zeiger auf eine Closure als "L-value" und die Closure selbst als "R-value" und benutzt die aus imperativen Sprachen bekannte "Wertzuweisung", urn eine Closure durch das Ergebnis ihrer Auswertung zu iiberschreiben. Die modifizierte SECD-Maschine von Burge behandelt Konstruktoren von frei erzeugten Datenstrukturen allerdings wie strikte Basisfunktionen. Unendliche Datenstrukturen sind bei diesem Ansatz also noch nicht zugelassen.
Die Behandlung von Konstruktoren wie nicht-strikte Funktionen, die ja erst das Arbeiten mit unendlichen Datenstrukturen ermoglicht, wurde erst ein Jahr nach dem Erscheinen des Buches von Burge unabhiingig in [Henderson, Morris 76] und [Friedman, Wise 76] propagiert. Henderson und Morris haben in dies em Zusammenhang den Begriff "lazy evaluation" gepriigt. Auch sie beschreiben eine umgebungsbasierte Reduktion, bei der die Zeigertechnik zur Verwirklichung des call-by-need-Mechanismus eingesetzt wird.
Wesentlich fiir eine Implementierung nach dem umgebungsbasierten Prinzip ist eine geeignete Verwaltung und Speicherung der Umgebungen. Bei der konventionellen Implementierung blockstrukturierter imperativer Sprachen wie PASCAL oder ALGOL geniigt ein Laufzeitkeller zur Speicherung der Umgebungsstrukturen (in sogenannten Aktivierungsblocken) und zur Durchfiihrung der Berechnungen. Diese Organisation ist auf Grund der 'last-in-first-out'-Disziplin der Blockstruktur naheliegend, aber letztendlich nur moglich, da keine beliebigen Funktionen
64 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
hoherer Ordnung behandelt werden. In PASCAL werden Funktionen oder Prozeduren zwar als Argumente aber nicht als Werte von Funktionen zugelassen. Bei Eintritt in eine Prozedur oder eine Funktion wird auf dem Laufzeitkeller ein Aktivierungsblock angelegt, der unter anderem die Ubergabeparameter enthii1t und Platz fUr lokale Variablen bereitstellt. Beim Verlassen der jeweiligen Struktur wird dieser Aktivierungsblock wieder geloscht. Zur Behandlung beliebiger Funktionen hoherer Ordnung sind komplexere Strukturen zur Organisation der Umgebungen notwendig. Der Grund hierfiir ist, daf3 funktionale Werte, die ja durch Closures reprasentiert werden, Referenzen auf Umgebungen (Aktivierungsblocke) enthalten konnen, die bei der Stackorganisation bereits geloscht wurden. Das Problem tritt auf, wenn der Korper der Funktion, die das Ergebnis einer anderen Funktion ist, freie (globale) Variablen enthiilt.
2.2.2 Beispiel Wir betrachten als Beispiel das folgende PASCAL-ahnliche Programmsegment:
function P (x: integer): function (integer) integer; function R (y: integer): integer;
return x + y; returnR;
g:= P(5);
Beim Aufruf der Funktion P wird auf dem Laufzeitkeller ein Aktivierungsblock angelegt, der neben anderen Informationen den Wert des aktuellen Parameters fUr x enthalt. Ais Ergebnis liefert die Funktion einen Zeiger auf den Code fiir die Funktion R. Beim Verlassen der Funktion P darf der zugehOrige Aktivierungsblock nun nicht einfach geloscht werden, da der Code der Funktion R den Parameter x von P referenziert. Da der Code von R Referenzen auf samtliche Werte in der aktuellen, durch den Laufzeitkeller gegebenen Umgebung enthalten kann, miifite neben dem Code fUr R der gesamte Laufzeitkeller als Ergebnis des Aufrufs von P iibergeben werden, was natiirlich v611ig inpraktikabel ist.
Ahnliche Probleme treten auf, wenn Funktionen Datenstrukturen als Werte liefern und das call-by-name (lazy evaluation) Auswertungsprinzip zugrundeliegt.
2.2. UMGEBUNGSBASIERTE REDUKTION 65
2.2.3 Beispiel Bei einer Laufzeitkeller-basierten Implementierung des im folgenden gegebenen PASCAL-ahnlichen Programmsegments treten dieselben Probleme auf wie im vorherigen Beispiel:
function L (x: integer) : list of integer; return CONS(X, L(x + x));
1:= L(5);
x := hd(l);
Es ist also offensichtlich nicht ohne wei teres moglich, die konventionellen Implementierungstechniken imperativer Sprachen auf funktionale Sprachen zu iibertragen, da diese Sprachen i.a. beliebige Funktionen hoherer Ordnung, insbesondere funktionswertige Funktionen, und Datenstrukturen in Verbindung mit einer callby-name Auswertungsstrategie unterstiitzen. Zur Verwaltung der Umgebungen bei der Implementierung funktionaler Sprachen sind also allgemeinere Speicherorganisationsformen, etwa Heap- oder Graphstrukturen notwendig. Dabei werden freie Speicherblocke dynamisch belegt und erst wieder freigegeben, wenn keine Referenzen mehr auf diese Blocke existieren. Urn letzteres zu testen, sind Verfahren der "Garbage Collection" notwendig. Da diese Vorgehensweise im Vergleich zur Laufzeitkellertechnik sehr zeit- und platzaufwendig ist, hat es auch Versuche gegeben, die Kellertechnik so zu erweitern, daB die oben geschilderten Probleme bewaltigt werden konnen.
In [Bobrow, Wegbreit 73] wird die Kellertechnik so verallgemeinert, daB Aktivierungsblocke so lange auf dem Stack erhalten bleiben, wie die in ihnen abgelegten Umgebungsteile benotigt werden. Urn dies zu erreichen, ist eine starkere Verzeigerung der Kellerelemente untereinander notwendig. Man spricht bei dieser Technik auch von "Spaghetti Stacks". Die verwendeten Keller werden im allgemeinen sehr groB und uniibersichtlich, was sich negativ auf die Laufzeit auswirken kann.
In [Georgeff 82/84] wird gezeigt, daB man zur Auswertung von Funktionen h6herer Ordnung mit einer reinen Kellertechnik auskommt, wenn man die Auswertung von funktionswertigen Funktionen so lange verz6gert, bis so viele Argumente vorhanden sind, daB der Ausdruck zu einem Basiswert reduziert werden kann. In Verbindung mit einer call-by-name Reduktionsstrategie kann die Mehrfachauswertung funktionswertiger Ausdriicke aber nicht verhindert werden. AuBerdem k6nnen U morganisationen des Kellers notwendig werden.
66 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
1m Hinblick auf eine parallele Implementierung ist eine Kellerorganisation der Umgebung nicht unbedingt erstrebenswert, da der Laufzeitkeller eine zentrale Struktur darstellen wiirde, die bei einer verteilten AusfUhrung zu einem Engpafi fiihren wiirde. Eine Technik, die einer verteilten Implementierung mehr Moglichkeiten bietet, ist die im folgenden beschriebene "Graphreduktion".
2.3 Graphreduktion
Bei der Graphreduktion wird der zu reduzierende Ausdruck als verzeigerte Struktur, d.h. als gerichteter Graph dargestellt und entsprechend den Reduktionsregeln transformiert. Variablensubstitutionen werden durch Umsetzen von Zeigern realisiert. Die Graphreduktionstechnik wurde von Wadsworth eingefiihrt. In seiner Dissertation [Wadsworth 71] beschreibt er einen Interpreter fUr den A-Kalkiil, der Graphreduktionen nach dem call-by-name Prinzip durchfUhrt. Bei jedem Reduktionsschritt wird die Wurzel des Graphen des reduzierbaren Ausdruckes mit der Wurzel des Ergebnisses der Reduktion iiberschrieben. Eine f3-Reduktion
(Ax.e, e') => e[x/e']
erfolgt z.B. dadurch, dafi eine Kopie des Graphen fiir den Ausdruck e erzeugt wird, in der der Knoten, der freie Vorkommen der Variablen x in e reprasentiert, durch einen Verweisknoten iiberschrieben wird, der einen Zeiger auf die Wurzel der Graphreprasentation von e' enthalt (siehe Bild 2.1).
Tritt x mehrfach im Rumpf von e auf, so existieren mehrere Zeiger auf den Knoten, der x reprasentiert und bei der Reduktion durch einen Verweisknoten mit einem Zeiger auf e' iiberschrieben wird. Dadurch wird sichergestellt, daB e' hochstens einmal und zwar beim erst en Zugriff ausgewertet wird. Die Wurzel des Graphen von e' wird nach der Auswertung mit der Wurzel des Ergebnisgraphen iiberschrieben, so daB bei allen weiteren Zugriffen das Ergebnis direkt vorliegt. Darum spricht man in diesem Zusammenhang vom "Sharing" des Teilgraphen e'. Die Verhinderung der Mehrfachauswertung von Teilausdriicken durch das "Sharing" von Teilgraphen ist eine der wichtigsten Eigenschaften der Graphreduktion.
Das Kopieren des Rumpfes e der A-Abstraktion in dem oben beschriebenen Graphreduktionsschritt ist notwendig, da auf diesen Rumpf mehrere Verweise existieren konnen ("Sharing von e bzw. Ax.e") und das Uberschreiben des x-Knotens in e durch den Verweisknoten auf e' dann zu Fehlern fiihren wiirde. Kopieren von Graphteilen bedeutet aber immer einen Verlust an "Sharing" und damit die Gefahr der Mehrfachauswertung von Ausdriicken, die durch die kopierten Graphen reprasentiert werden. Betrachten wir dazu etwa folgendes Beispiel:
2.3. GRAPHREDUKTION 67
Kopie von e
Bild 2.1: ,B-Reduktion fUr Graphen
2.3.1 Beispiel Eine Graphreduktion des Ausdruckes
mit vollstandigem Kopieren der Riimpfe der >'-Abstraktionen bei der ,B-Reduktion nimmt etwa den in Bild 2.2 skizzierten Verlauf.
Der Graph, der dem Teilausdruck (>'X2.(X,X2,X2),5) entspricht, wird durch das vollstandige Kopieren des Rumpfes der >,xI-Abstraktion dupliziert, was im weiteren Verlauf zur doppelten Auswertung dieses Ausdruckes fiihrt.
In Wadsworth's Graph-Interpreter werden solche Mehrfachauswertungen vermieden, indem bei einem ,B-Reduktionsschritt nur die Teile des Rumpfes der >.Abstraktion kopiert werden, die von der (oder den) zu substituierenden Variablen abhangen. Formal wird dies wie folgt prazisiert. Zunachst wird der Begriff der frei vorkommenden Variable fUr Teilausdriicke verallgemeinert.
Ein Teilausdruck E' des Rumpfes E einer >'-Abstraktion >'x.E heifit /rei (bezuglich der >.-Abstraktion), falls keine in E' frei auftretende Variable in >'x.E gebunden wird.
Ein Teilausdruck E' des Rumpfes E einer A-Abstraktion Ax.E heifit maximal/rei bzgl. dieser A-Abstraktion, falls es keinen bzgl. dieser AAbstraktion freien Teilausdruck gibt, der E' umfafit.
2.3. GRAPHREDUKTION 69
2.3.2 Beispiel In AX.(Ay.(+,(*,x,x),(*,y,5)) ist (*,x,x) frei bzgl. der inneren A-Abstraktion und lediglich 5 frei bzgl. der ausseren A-Abstraktion.
In Wadsworth's Graph-Interpreter werden bei der Durchfiihrung einer ,B-Reduktion
(Ax.e e') '* e[x/e']
die beziiglich Ax.e maximal freien Teilausdriicke von e nicht kopiert, da die Substitution von x durch e' fUr diese ohne Auswirkung ist.
In Beispiel 2.3.1 ist der Teilausdruck E = (AX2'( *, X2, X2), 5) maximal frei in dem A-Ausdruck Axd *, xl, E). Wadsworth's Graphinterpreter vermeidet also das Kopieren des zu dies em Ausdruck gehorenden Graphen.
Bemerkenswert ist, daB die doppelte Auswertung dieses Ausdruckes bei der umgebungsbasierten Reduktion, wie sie etwa in der SECD-Maschine implementiert ist, nicht verhindert wird. Bei der umgebungsbasierten Reduktion wird nur sichergestellt, daB Argumentausdriicke, also Ausdriicke, die in der Umgebung an Variablen gebunden sind, hochstens einmal reduziert werden. Der Ausdruck AXI.( *, Xl, E) wird zwar als Argument iibergeben und daher hochstens einmal reduziert. Er befindet sich allerdings It. Lemma 1.4.4 in '*n-Normalform, ist also auf Grund der auBeren A-Abstraktion nicht weiter reduzierbar, obwohl er einen reduzierbaren Teilausdruck (E) enthalt. Die eigentliche Ursache der Mehrfachauswertung von E ist also die Wahl der '*n-Normalform bzw. die Tatsache, daB die Reduktionsstrategien applicative-order und normal order immer nur Reduktionen auf dem auBersten Level (top-level) durchfUhren. Die Beschrankung auf top-level Reduktionen hat, wie wir in Abschnitt 1.4 bereits festgestellt haben, den entscheidenden Vorteil, daB wahrend der Reduktion keine Variablenkonflikte auftreten konnen. Hier zeigt sich allerdings, daB im Zusammenhang mit Funktionen hOherer Ordnung Mehrfachauswertungen von Ausdriicken auftreten konnen, sofern sie nicht durch spezielle Techniken wie die Erkennung maximal freier Ausdriicke in Wadsworth's Graphinterpreter vermieden werden.
Die Erkennung maximal freier Teilausdriicke in Wadsworth's Interpreter verhindert zwar die Mehrfachauswertung solcher, ist aber eine sehr teure Operation, da jeweils der gesamte Rumpf von A-Abstraktionen durchlaufen werden muB. AuBerdem muB diese Operation vor jedem Reduktionsschritt erfolgen.
Wesentlich einfacher und effizienter laBt sich eine Graphreduktion verwirklichen, die dasselbe Verhalten zeigt wie etwa die umgebungsbasierte Reduktion. Das heiBt, es wird nur sichergestellt, daB Argumentausdriicke hochstens einmal ausgewertet werden. Wie man leicht sieht, sind Argumentausdriicke, die in den Rumpf einer Abstraktion substituiert werden, immer frei bzgl. dieser Abstraktion. Bei einer Reduktion
(AX.Ay.M, A) '* Ay.(M[x/A])
70 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
wird jedes freie Vorkommen von x in M durch A ersetzt. Dies bedeutet aber unmittelbar, daB A ein freier Teilausdruck von Ay.M[x/A] ist. Die Wurzel von Ausdriicken, die in andere Ausdriicke substituiert wurden, erkennt man bei der Graphreduktion in einfacher Weise dadurch, daB ein Verweisknoten, durch den der Variablenknoten iiberschrieben wurde, auf sie zeigt. Mochte man Verweisknoten vermeiden, kann man die Wurzel von Argumentausdriicken auch bei der Ersetzung geeignet markieren. Kopiert man nun bei einer ,B-Reduktion den Graphen, der dem Rumpf entspricht, bis zu den Verweisknoten bzw. markierten Wurzeln der Argumentgraphen, so wird die Mehrfachauswertung von Argumentausdriicken vermieden. Es besteht eine l-l-Korrespondenz zur umgebungsbasierten Reduktion. Lediglich die Darstellung der Berechnungsausdriicke ist unterschiedlich.
Wird bei einer call-by-name Reduktion die Mehrfachauswertung von maximal freien Teilausdriicken von Abstraktionsausdriicken vermieden, so spricht man von einer ''fully lazy evaluation". Wadsworth's Graphinterpreter realisiert also eine "fully lazy" Reduktion. Zur Unterscheidung bezeichnet man i.a. die Auswertung, die von dem oben beschriebenen vereinfachten Graphinterpreter durchgefiihrt wird, als "lazy". Den vereinfachten Graphinterpreter nennt man "lazy interpreter".
Der Begriff "fully lazy evaluation" wurde von Hughes gepragt [Hughes 82]' der gezeigt hat, daB man jeden A-Ausdruck so transformieren kann, daB die maximal freien Teilausdriicke von Abstraktionsausdriicken trivial, d.h. Variablen oder Konstante sind. Diese Transformation hat zur Folge, daB es wahrend der Reduktion geniigt, darauf zu achten, daB keine Argumentausdriicke (fiir Variablen substituierte Ausdriicke) mehrfach ausgewertet werden, wie es etwa bei dem oben beschriebenen Graphinterpreter der Fall ist. Die transformierten Ausdriicke garantieren eine "fully lazy" Auswertung mittels eines "lazy" Interpreters. Die wesentliche Idee der von Hughes vorgeschlagenen Transformation besteht darin, maximal freie Teilausdriicke aus A-Ausdriicken zu abstrahieren und als Argumente zu iibergeben.
2.3.3 Beispiel In dem A-Ausdruck
aus dem obigem Beispiel ist (AX2.(*,X2,X2),5) ein nicht-trivialer maximal freier Teilausdruck des Rumpfes der auBeren A-Abstraktion. Die von Hughes definierte Transformation besteht im wesentlichen darin, den Rumpf der AAbstraktion durch eine Applikation zu ersetzen, in der alle nicht-trivialen maximal freien Ausdriicke als Argumentausdriicke auftreten, die aus dem Rumpf herausabstrahiert wurden. Obiger Ausdruck wird also in folgende
2.4. KOMBINATOREN 71
Form gebracht:
Durch diese Transformation wird der maximal freie Ausdruck zu einem Argumentausdruck, fUr den auch bei einem "lazy" Interpreter sichergestellt ist, daB hochstens eine Auswertung dieses Ausdruckes erfolgt.
Der in [Hughes 82] beschriebene Algorithmus iibersetzt A-Ausdriicke in Kombinatoren. Auf die Bedeutung und Vorteile von Kombinatoren fUr die Implementierung funktionaler Sprachen werden wir im nachsten Abschnitt naher eingehen. In [Arvind, Kathail, Pingali 85] findet sich eine interessante Gegeniiberstellung von Wadsworth's Graphinterpreter, dem oben skizzierten vereinfachten ("lazy") Graphinterpreter sowie des umgebungsbasierten Interpreters von Henderson und Morris [Henderson, Morris 76].
Die besonderen Vorteile der Graphreduktionstechnik liegen zum einen im einfachen Sharing von Ausdriicken zur Vermeidung von Mehrfachauswertungen. Zum anderen eignet sich die Graphreduktion im Gegensatz zur umgebungsbasierten Reduktion zunachst besser zum Einsatz in parallelen Systemen, da sie schnelle und effiziente Kontextwechsel ermoglicht. Die gesamte Information zur Reduktion von Ausdriicken ist im Graphen enthalten. Kontextwechsel konnen also im wesentlichen durch Umsetzen von Zeigern erfolgen, ohne daB groBe Informationsmengen gesichert werden miissen. Ein Nachteil der Graphreduktion ist allerdings, daB man Kopien von Funktionsriimpfen machen muB. Dies wird oft als Hauptquelle der Ineffizienz bei der Graphreduktion betrachtet [Hughes 84]. Wie wir jedoch im folgenden Abschnitt sehen werden, kann man auf das Kopieren von Funktionsriimpfen verzichten, wenn man Graphreduktion in einem Kombinatorkalkiil betreibt. AuBerdem werden sich weitere Vorteile der Graphreduktion zeigen, wenn man sie im Zusammenhang mit der Kombinatortechnik betrachtet.
2.4 Kombinatoren
Das Problem der Variablensubstitution bei der Reduktion von A-Ausdriicken versucht Turner [Turner 79] zu umgehen, indem er A-Ausdriicke in variablenfreie Ausdriicke der kombinatorischen Logik [Schonfinkel 24, Curry, Feys 58] iibersetzt. Es ist moglich, Ausdriicke des reinen ,X-Kalkiils in rein applikative (also nur mittels monadischer Applikation erzeugte) Ausdriicke zu iibersetzten, die nur aus den drei Kombinatoren
S AJ.,Xg.AX.((fX)(gx)) K AX.'xy.X I 'xx.x
72 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
mit den Reduktionsregeln
(((Set}e2)e3) -+ ((eIe3)(e2 e3)) ((Ket}e2) -+ el (leI) -+ el
aufgebaut sind. Turner nennt den UbersetzungsprozeB, der durch die folgenden Regeln gegeben ist, Variablenabstraktiont:
,xx.e "-+ [x]e [x](eIe2) "-+ ((8 [x]el) [x]e2) [x]x "-+ I fur Variable x, [x]y "-+ (Ky) fur Variable y i=- x, [x]a "-+ (Ka) fur Konstante a.
2.4.1 Beispiel Der ,x-Ausdruck (,xx.(( +x)3) 5), wobei +, 3 und 5 Konstante seien, wird etwa in folgenden Kombinatorausdruck iibersetzt:
( ( (8 (( 8 (K + )) I)) (K 3)) ([{ 5)).
Beschdinkt man sich auf die Kombinatoren S, K und I, so fiihrt die Variablenabstraktion zu einem exponentiellen Wachstum der GroBe der Ausdriicke, was natiirlich fiir eine Implementierung untragbar ist. Turner bewaltigt dieses Problem, indem er einige zusatzliche Kombinatoren erlaubt und wahrend der Ubersetzung Optimierungsregeln anwendet, die die GroBe der Kombinatorausdriicke drastisch reduzieren.
Die Ausdriicke des Kombinatorkalkiils konnen mittels der Reduktionsregeln fUr die Kombinatoren und der Regeln fUr die Konstanten in sehr einfacher Weise, insbesondere auf Grund der Variablenfreiheit ohne die Notwendigkeit einer Umgebung, reduziert werden.
Turner sah die Kombinatorreduktion vor allem als Alternative zur umgebungsbasierten Reduktion, da seinen Untersuchungen zufolge die Verwaltung und der Zugriff auf die Umgebungsstruktur zuviel Aufwand erfordern. Bei der Kombinatorreduktion wird die zentrale Umgebungsstruktur aufgelost im Zusammenspiel der Kombinatoren. Dies wird besonders deutlich, wenn man Kombinatoren wie in [Kennaway, Sleep 82] als Richtungsweiser ('directors') in der Graphdarstellung von Kombinatorausdriicken interpretiert. Die Kombinatoren steuern namlich den FluB von Argumenten durch den Graphen bis zu den Positionen, wo sie benotigt werden. Die von Turner definierte SKI-Reduktionsmaschine fiihrt Graphreduktionen von Kombinatorausdriicken durch. Dieser GraphreduktionsprozeB ist im
tOft wird auch die Bezeichnung bracket abstraction in Anlehnung an die Notation [z]e, bei der die zu abstrahierende Variable in eckigen Klammern notiert wird, verwendet.
2.4. KOMBINATOREN 73
Vergleich zu den im vorigen Abschnitt beschriebenen Graphinterpretern sehr elementar, da ,B-Reduktionen nur fiir Kombinatoren durchgefiihrt zu werden brauchen und deren Riimpfe eine sehr einfache Struktur haben. Insbesondere enthalten die Kombinatoren keine nicht-trivialen maximal freien Ausdriicke, d.h. SKIKombinatorreduktion fiihrt automatisch zu einer "fully lazy evaluation". Turner spricht diesbeziiglich von selbstoptimierenden Eigenschaften der Kombinatoren.
Die bestechende Einfachheit dieser Kombinatortechnik, die auf einem festen Satz von elementaren Kombinatoren aufbaut, fiihrte zu vielen Projekten, die sich mit der Implementierung funktionaler Sprachen auf der Basis dieses Kalkiils befafiten. Die Implementierungen der Sprachen SASL [Turner 76] und MIRANDA [Turner 85] basieren auf der SKI-Reduktionsmaschine von Turner.
In [Jones, Muchnick 82] wird gezeigt, wie SKI Kombinatorausdriicke in Code einer abstrakten Maschine iibersetzt werden konnen, der dann die Kombinatorreduktionen steuert. In [Hudak, Kranz 84] wird ein Compiler zur Implementierung einer funktionalen Sprache nach dem call-by-name Prinzip vorgestellt, der SKI-Kombinatoren als Zwischenstufe zur Optimierung der zu iibersetzenden Programme benutzt. Weiterhin wurden insbesondere Maschinen entwickelt, die eine direkte Implementierung der Kombinatoren in Hardware vornehmen, wie etwa die 'Cambridge SKIM Machine' [Stoye 85, Clarke, Gladstone, MacLean, Norman 80] und 'Burroughs NORMA Machine' [Richards 85, Scheevel 86]. Die SKIKombinatortechnik wurde auch im Hinblick auf die Parallelisierbarkeit des Reduktionsprozesses untersucht [Hankin, Burn, Peyton-Jones 86], [Maurer, Oberhauser 85], [Hudak, Goldberg 84]. In diesen Ansatzen zerfallt die parallele Reduktion von Kombinatorausdriicken allerdings in viele kleine Teilprozesse, die einzelnen Kombinatorreduktionen entsprechen. Da in existierenden Multiprozessorsystemen Kommunikationen aufwendig sind und i.a. mehr Zeit benotigen als eine CPUInstruktion, scheint es ratsamer eine Reduktion in komplexere Teilprozesse zu zerlegen, damit der Kommunikationsaufwand nicht den Zeitgewinn der parallelen Ausfiihrung zunichte macht.
Hudak und Goldberg machten bei ihren Simulationen zudem die Beobachtung, daB eine auf Grund von Datenabhangigkeiten vollig sequentielle Berechnung bei der parallelen Kombinatorreduktion (mit einer fest en Zahl von Kombinatoren) zur Ausfiihrung auf mehrere Prozessoren verteilt werden kann, was natiirlich mit unnotigem Kommunikationsaufwand verbunden ist [Hudak, Goldberg 84].
Auch Untersuchungen mit der COBWEB-Architektur [Hankin, Shute, Osmon 85] [Shute, Osmon 85], die auf der "Wafer Scale Integration" basiert und aus einer groBen Anzahl identischer Prozessorelemente auf einem Wafer besteht, haben gezeigt, daB bei zu kleinen parallelen Prozessen der Aufwand fiir die Kommunikationen nicht kompensiert werden kann. Daher wurde der KombinatorkalkUl urn einen speziellen Kombinator P erweitert, der die Stellen im Kombinatoraus-
74 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
druck anzeigt, an denen parallele Reduktionen angesto:Ben werden sollen [Anderson, Hankin, Kelly, Osmon, Shute 87]. Dadurch ist es moglich, mehrere Kombinatorreduktionen zu einem Proze:B zusammenzufassen. Ahnlich gehen [Maurer, Oberhauser 85] vor, wobei sie anstatt des zusatzlichen P Kombinators mit Annotationen an den Kombinatorgraphen arbeiten.
Obwohl der auf einer fest en Menge von Kombinatoren beruhende Kombinatorkalkiil verschiedene Vorteile wie etwa die Einfachheit des Reduktionsmechanismus und die "fully lazy evaluation" aufweist, ist das Zerstiickeln der Programmausfiihrung in so element are Einzelschnitte wie die Reduktionen der endlich vielen Kombinatoren letztendlich auch bei sequentieller Ausfiihrung zu ineffizient. Aus diesem Grunde schlug Hughes [Hughes 82] vor, die Beschrankung auf einen festen Satz von Kombinatoren fallen zu lassen und zu jedem funktionalen Programm (.\-Ausdruck) ein individuelles System von effizienten Kombinatoren herzuleiten. Technisch ist ein Kombinator (des reinen .\-Kalkiils) ein geschlossener '\-Ausdruck
wobei der Rumpf e rein applikativ aus Konstanten, den Variablen Xl, ... ,Xk und Kombinatornamen aufgebaut ist. Man schreibt die Kombinatoren i.a. als Gleichungssystem
wobei ei (1 ~ i ~ r) aus Konstanten, den Variablen XiI, •.. ,Xik, und den Kombinatornamen F I , ... ,Fr mittels Applikation erzeugt ist. Die Kombinatorgleichungen definieren die Reduktionsregeln des Kombinatorreduktionssystems:
wobei zu beachten ist, da:B eine Kombinatorreduktion nur erfolgen kann, wenn der Kombinator auf geniigend viele Argumente appliziert wird tt. Dadurch wird sichergestellt, daB die Berechnungsausdriicke rein applikativ sind und wahrend des Reduktionsprozesses keine beliebigen {3- Reduktionen mehr erforderlich sind, sondern nur die speziellen Kombinatorreduktionen (*). Diese haben den entscheidenden Vorteil, daB die Riimpfe nur gebundene Variablen enthalten, die bei der Kombinatorreduktion ersetzt werden, so daB der sich ergebende Ausdruck variablenfrei ist. Es ist also nicht notwendig, wahrend der Reduktion die Kombinatorriimpfe zu kopieren, da in diese hochstens einmal substituiert wird. Natiirlich la:Bt es sich nicht vermeiden, daB wahrend eines Reduktionsprozesses verschiedene Instanzen eines Kombinatorrumpfes erzeugt werden.
ttBeachte, daB im reinen >.-Kalkiil aUe Funktionen 'gecurried' sind.
2.4. KOMBINATOREN 75
Eine Ubersetzung eines A-Ausdruckes in ein Kombinatorsystem kann etwa wie folgt beschrieben werden [Hughes 82/84]:
1. Bestimme die am weitesten links und am weitesten innen stehende
A-Abstraktion Ax.e.
2. Bestimme die in Ax.e frei vorkommenden Variablen, etwa Xl, ... , Xk.
3. Definiere einen neuen Kombinator
und ersetze Ax.e durch (FXI ... Xk).
4. Wiederhole (1) - (3) solange wie moglich.
Unabhangig von Hughes entwickelte Johnsson [Johnsson 85/87] ein entsprechendes Verfahren, welches er "Lambda Lifting" nannte, da aus den A-Ausdriicken die inneren Funktionsdefinitionen auf die oberste Ebene gehoben werden. Lokale Definitionen werden globalisiert. Das von Johnsson definierte Verfahren basiert ebenfalls auf der Grundidee, globale (freie) Variable zu Funktionsparametern zu machen. Als Ausgangsbasis wahlt er allerdings einen verallgemeinerten A-Kalkiil, in dem es moglich ist, simultan rekursive Funktionen -ahnlich wie in unserem erweiterten Kalkiil (letrec-Konstrukt) - zu definieren. In diesem Fall ist der Algorithmus geringfUgig komplizierter. 1m nachsten Kapitel werden wir 'Johnsson's Lambda Lifting'-Algorithmus fUr unseren A-Kalkiil definieren.
Der oben beschriebene Algorithmus erzeugt zu einem A-Programm (oder Ausdruck) ein System von Kombinatoren, welches jedoch zunachst keine "fully lazy" Auswertung garantiert. Ersetzt man in obi gem Algorithmus die Schritte (2) und (3) durch
2'. Bestimme die in Ax.e maximal freien Teilausdriicke el, ... , ek.
3'. Definiere einen neuen Kombinator
und ersetze Ax.e durch (Fel ... ek),
so garantiert man aber, wie wir bereits im vorigen Abschnitt erHiutert haben "full laziness". Hughes nennt die Kombinatorsysteme, die durch den abgewandelten
tErsetze in e die Ausdriicke ei durch x, (1 ::; i ::; k).
76 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
Algorithmus erzeugt werden, auf Grund dieser besonderen Eigenschaft Superkombinatoren [Hughes 82/84]. Eine umfassende Darstellung der verschiedenen Ansatze zur Ubersetzung von funktionalen Programmen in Kombinatorsysteme findet sich auch in [Peyton-Jones 87].
Superkombinatorsysteme bieten gute Moglichkeiten zur Parallelisierung des Reduktionsprozesses. Ein erster Ansatz diesbeziiglich sind die in [Hudak, Goldberg 85a/b] eingefiihrten seriellen Kombinatoren. Ein serieller Kombinator ist eine Verfeinerung eines Superkombinators derart, daB im Rumpf des Kombinators explizit angezeigt wird, welche Teilausdriicke parallel auszuwerten sind und welche nicht. Bei der Entscheidung, welche Teilausdriicke parallel ausgewertet werden sollen, wird abgeschatzt, ob eine parallele Auswertung wirklich einen Effizienzgewinn bringt oder ob der Kommunikationsaufwand zu groB ist. Fur jeden Teilausdruck, dessen parallele Auswertung lohnend erscheint, wird ein neuer serieller Kombinator definiert. Auf die Parallelisierung von Superkombinatorsystemen werden wir im zweiten Teil der Arbeit genau eingehen.
Ein Vorteil der Kombinatorsysteme in Bezug auf die Graphreduktion ist eine Vereinfachung des Reduktionsprozesses durch die Beschrankung auf Kombinatorreduktionen. Ein weiterer Vorteil ist die Moglichkeit, die Graphreduktion von Kombinatorsystemen durch Code zu steuern, d.h. die interpretative Graphreduktion durch die sogenannte programmierte Graphreduktion zu ersetzen.
2.5 Programmierte Graphreduktion
Da die Kombinatorriimpfe keine freien Variablen enthalten, ist es moglich, die Kombinatoren in eine feste Maschinencodesequenz zu ubersetzen, die bei AusfUhrung eine Instanz des Kombinatorrumpfes erzeugt, also im wesentlichen eine Kombinatorreduktion durchfiihrt. Natiirlich ist die Ausfiihrung des compilierten Codes schneller als jeder allgemeine Graphinterpreter, da der Code auf das jeweilige Kombinatorprogramm zugeschnitten ist.
Die G-Maschine [Johnsson 84/87, Augustsson 84/87] ist der erste Entwurf einer programmierten Graphreduktionsmaschine. Sie wurde zunachst als Zwischenstufe in einem Compiler fUr LazyML - eine ML-Version mit call-by-name Semantik - eingesetzt. Diese Implementierung erwies sich als extrem schnell im Vergleich zu anderen Implementierungen von ML oder vergleichbaren funktionalen Sprachen. Es wurde sogar eine direkte Hardwarerealisierung der G-Maschine erstellt [Kieburtz 85/87]. Auch die Korrektheit der G-Maschine wurde formal bewiesen [Lester 87/88].
2.5. PROGRAMMIERTE GRAPHREDUKTION 77
In [Fairbairn, Wray 86] ist ebenfalls die Implementierung einer funktionalen Sprache auf der Basis programmierter Graphreduktion beschrieben. Die Vorgehensweise ist sehr ahnlich zur G-Maschine.
1m folgenden werden wir kurz die Struktur und Arbeitsweise der G-Maschine skizzieren und einige Vorteile programmierter Graphreduktion aufzeigen. Die GMaschine ist eine abstrakte Maschine zur Graphreduktion von Kombinatorsystemen. AIle Funktionen sind vollstandig 'gecurried'. Die Kombinatorrumpfe sind aus den Parametervariablen und Konstanten (Basiswerte, Grundoperationen und -konstruktoren) mittels binarer Applikation und let-Konstrukten aufgebaut. Es ist sogar ein "rekursives let-Konstrukt" zur Definition von rekursiven Datenstrukturen zugelassen. Diese rekursiven Datenstrukturen werden in der Maschine durch zyklische Graphen modelliert. Darauf werden wir aber nicht weiter eingehen.
Die G-Maschine besteht aus sieben Komponenten:
(G,B, V,E,C,D,O).
Den Kern der Maschine bilden naturlich die Graphkomponente G, in der der Programmgraph dargestellt und transformiert wird, und der Stack B, uber den der Zugriff auf den Graphen erfolgt. Auf Grund des voIlstandigen 'Currying' aller Funktionen ist der Graph binar. Er wird modelliert als Abbildung der Knotenadressen in die Knoten. Dabei werden folgende Knotentypen unterschieden:
• Datenknoten, wie etwa INT i, BaaL b,
• Konstruktorknoten wie etwa CONS nl n2 zur Beschreibung von Listen, wobei nl bzw. n2 die Knotenadresse des Kopfes bzw. des Restes des Listengraphen ist,
• Applikationsknoten @ nl n2, wobei nl auf den Funktionsgraphen und n2 auf den Argumentgraphen zeigt,
• Funktionsknoten FUN f, wobei f ein Kombinatorname ist und
• Leerknoten HOLE, die zur Konstruktion zyklischer Graphen benotigt werden.
Die Graphtransformationen werden mit Hilfe des Stacks B, auf dem Knotennamen gespeichert werden konnen, vorgenommen. Fur Datenrechnungen (Anwendung von Basisoperationen auf Basiswerte) steht ein spezieIler Wertestack (Value-stack) V mit entsprechenden operativen Fahigkeiten zur Verfiigung. In einer Umgebung (Environment) E ist zu jedem Kombinatornamen die Anzahl der Argumente, die zu einer Kombinatorreduktion gemafi der Definitionsgleichung des
78 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
Kombinators notwendig sind, und die fUr den Kombinator erzeugte G-Maschinencodesequenz gespeichert. Fur die Basisfunktionen finden sich in dieser Komponente ebenfalls entsprechende Angaben. E entspricht dem Programmspeicher, der wahrend einer AusfUhrung unverandert bleibt. Eine weitere Komponente C enthiilt den noch auszufuhrenden Code. C entspricht dem Programmzahler (Instruktionszeiger) in herkommlichen Maschinen. Zur Organisation von rekursiven Aufrufen wird ein Dump D zur Rettung von Stackinhalt 8 und Programmzahler C beim rekursiven Abstieg verwendet. Der Dump ist als Stack organisiert. Die letzte Komponente 0 (Output) der G-Maschine ist ein Ausgabeband, auf das Integerzahlen und Wahrheitswerte vom Programm ausgegeben werden konnen.
1m Prinzip wird fur jede Kombinatordefinition
eine Codesequenz folgender Art erzeugt:
CONSTRUCT-GRAPH [e]; EVAL; UPDATE k + 1; RET k.
Der durch das Ubersetzungsschema CONSTRUCT-GRAPH generierte Code fur e erzeugt zunachst eine Graphinstanz des Rumpfes e des Kombinators, wobei fUr die formalen Parameter Zeiger auf die aktuellen Parameter des Kombinators eingesetzt werden. Zur Zeit der AusfUhrung dieser Codesequenz stehen die Zeiger auf die Argumente des Kombinators auf dem Stack (8) zur Verfugung. Durch die Instruktion EVAL wird die Reduktion des Kombinatorrumpfes angestoBen. Durch die UPDATE-Instruktion wird schlieBlich die Wurzel des Graphen der Kombinatorapplikation mit dem Ergebnis der Reduktion des Kombinatorrumpfes uberschrieben. Die RET-Instruktion beendet die Codesequenz eines Kombinators. Sie bewirkt u.a., daB die Zeiger auf die k Argumente des Kombinators vom 8-Stack geloscht werden.
1m folgenden werden wir die prinzipielle Arbeitsweise der G-Maschine am Beispiel der Reduktion einer Kombinatorapplikation zeigen. Der Kombinator sei etwa durch eine Gleichung
FXIX2X3 = exp
definiert. In der Umgebungskomponente E der G-Maschine findet sich also ein Eintrag der Form
E: ... (F : (3/ cexp ; EVAL; UPDATE 4; RET 3)) ... ,
wobei die erste Komponente des Eintrages fur F die Stelligkeit von F angibt und die zweite Komponente den Code fur F. cexp sei die Codesequenz, die zur Konstruktion von Instanzen von exp erzeugt wurde. Wir betrachten eine Applikation der Form
2.5. PROGRAMMIERTE GRAPHREDUKTION 79
Bild 2.3 zeigt die wesentlichen Arbeitsphasen der G-Maschine bei der Auswertung dieser Applikation.
Die Auswertung der Applikation wird durch den Befehl EVAL angestoBen. Auf der Spitze des Kellers 8 liegt ein Zeiger auf die Wurzel des zu dieser Applikation gehOrenden Graphen. Die Ausfiihrung des Befehls EVAL bewirkt eine Art Unterprogrammsprung: der noch auszufiihrende Code C' und der Kellerinhalt ohne die Kellerspitze, 8', werden auf dem Dump D gesichert. Das Unterprogramm beginnt mit der sogenannten UNWIND-Phase, wahrend der die Folge der Applikationsknoten durchlaufen und auf dem Keller 8 vermerkt wird, bis ein Funktionssymbol (Basisfunktion oder Kombinator) gefunden wird. Sodann wird in der Umgebungskomponente E nachgesehen, wieviele Argumente zur Reduktion benotigt werden, und anhand der Anzahl der Zeiger auf dem Stack festgestellt, ob geniigend Argumente vorhanden sind. Sind nicht geniigend Argumente vorhanden, so wird der Unterprogrammsprung beendet, da eine partielle Applikation nicht reduziert wird. Ansonsten erfolgt, wie in unserem Beispiel, eine Umorganisation des Kellers. Hat das Funktionssymbol die Stelligkeit k, so werden die k oberst en Zeiger auf linke Sohne von Applikationsknoten ersetzt durch Zeiger auf die jeweiligen rechten Sohne der Applikationsknoten, also durch Zeiger auf die Argumentgraphen. Der Code des Funktionssymbols wird aus der Umgebung E in die Codekomponente C geladen. Handelt es sich bei dem Funktionsymbol urn einen Kombinator, wie in unserem Beispiel, so bewirkt die Ausfiihrung der Codesequenz zunachst den Aufbau des Graphen des Kombinatorrumpfes unter Beriicksichtigung der auf dem Keller gegebenen Zeiger auf die aktuellen Parameter. Die Instruktion EVAL stoBt die Reduktion dieses Graphen an. N ach Beendigung dieses Reduktionsprozesses liegt auf der Kellerspitze ein Zeiger auf das Reduktionsergebnis. Der Befehl UPDATE 4 iiberschreibt den Knoten, auf den das vierte Kellerelement unter der Kellerspitze zeigt, mit dem Wurzelknoten des Reduktionsergebnisses, auf den die Kellerspitze zeigt. Der Returnbefehlloscht die Zeiger auf die Argumente der Reduktion vom Keller und beendet den Unterprogrammsprung, indem vom Dump der noch auszufiihrende Code sowie der vorherige Kellerinhalt zuriickgeladen werden. Das Ergebnis des Unterprogrammsprungs wird auf der Kellerspitze angezeigt.
Dieses Prinzip der programmierten Graphreduktion bietet viele Optimierungsmoglichkeiten, die zu einem groBen Teil bereits in der urspriinglichen Version der G-Maschine [Johnsson 84, Augustsson 84J integriert waren. Einer der wesentlichsten Vorteile ist sicherlich, daB man die Konstruktion und Reduktion von Graphen vermeidet, wenn Werte direkt berechnet werden konnen. So wird etwa in der GMaschine fiir den Rumpf des Kombinators
FXIX2X3 = (Xl X X2) + X3
kein Graph aufgebaut, sondern folgende Codesequenz erzeugt, die den Wert des
80 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN
C: EVAL; C' D:D'
C: UNWIND D : (C', 8') : D'
8
8'
StackUmorgani-sation
~ UPDATE
8 G
~ UNWIND
Phase
C : cexp ; EVAL; UPDATE 4; RET 3 D: (C', 8') : D' 8 G
C: RET 3 D: (C', 8') : D' 8 G
I
~ Aufbau und Reduktion des Graphen fur den Rumpf von F
~ RETURN
C : UPDATE 4; RET 3 D: (C',8'): D' 8 G
C: C' D:D' 8 G
V /Z~
Bild 2.3: Einige Arbeitsschritte der G-Maschine
2.5. PROGRAMMIERTE GRAPHREDUKTION
Kombinators direkt berechnet:
PUSH Xl; EVAL; GET; PUSH X2; EVAL; GET; MUL; PUSH X3; EVAL; GET; ADD; MKINT; UPDATE 4; RET 3.
81
Der PUSH-Befehlliidt einen Zeiger auf ein Argument auf die Spitze des Verwaltungskellers S. 1m allgemeinen erhalt er als Parameter den Offset des Argumentzeigers von der Kellerspitze. Der Einfachheit halber schreiben wir hier stattdessen den formalen Parameternamen Xi. Der Befehl GET loot den Basiswert, auf den die Kellerspitze des Verwaltungskellers S zeigt, aus dem Graphen auf den Datenkeller V, auf dem die Datenrechnungen mittels der Basisbefehle ADD, MUL etc. durchgefiihrt werden. Der Befehl MKINT ladt das Ergebnis der Datenrechnung zuriick in einen Graphknoten und schreibt einen Zeiger auf diesen Graphknoten auf die Spitze des Verwaltungskellers. In der G-Maschine werden so oft wie moglich "teure" Graphoperationen durch vergleichsweise "billige" Stackoperationen ersetzt. Urn festzustellen, wann Ausdriicke direkt ausgewertet werden konnen, wird i.a. eine Striktheitsanalyse verwendet [Mycroft 82] [Peyton-Jones 87]. Auf Grund der Ahnlichkeit programmierter Graphreduktion zu konventionellen Implementierungstechniken, ist es auBerdem moglich, konventionelle Codeoptimierungstechniken einzusetzen. Ais Beispiel nennen wir hier nur die spezielle Behandlung von "Tail Recursion" in der Art, daB bei "tail-rekursiven" Aufrufen ein erneuter Unterprogrammsprung vermieden wird [Johnsson 84].
In der G-Maschine ist die UNWIND-Phase zur Bestimmung des Funktionssymbols einer Applikation eine sehr aufwendige Operation. In der "spineless" GMaschine von [Burn, Peyton-Jones, Robson 88] wird versucht, diese Phase sooft wie moglich zu umgehen. Wir werden in Teil III zeigen, daB man bei Zugrundelegung eines Typkonzeptes mit direkter Unterstiitzung kartesischer Typen sowie durch Wahl einer geeigneten Graphstruktur auf diese Phase vollig verzichten kann.
Damit beenden wir den Uberblick iiber die verschiedenen Techniken zur Implementierung funktionaler Sprachen. Wir werden in diesem Buch eine Kombination umgebungsbasierter und programmierter Graph-Reduktion von Kombinatorsystemen vorstellen, bei der die Umgebungsblocke in der Graphstruktur gespeichert werden. Wir folgen damit der in [Hudak, Goldberg 85a] vertretenen Sichtweise, daB Graphreduktion eine Verallgemeinerung konventioneller stack- und umgebungsbasierter Reduktion ist, bei der die AktivierungsblOcke in der Graphstruktur abgelegt werden. Diese Implementierungstechnik eignet sich, wie wir sehen werden, in besonderer Weise, zum Einsatz in einem parallelen verteilten System. Bevor wir auf diese Dinge naher eingehen, geben wir noch einen kurzen Uberblick iiber parallele Architekturen und Multiprozessorsysteme.