使用OCaml等功能语言实现直接线程解释器

时间:2010-08-31 00:23:00

标签: functional-programming ocaml interpreter

在C / C ++中,您可以使用函数指针数组实现直接线程解释器。该数组代表您的程序 - 一系列操作。每个操作函数必须以对数组中下一个函数的调用结束,如:

void op_plus(size_t pc, uint8_t* data) {
  *data += 1;
  BytecodeArray[pc+1](pc+1, data); //call the next operation in the array
}

BytecodeArray 是一个函数指针数组。如果我们有一个这些op_plus操作的数组,那么数组的长度将决定我们如何增加数据的内容。 (当然,您需要添加某种终止操作作为数组中的最后一个操作。)

如何在OCaml中实现类似的功能?我可能试图翻译这段代码:我在C ++中使用OCaml函数数组。问题在于我最终会遇到类似的问题:

let op_plus pc data = Printf.printf "pc: %d, data_i: %d \n" pc data;
                        let f = (op_array.(pc+1)) in         
                        f (pc+1) (data+1) ;;

其中op_array是在上面的作用域中定义的数组,然后稍后重新定义它以填充一堆op_plus函数...但是,op_plus函数使用op_array的先前定义。这是鸡和蛋的问题。

3 个答案:

答案 0 :(得分:5)

另一种选择是使用CPS并完全避免使用显式函数数组。在这种情况下,尾调用优化仍然适用。

我不知道你是如何生成代码的,但是让我们做出不合理的假设,即在某些时候你有一系列你想要准备执行的VM指令。每个指令仍然表示为一个函数,但它不是程序计数器,而是接收连续函数。

这是最简单的例子:

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)

用法(查看推断类型):

# #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

更新:

在此模型中引入[条件]分支很容易。 if continuation由两个参数构成:iftrue-continuation和iffalse-continuation,但与其他每个continuation函数具有相同的类型。问题是我们不知道在向后分支的情况下是什么构成了这些延续(向后,因为我们从尾部到头部编译)。这很容易通过破坏性更新来克服(尽管如果您使用高级语言进行编译,可能会有更优雅的解决方案):只需留下“漏洞”并在编译器达到分支目标时填写它们。

示例实现(我使用了字符串标签而不是整数指令指针,但这几乎不重要):

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

为了清晰起见,我使用了两次通过,但很容易看出它可以一次完成。

这是一个简单的循环,可以通过上面的代码编译和执行:

let code = [|
    Label "entry";
    Phi (((<) 0), "body", "exit");
    Label "body";
    Sub 1;
    Jmp "entry";
    Label "exit" |]

执行追踪:

# 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

更新2:

在性能方面,CPS表示可能比基于数组的更快,因为在线性执行的情况下没有间接性。延续函数直接存储在指令闭包中。在基于数组的实现中,它必须首先递增程序计数器并执行数组访问(带有额外的边界检查开销)。

我已经做了一些基准测试来证明它。这是基于数组的解释器的实现:

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

让我们看看它与上面基于CPS的解释器的比较(当然,剥离了所有调试跟踪)。我在Linux / amd64上使用了OCaml 3.12.0本机编译器。每个程序运行5次。

array: mean = 13.7 s, stddev = 0.24
CPS: mean = 11.4 s, stddev = 0.20

因此,即使在紧密循环中,CPS的性能也远远优于阵列。如果我们展开循环并用五个替换一个sub指令,则数字会发生变化:

array: mean = 5.28 s, stddev = 0.065
CPS: mean = 4.14 s, stddev = 0.309

有趣的是,这两个实现实际上都击败了OCaml字节码解释器。以下循环需要17秒才能在我的机器上执行:

for i = 500_000_000 downto 0 do () done

答案 1 :(得分:4)

你不应该重新定义op_array,你应该通过修改它来填写它,使它与你的函数已经引用的op_array相同。不幸的是,您无法在OCaml中动态更改数组的大小。

我看到两个解决方案:

1)如果您不需要更改“指令”的顺序,请在与数组op_array的相互递归中定义它们。 OCaml允许相互递归的函数和值以定义构造函数的应用程序开始。类似的东西:

let rec op_plus pc data = ...
and op_array = [| ... |]

2)或者使用额外的间接:make op_array对指令数组的引用,并在函数中引用(!op_array)。(pc + 1)。稍后,在定义了所有指令后,可以使op_array指向一个正确大小的数组,其中包含您想要的指令。

let op_array = ref [| |] ;;
let op_plus pc data = ... ;;
op_array := [| ... |] ;;

答案 2 :(得分:2)

还有一个选项(如果事先知道大小) - 最初使用void指令填充数组:

let op_array = Array.create size (fun _ _ -> assert false)
let op_plus = ...
let () = op_array.(0) <- op_plus; ...
相关问题