[Informatik-Fachberichte] Parallele Implementierung funktionaler Programmiersprachen Volume 232 ||...

24
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 Bin- dung 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 Aus- druck wird als Zeichenkette (string) dargestellt. Wahrend einer Reduktion wird die Substitution von Variablennamen durch Ausdriicke explizit durchgefUhrt. Da- bei 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 exi- stieren aber Vorschlage fUr Maschinen, die dieses einfache Prinzip zugrundelegen, R. Loogen, Parallele Implementierung funktionaler Programmiersprachen © Springer-Verlag Berlin Heidelberg 1990

Transcript of [Informatik-Fachberichte] Parallele Implementierung funktionaler Programmiersprachen Volume 232 ||...

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 Bin­dung 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 Aus­druck wird als Zeichenkette (string) dargestellt. Wahrend einer Reduktion wird die Substitution von Variablennamen durch Ausdriicke explizit durchgefUhrt. Da­bei 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 exi­stieren 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 In­effizienz 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 Letzt­endlich 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 Va­riablennamen 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 Clo­sure 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 Bindun­gen 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-by­name 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 Problema­tik 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 Berechnungs­ausdruck 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 Ab­straktionstiefe des jeweiligen Ausdruckes. Bei obiger normal order Reduk­tion treten die Umgebungen U(l) und U(2) durch die Schachtelung der Clo­sures 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 Ab­speicherung von Closures Zeigertechniken verwendet, auf die wir spater noch eingehen werden.

Die erste abstrakte Maschine, die zur Ausfiihrung funktionaler Sprachen ent­worfen wurde, war die SECD-Maschine von Landin [Landin 64]. Diese Maschine implementiert eine applicative-order Reduktionsstrategie auf der Basis der Closure­Technik. Auf Grund der applicative-order Strategie sind Closures lediglich zur Re­prasentierung von Funk~ionsausdriicken (Ausdriicken von funktionalem Typ) not­wendig. Alle anderen Ausdriicke werden ja vor der Bindung an Variable vollstandig zu Konstanten aus AUTr(A) reduziert. Die Berechnungsausdriicke werden in ele­mentaren Maschinencode iibersetzt. Umgebungen werden durch Listen von ( Va­riablen, Wert oder Closure )-Paaren realisiert. Das Kopieren von Umgebungen hat zwar keine Mehrfachauswertungen zur Folge, sollte aber wegen des Kopier­aufwands trotzdem umgangen werden. Dies geschieht im allgemeinen, indem die zweite Komponente einer Closure einen Zeiger auf die Umgebung enthiilt und le­diglich 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 Be­rechnungsausdriicke in einen sehr miichtigen FAM-Maschinencode iibersetzt wer­den, 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 son­dern 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 unausgewer­tete 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 refe­renziert 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 iiber­schreiben. Die modifizierte SECD-Maschine von Burge behandelt Konstruktoren von frei erzeugten Datenstrukturen allerdings wie strikte Basisfunktionen. Unend­liche 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 konven­tionellen 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 Blockstruk­tur naheliegend, aber letztendlich nur moglich, da keine beliebigen Funktionen

64 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN

hoherer Ordnung behandelt werden. In PASCAL werden Funktionen oder Proze­duren zwar als Argumente aber nicht als Werte von Funktionen zugelassen. Bei Eintritt in eine Prozedur oder eine Funktion wird auf dem Laufzeitkeller ein Ak­tivierungsblock 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 Pro­grammsegment:

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 Aktivierungs­block 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 zu­gehOrige 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 ge­samte 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 fol­genden 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 Im­plementierungstechniken imperativer Sprachen auf funktionale Sprachen zu iiber­tragen, da diese Sprachen i.a. beliebige Funktionen hoherer Ordnung, insbesondere funktionswertige Funktionen, und Datenstrukturen in Verbindung mit einer call­by-name Auswertungsstrategie unterstiitzen. Zur Verwaltung der Umgebungen bei der Implementierung funktionaler Sprachen sind also allgemeinere Speicheror­ganisationsformen, 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 Verfah­ren 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 Akti­vierungsblocke so lange auf dem Stack erhalten bleiben, wie die in ihnen abgelegten Umgebungsteile benotigt werden. Urn dies zu erreichen, ist eine starkere Verzeige­rung 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 Auswer­tung 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 Mehrfachaus­wertung 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 Um­gebung nicht unbedingt erstrebenswert, da der Laufzeitkeller eine zentrale Struk­tur 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 Struk­tur, d.h. als gerichteter Graph dargestellt und entsprechend den Reduktionsregeln transformiert. Variablensubstitutionen werden durch Umsetzen von Zeigern rea­lisiert. 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 Reduk­tionsschritt 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 "Sha­ring" 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 exi­stieren 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 Ge­fahr 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-Re­duktion 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 ver­mieden, 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 A­Abstraktion freien Teilausdruck gibt, der E' umfafit.

68 KAPITEL 2. IMPLEMENTIERUNGSTECHNIKEN

ap

/ +

Bild 2.2: Graphreduktion eines Beispielausdruckes

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-Re­duktion

(Ax.e e') '* e[x/e']

die beziiglich Ax.e maximal freien Teilausdriicke von e nicht kopiert, da die Sub­stitution 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 implemen­tiert 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 re­duziert. 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 Mehrfachaus­wertung 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 entschei­denden Vorteil, daB wahrend der Reduktion keine Variablenkonflikte auftreten konnen. Hier zeigt sich allerdings, daB im Zusammenhang mit Funktionen hOhe­rer 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 ver­hindert zwar die Mehrfachauswertung solcher, ist aber eine sehr teure Opera­tion, 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 verwirkli­chen, 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 Erset­zung 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 ver­mieden. 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 maxi­mal 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 Aus­wertung, die von dem oben beschriebenen vereinfachten Graphinterpreter durch­gefiihrt 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 Kon­stante sind. Diese Transformation hat zur Folge, daB es wahrend der Reduktion geniigt, darauf zu achten, daB keine Argumentausdriicke (fiir Variablen substitu­ierte Ausdriicke) mehrfach ausgewertet werden, wie es etwa bei dem oben beschrie­benen 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 A­Abstraktion 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 Ar­gumentausdruck, 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 Kom­binatoren. Auf die Bedeutung und Vorteile von Kombinatoren fUr die Implemen­tierung 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 ein­fachen Sharing von Ausdriicken zur Vermeidung von Mehrfachauswertungen. Zum anderen eignet sich die Graphreduktion im Gegensatz zur umgebungsbasierten Re­duktion 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 wesentli­chen 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 fol­genden Abschnitt sehen werden, kann man auf das Kopieren von Funktionsriimp­fen 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 ver­sucht 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 Varia­blenabstraktion zu einem exponentiellen Wachstum der GroBe der Ausdriicke, was natiirlich fiir eine Implementierung untragbar ist. Turner bewaltigt dieses Pro­blem, indem er einige zusatzliche Kombinatoren erlaubt und wahrend der Uber­setzung 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 Um­gebung, reduziert werden.

Turner sah die Kombinatorreduktion vor allem als Alternative zur umgebungs­basierten Reduktion, da seinen Untersuchungen zufolge die Verwaltung und der Zugriff auf die Umgebungsstruktur zuviel Aufwand erfordern. Bei der Kombina­torreduktion 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 Graphreduk­tionen 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 ele­mentar, da ,B-Reduktionen nur fiir Kombinatoren durchgefiihrt zu werden brau­chen und deren Riimpfe eine sehr einfache Struktur haben. Insbesondere enthal­ten die Kombinatoren keine nicht-trivialen maximal freien Ausdriicke, d.h. SKI­Kombinatorreduktion 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 Kombinator­reduktionen steuert. In [Hudak, Kranz 84] wird ein Compiler zur Implementie­rung einer funktionalen Sprache nach dem call-by-name Prinzip vorgestellt, der SKI-Kombinatoren als Zwischenstufe zur Optimierung der zu iibersetzenden Pro­gramme 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, Nor­man 80] und 'Burroughs NORMA Machine' [Richards 85, Scheevel 86]. Die SKI­Kombinatortechnik wurde auch im Hinblick auf die Parallelisierbarkeit des Reduk­tionsprozesses 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 CPU­Instruktion, 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, Os­mon 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 Kom­munikationen 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 [Ander­son, Hankin, Kelly, Osmon, Shute 87]. Dadurch ist es moglich, mehrere Kom­binatorreduktionen zu einem Proze:B zusammenzufassen. Ahnlich gehen [Maurer, Oberhauser 85] vor, wobei sie anstatt des zusatzlichen P Kombinators mit Anno­tationen an den Kombinatorgraphen arbeiten.

Obwohl der auf einer fest en Menge von Kombinatoren beruhende Kombina­torkalkiil verschiedene Vorteile wie etwa die Einfachheit des Reduktionsmechanis­mus und die "fully lazy evaluation" aufweist, ist das Zerstiickeln der Programm­ausfiihrung 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 Glei­chungssystem

wobei ei (1 ~ i ~ r) aus Konstanten, den Variablen XiI, •.. ,Xik, und den Kombina­tornamen 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 sicher­gestellt, daB die Berechnungsausdriicke rein applikativ sind und wahrend des Re­duktionsprozesses 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 Kombi­natorreduktion 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 entspre­chendes 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 Al­gorithmus 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 Aus­druck) 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 Superkom­binatoren [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, Gold­berg 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 sol­len, 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 Kombina­tor 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 Kombinator­reduktionen. Ein weiterer Vorteil ist die Moglichkeit, die Graphreduktion von Kombinatorsystemen durch Code zu steuern, d.h. die interpretative Graphreduk­tion 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 Aus­fUhrung 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 jewei­lige 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 Spra­chen. 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 Vorge­hensweise 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 G­Maschine ist eine abstrakte Maschine zur Graphreduktion von Kombinatorsyste­men. 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 Datenstruk­turen 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 Pro­grammgraph 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 Knotena­dressen 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, wo­bei nl bzw. n2 die Knotenadresse des Kopfes bzw. des Restes des Listengra­phen 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 wer­den.

Die Graphtransformationen werden mit Hilfe des Stacks B, auf dem Knoten­namen gespeichert werden konnen, vorgenommen. Fur Datenrechnungen (An­wendung 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-Maschi­nencodesequenz gespeichert. Fur die Basisfunktionen finden sich in dieser Kom­ponente 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 (In­struktionszeiger) 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 In­tegerzahlen 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 er­zeugt zunachst eine Graphinstanz des Rumpfes e des Kombinators, wobei fUr die formalen Parameter Zeiger auf die aktuellen Parameter des Kombinators einge­setzt werden. Zur Zeit der AusfUhrung dieser Codesequenz stehen die Zeiger auf die Argumente des Kombinators auf dem Stack (8) zur Verfugung. Durch die In­struktion EVAL wird die Reduktion des Kombinatorrumpfes angestoBen. Durch die UPDATE-Instruktion wird schlieBlich die Wurzel des Graphen der Kombi­natorapplikation mit dem Ergebnis der Reduktion des Kombinatorrumpfes uber­schrieben. 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 Bei­spiel 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 Kon­struktion 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 Unter­programmsprung: 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 Applikations­knoten durchlaufen und auf dem Keller 8 vermerkt wird, bis ein Funktionssymbol (Basisfunktion oder Kombinator) gefunden wird. Sodann wird in der Umgebungs­komponente E nachgesehen, wieviele Argumente zur Reduktion benotigt werden, und anhand der Anzahl der Zeiger auf dem Stack festgestellt, ob geniigend Ar­gumente 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 Kel­lers. 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 UP­DATE 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 Re­duktion vom Keller und beendet den Unterprogrammsprung, indem vom Dump der noch auszufiihrende Code sowie der vorherige Kellerinhalt zuriickgeladen wer­den. Das Ergebnis des Unterprogrammsprungs wird auf der Kellerspitze angezeigt.

Dieses Prinzip der programmierten Graphreduktion bietet viele Optimierungs­moglichkeiten, die zu einem groBen Teil bereits in der urspriinglichen Version der G-Maschine [Johnsson 84, Augustsson 84J integriert waren. Einer der wesentlich­sten Vorteile ist sicherlich, daB man die Konstruktion und Reduktion von Graphen vermeidet, wenn Werte direkt berechnet werden konnen. So wird etwa in der G­Maschine 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'

Stack­Um­organi-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 Reduk­tion 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 Verwal­tungskellers S. 1m allgemeinen erhalt er als Parameter den Offset des Argument­zeigers 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 Da­tenkeller V, auf dem die Datenrechnungen mittels der Basisbefehle ADD, MUL etc. durchgefiihrt werden. Der Befehl MKINT ladt das Ergebnis der Datenrech­nung zuriick in einen Graphknoten und schreibt einen Zeiger auf diesen Graph­knoten auf die Spitze des Verwaltungskellers. In der G-Maschine werden so oft wie moglich "teure" Graphoperationen durch vergleichsweise "billige" Stackope­rationen 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 konventio­nellen Implementierungstechniken, ist es auBerdem moglich, konventionelle Code­optimierungstechniken 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 Funktions­symbols einer Applikation eine sehr aufwendige Operation. In der "spineless" G­Maschine von [Burn, Peyton-Jones, Robson 88] wird versucht, diese Phase sooft wie moglich zu umgehen. Wir werden in Teil III zeigen, daB man bei Zugrunde­legung 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 Im­plementierung funktionaler Sprachen. Wir werden in diesem Buch eine Kombi­nation umgebungsbasierter und programmierter Graph-Reduktion von Kombina­torsystemen vorstellen, bei der die Umgebungsblocke in der Graphstruktur ge­speichert 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 Graph­struktur 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.