1.08k likes | 1.2k Views
Effiziente Virtuelle Maschinen für funktionale Programmiersprachen. Xavier Leroy, The ZINC experiment: an economical implementation of the ML language. Technical report 117, INRIA, 1990. Motivation Eigenschaften funktionaler Sprahen N-ary / Unary / curried functions Die abstrakte Maschine
E N D
EffizienteVirtuelle Maschinenfür funktionale Programmiersprachen Xavier Leroy, The ZINC experiment:an economical implementation of the ML language.Technical report 117, INRIA, 1990
Motivation Eigenschaften funktionaler Sprahen N-ary / Unary / curried functions Die abstrakte Maschine Naive Auswertung Analyse der Auswertung Echte Mehrfachapplikation Probleme Analyse der Auswertung Darstellung von Funktionen Das Instruction Set Die Übersetzungsfunktion Operationale Semantik Unterversorgung Überversorgung Optimierungen Darstellung von Werten Das reale Instruction Set Benchmarks Überblick
Motivation • Wie können Programme in funktionalen Sprachen effizient und plattformunabhängig ausgeführt werden ? • native Code Compiler • hohe Ausführungsgeschwindigkeit • Programmdarstellung ist plattformabhängig • Compiler müssen für jede Ziel-Plattform neu entwickelt werden • Virtuelle Maschine • VM selbst ist plattformabhängig, aber leicht portierbar • Bytecode ist plattformunabhängig • Overhead der VM kann minimiert werden
Eigenschaften funktionaler Sprachen • Abstraktion • Curried functions: val f = fn a => fn b => a+b • Unary functions: val f = fn (a,b) => a+b • N-ary functions ? • Applikation • mit Unterversorgung von Argumenten • mit Überversorgung von Argumenten • Schleifen werde durch rekursive Funktionen dargestellt.
N-ary functions • n-ary functions sind Funktionen mit mehr als einem Argument.Bei der Ausführung werden diese zurückübersetzt in einfachere Funktionen. Es gibt dazu zwei Möglichkeiten fun f (a b) = a+b • als unary functions mit Argumenten-Tupelfun f (a,b) = a+b • oder als curried functionsval f = fn a => fn b => a+b
Vergleich von Unary / curried functions • Nachteil bei Argumenten-Tupel: • Allokation des Argumenten-Tupel auf dem Heap bei jedem Aufruf • Vorteil von curried functions: • Curried functions sind bei partieller Anwendung verwendbar: • val add = fn a => fn b => a+b • map (add 5) [1,2,3] • Partielle Applikation ist bei Argumenten-Tupel nicht möglich.=> Curried functions sollten effizient implementiert werden!=> N-ary functions als curried functions übersetzbar.
Motivation Eigenschaften funktionaler Sprahen N-ary / Unary / curried functions Die abstrakte Maschine Naive Auswertung Analyse der Auswertung Echte Mehrfachapplikation Probleme Analyse der Auswertung Darstellung von Funktionen Das Instruction Set Die Übersetzungsfunktion Operationale Semantik Unterversorgung Überversorgung Optimierungen Darstellung von Werten Das reale Instruction Set Benchmarks Überblick
Die abstrakte Maschine • Die abstrakte Maschine besteht aus folgenden Komponenten: • Der Akkumulator enthält Zwischenergebnisse • Der Code-Zeiger zeigt auf die nächste auszuführende Instruktion. • Die Umgebung verwaltet alle aktuellen Bezeichnerbindungen. • Auf dem Stack werden Aufrufparameter, Ergebnis-Werte und Closures von unterbrochenen Auswertungen abgelegt. • Die Semantik der Maschine wird bestimmt durch: • Das Instruction Set der Maschine. • Die Operationale Semantik bestimmt wie diese Instruktionen in Abhängigkeit vom aktuellen Inhalt von Umgebung und Stack ausgewertet werden.
Naive Auswertung • Bei der naiven Auswertung wird jeweils ein Parameter angewandt. • M N1 N2 ==> (M N1) N2 • Über- und Unterversorgung betrachten wir später!
Auswertungsbeispiel • (fn a => fn b => a + x) 3 x • Um die erste Applikation auszuführen, wird der Folgeausdruck als Closure auf den Stack gelegt.
Auswertungsbeispiel • (fn a => fn b => a + x) 3 • Der Parameter wird in den Akkumulator geladen.
Auswertungsbeispiel • (fn a => fn b => a + x) • … und auf den Stack gelegt.
Auswertungsbeispiel • fn a => fn b => a + x • Die Abstraktion wird ausgeführt, indem das Argument von Stack genommen wird und als „a“ in die Umgebung eingefügt.
Auswertungsbeispiel • fn b => a + x • Bei einer weiteren Abstraktion ist die Auswertung zunächst abgebrochen. Das Zwischenergebnis wird in einer Closure in den Akkumulator gepackt.
Auswertungsbeispiel • Die neue Closure ersetzt die unterbrochene Auswertung auf dem Stack.Diese wird fortgesetzt.
Auswertungsbeispiel • x • Das nächste Argument wird in den Akkumulator geladen.
Auswertungsbeispiel • … und auf den Stack gelegt. Die unterbrochene Auswertung wird fortgesetzt.
Auswertungsbeispiel • fn b => a + x • Die Abstraktion wird ausgeführt, indem das Argument von Stack genommen wird und als „b“ in die Umgebung eingefügt.
Auswertungsbeispiel • a + x • „a“ wird aus der Umgebung in den Akkumulator geladen.
Auswertungsbeispiel • + x • … und auf den Stack gelegt.
Auswertungsbeispiel • + x • „x“ wird aus der Umgebung in den Akkumulator geladen.
Auswertungsbeispiel • + • Der oberste Wert auf dem Stack wird zum Akkumulator addiert.
Auswertungsbeispiel • Der Wert im Akkumulator ist das Ergebnis.
Analyse der Auswertung • Die Auswertung von Mehrfach-Applikationen erfolgt schrittweise: • Bei Left-To-Right Evaluation: • ((((M) (N1)) (N2)) (N3)) • Reihenfole: M, N1, (M N1)=a, N2, (a N2)=b, N3, (b N3) • Bei der Auswertung jeder Applikation [außer der letzten] entsteht eine Closure für das bisher erzeugte Zwischenergebnis.n-Argumente => n-1 Closures.
Probleme dieser Auswertung • Es werden für k Argumente mindestens k - 1 Closures verwendet. • Closures werden auf dem Heap angelegt. • Allokationen sind zeitintensiv • Die Closures werden [teilweise] nur einmal verwendet. • Der Speicherbedarf steigt. • Die Garbage Collection wird häufiger verwendet. • Wie kann man diese Closures meiden ?
Motivation Eigenschaften funktionaler Sprahen N-ary / Unary / curried functions Die abstrakte Maschine Naive Auswertung Analyse der Auswertung Echte Mehrfachapplikation Probleme Analyse der Auswertung Darstellung von Funktionen Das Instruction Set Die Übersetzungsfunktion Operationale Semantik Unterversorgung Überversorgung Optimierungen Darstellung von Werten Das reale Instruction Set Benchmarks Überblick
„Echte“ Mehrfachapplikation - Vorteile • Alle Argumente könnten vor der Applikation auf den Stack gelegt werden. Die Auswertung muss nicht nach jeder Teilapplikation unterbrochen werden. • Die Reihenfolge bei 3-fach-Applikation M N1 N2 N3 ist: • bei Einzelapplikationen: M, N1, (M N1)=a, N2, (a N2)=b, N3, (b N3) • bei Mehrfachapplikation: M, N1, N2, N3, (M N1 N2 N3) • Die Applikation aller Argumente erzeugt keine unnötigen Closures.
Probleme der Mehrfachapplikation • Mehrere Einzelapplikationen sollten die gleiche Auswertungsreihenfolge haben wie eine Mehrfachapplikation.[Gilt nicht für „Zwischenergebnisse“ !] • Problem: Left-To-Right Evaluation Order • M N1 N2 => M, N1, N2, (M N1 N2) • (M N1) N2 => [M, N1, (M N1)=a], N2, (a N2) • Lösung: Right-To-Left Evaluation Order • M N1 N2 => N2, N1, (M N1 N2) • (M N1) N2 => N2, [N1, (M N1)=a], (a N2)
Beispiel für Inkonsistenz • exception Abs exception Right • val f = fn x => (raise Abs; fn y => y) • Problem: Left-To-Right Evaluation Order • f 1 (raise Right) => raise Right • ( f 1 ) (raise Right) => raise Abs • Lösung: Right-To-Left Evaluation Order • f 1 (raise Right) => raise Right • ( f 1 ) (raise Right) => raise Right
Auswertungsbeispiel • (fn a => fn b => a + x) 3 x • Zuerst wird das rechte Argument in den Akkumulator geladen.
Auswertungsbeispiel • (fn a => fn b => a + x) 3 • … und auf den Stack gelegt.
Auswertungsbeispiel • (fn a => fn b => a + x) 3 • Dann wird das nächste Argument in den Akkumulator geladen.
Auswertungsbeispiel • fn a => fn b => a + x • … und auf den Stack gelegt.
Auswertungsbeispiel • fn a => fn b => a + x • Die Abstraktion wird ausgeführt, indem das Argument von Stack genommen wird und als „a“ in die Umgebung eingefügt.
Auswertungsbeispiel • fn b => a + x • Die Abstraktion wird ausgeführt, indem das Argument von Stack genommen wird und als „b“ in die Umgebung eingefügt.
Auswertungsbeispiel • a + x • „a“ wird aus der Umgebung in den Akkumulator geladen.
Auswertungsbeispiel • + x • … und auf den Stack gelegt.
Auswertungsbeispiel • + x • „x“ wird aus der Umgebung in den Akkumulator geladen.
Auswertungsbeispiel • + • Der oberste Wert auf dem Stack wird zum Akkumulator addiert.
Auswertungsbeispiel • Der Wert im Akkumulator ist das Ergebnis.
Analyse der Auswertung • Bei dieser Auswertung wurden KEINE Closures erzeugt.
Motivation Eigenschaften funktionaler Sprahen N-ary / Unary / curried functions Die abstrakte Maschine Naive Auswertung Analyse der Auswertung Echte Mehrfachapplikation Probleme Analyse der Auswertung Darstellung von Funktionen Das Instruction Set Die Übersetzungsfunktion Operationale Semantik Unterversorgung Überversorgung Optimierungen Darstellung von Werten Das reale Instruction Set Benchmarks Überblick
Darstellung von Funktionen • Funktionen werden durch λ-Abstraktionen mit de-Bruijn-Darstellung ersetzt: • val f = fn a => fn b => a + b • val f = λ . λ . <1> + <0> • val f = fn a => fn b => fn x => a + b • val f = λ . λ . λ . <2> + <1> • Die Bezeichner von neu gebundenen Abstraktionen entfallen.Außerdem reduzieren sich Umgebungen von Funktionen Bezeichner -> Wert auf einfachere Werte-Listen.
Darstellung von Funktionen • Funktionen werden als Werte in Form von Closures dargestellt. • Eine Closure besteht aus einer Umgebung und einer Code-Pointer. • Beispiel (ML-Notation): • val f = fn a => fn b => fn c => a + b • f : {}, o • val g = f 5 3 • g : {a:5, b:3}, o
Darstellung von Funktionen • Funktionen werden als Werte in Form von Closures dargestellt. • Eine Closure besteht aus einer Umgebung und einer Code-Pointer. • Beispiel (de-Bruijn-Notation): • val f = λ . λ . λ . <2> + <1> • f : [], o • val g = f 5 3 • g : [3, 5], o
Das Instruction Set • Access(n) - liest das n. Element aus der Umgebung in den Akkumulator. • Reduce(c) - führt c aus und legt den Wert des Akkumulators auf den Stack. • Return - Beendet ein Auswertung eines Ausdrucks • Grab - nimmt das oberste Element [das nächste Argument] vom Stack und fügt sie als neues erstes Element in die Umgebung ein. • ConstInt(i) - legt die Integer-Konstante i in den Akkumulator. • AddInt - Addiert das oberste Element des Stacks zum Akkumulator.
Die Übersetzungsfunktion [ ] • [ M N ] --> Reduce( [ N ]; Return); [ M ] • [ <n> ] --> Access(n) • [ λ . N ] --> Grab; [ N ] • [ i ] --> ConstInt(i) • [ N1 + N2] --> Reduce( [ N2]; Return );[ N1]; AddInt • Jedes Programm endet mit Return.
Übersetzungsbeispiel • ( fn a => fn b => a + b ) ( ( fn x => x ) 1 ) 2 • ( λ . λ . <1> + <0> ) ( ( λ . <0> ) 1 ) 2 • [( λ . λ . <1> + <0> ) ( ( λ . <0> ) 1 ) 2 ]; Return
Übersetzungsbeispiel • ( fn a => fn b => a + b ) ( ( fn x => x ) 1 ) 2 • ( λ . λ . <1> + <0> ) ( ( λ . <0> ) 1 ) 2 • Reduce( [ 2 ]; Return );[( λ . λ . <1> + <0> ) ( ( λ . <0> ) 1 ) ]; Return
Übersetzungsbeispiel • ( fn a => fn b => a + b ) ( ( fn x => x ) 1 ) 2 • ( λ . λ . <1> + <0> ) ( ( λ . <0> ) 1 ) 2 • Reduce( ConstInt(2); Return );[( λ . λ . <1> + <0> ) ( ( λ . <0> ) 1 ) ]; Return