Eine weitere Alternative wäre CPS verwenden und ganz explizite Funktion Array zu vermeiden. In diesem Fall gilt weiterhin die Tail-Call-Optimierung.
Ich weiß nicht, wie Sie den Code generieren, aber lassen Sie uns nicht unvernünftig davon ausgehen, dass Sie irgendwann eine Reihe von VM-Anweisungen haben, die Sie für die Ausführung vorbereiten möchten. Jeder Befehl wird immer noch als Funktion dargestellt, aber anstelle des Programmzählers erhält er eine Fortsetzungsfunktion.
Hier ist das einfachste Beispiel:
type opcode = Add of int | Sub of int
let make_instr opcode cont =
match opcode with
| Add x -> fun data -> Printf.printf "add %d %d\n" data x; cont (data + x)
| Sub x -> fun data -> Printf.printf "sub %d %d\n" data x; cont (data - x)
let compile opcodes =
Array.fold_right make_instr opcodes (fun x -> x)
Usage (Blick auf abgeleitete Typen):
# #use "cpsvm.ml";;
type opcode = Add of int | Sub of int
val make_instr : opcode -> (int -> 'a) -> int -> 'a = <fun>
val compile : opcode array -> int -> int = <fun>
# let code = [| Add 13; Add 42; Sub 7 |];;
val code : opcode array = [|Add 13; Add 42; Sub 7|]
# let fn = compile code;;
val fn : int -> int = <fun>
# fn 0;;
add 0 13
add 13 42
sub 55 7
- : int = 48
UPDATE:
Es ist einfach [bedingt] in diesem Modell Verzweigung einzuführen. if
Fortsetzung wird aus zwei Argumenten konstruiert: iftrue-Fortsetzung und iffalse-Fortsetzung, aber hat den gleichen Typ wie jede andere Fortsetzungsfunktion. Das Problem ist, dass wir nicht wissen, was diese Fortsetzungen im Falle einer Rückwärtsverzweigung ausmacht (rückwärts, weil wir von Schwanz zu Kopf kompilieren). Das ist leicht mit destruktiven Updates zu bewältigen (obwohl eine elegantere Lösung möglich ist, wenn Sie aus einer höheren Sprache kompilieren): lassen Sie einfach "Löcher" und füllen Sie sie später, wenn das Verzweigungsziel vom Compiler erreicht wird.
Beispielimplementierung (ich habe die Verwendung von String-Labels anstelle von ganzzahligen Befehlszeiger gemacht, aber das spielt kaum eine Rolle):
type label = string
type opcode =
Add of int | Sub of int
| Label of label | Jmp of label | Phi of (int -> bool) * label * label
let make_instr labels opcode cont =
match opcode with
| Add x -> fun data -> Printf.printf "add %d %d\n" data x; cont (data + x)
| Sub x -> fun data -> Printf.printf "sub %d %d\n" data x; cont (data - x)
| Label label -> (Hashtbl.find labels label) := cont; cont
| Jmp label ->
let target = Hashtbl.find labels label in
(fun data -> Printf.printf "jmp %s\n" label; !target data)
| Phi (cond, tlabel, flabel) ->
let tcont = Hashtbl.find labels tlabel
and fcont = Hashtbl.find labels flabel in
(fun data ->
let b = cond data in
Printf.printf "branch on %d to %s\n"
data (if b then tlabel else flabel);
(if b then !tcont else !fcont) data)
let compile opcodes =
let id = fun x -> x in
let labels = Hashtbl.create 17 in
Array.iter (function
| Label label -> Hashtbl.add labels label (ref id)
| _ ->())
opcodes;
Array.fold_right (make_instr labels) opcodes id
ich zwei Pässe für Klarheit verwendet habe, aber es ist leicht zu sehen, dass es in einem Durchgang durchgeführt werden.
Hier ist eine einfache Schleife, die oben durch den Code kompiliert und ausgeführt werden können:
let code = [|
Label "entry";
Phi (((<) 0), "body", "exit");
Label "body";
Sub 1;
Jmp "entry";
Label "exit" |]
Execution Trace:
# let fn = compile code;;
val fn : int -> int = <fun>
# fn 3;;
branch on 3 to body
sub 3 1
jmp entry
branch on 2 to body
sub 2 1
jmp entry
branch on 1 to body
sub 1 1
jmp entry
branch on 0 to exit
- : int = 0
UPDATE 2:
Performance-weise, CPS Darstellung ist wahrscheinlich schneller als Array-basierte, weil es keine indirekte Richtung im Falle der linearen Ausführung gibt. Die Fortsetzungsfunktion wird direkt im Anweisungsabschluss gespeichert. In der Array-basierten Implementierung muss der Programmzähler zuerst inkrementiert werden und der Array-Zugriff (mit einem Extra-Überschreibungs-Overhead) zuerst durchgeführt werden.
Ich habe einige Benchmarks gemacht, um es zu demonstrieren. Hier ist eine Implementierung von Array-basierten Interpreter:
type opcode =
Add of int | Sub of int
| Jmp of int | Phi of (int -> bool) * int * int
| Ret
let compile opcodes =
let instr_array = Array.make (Array.length opcodes) (fun _ data -> data)
in Array.iteri (fun i opcode ->
instr_array.(i) <- match opcode with
| Add x -> (fun pc data ->
let cont = instr_array.(pc + 1) in cont (pc + 1) (data + x))
| Sub x -> (fun pc data ->
let cont = instr_array.(pc + 1) in cont (pc + 1) (data - x))
| Jmp pc -> (fun _ data ->
let cont = instr_array.(pc) in cont (pc + 1) data)
| Phi (cond, tbranch, fbranch) ->
(fun _ data ->
let pc = (if cond data then tbranch else fbranch) in
let cont = instr_array.(pc) in
cont pc data)
| Ret -> fun _ data -> data)
opcodes;
instr_array
let code = [|
Phi (((<) 0), 1, 3);
Sub 1;
Jmp 0;
Ret
|]
let() =
let fn = compile code in
let result = fn.(0) 0 500_000_000 in
Printf.printf "%d\n" result
Mal sehen, wie es auf die CPS-basierten Interpreter vergleicht oben (mit allen Debug-gestrippt Tracing, natürlich). Ich habe OCaml 3.12.0 nativen Compiler auf Linux/AMD64 verwendet. Jedes Programm wurde 5 Mal ausgeführt.
array: mean = 13.7 s, stddev = 0.24
CPS: mean = 11.4 s, stddev = 0.20
Also auch in engen Schleife führt CPS wesentlich besser als Array. Wenn wir Schleife abrollen und mit fünf eine sub
Anweisung ersetzen, Zahlen ändern:
array: mean = 5.28 s, stddev = 0.065
CPS: mean = 4.14 s, stddev = 0.309
Es ist interessant, dass beide Implementierungen tatsächlich OCaml Bytecode-Interpreter schlagen. Die folgende Schleife dauert 17 Sekunden auf meinem Rechner auszuführen:
for i = 500_000_000 downto 0 do() done
Wenn Sie eine direkte Gewinde Interpreter auf diese Weise implementieren Sie sehr bald einen Stapelüberlauf bekommen :-) Es gibt keine Möglichkeit, ein direktes Gewinde Interpreter zur Umsetzung in Standard C, deshalb hat GNU berechnete Label-Gotos als Compiler-Erweiterung erfunden. – Lothar
@Lothar "Stapelüberlauf" -> Nicht in der OCaml-Version.Der Aufruf von 'f' in der Frage wird als Tail-Call kompiliert. Ich habe fast darauf hingewiesen, und dann habe ich entschieden, dass es das Thema der Frage nicht war. –