Реализация интерпретатора с прямым потоком на функциональном языке, таком как OCaml - PullRequest
8 голосов
/ 31 августа 2010

В 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? Возможно, я пытаюсь перевести этот код слишком буквально: я использовал массив функций OCaml, как в C ++. Проблема в том, что я продолжаю получать что-то вроде:

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 ]

5 голосов
/ 02 сентября 2010

Другой альтернативой будет использование 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

UPDATE:

В этой модели легко ввести [условное] ветвление. if продолжение строится из двух аргументов: iftrue-продолжение и iffalse-продолжение, но имеет тот же тип, что и любая другая функция продолжения. Проблема в том, что мы не знаем, что составляет эти продолжения в случае обратного ветвления (назад, потому что мы компилируем от хвоста до головы). Это легко преодолеть с помощью деструктивных обновлений (хотя, возможно, более элегантное решение возможно, если вы компилируете из языка высокого уровня): просто оставьте «дыры» и заполните их позже, когда компилятором будет достигнута цель ветвления.

Пример реализации (я использовал строковые метки вместо целочисленных указателей команд, но это вряд ли имеет значение):

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, описанным выше (разумеется, со всей трассировкой отладки). Я использовал собственный компилятор OCaml 3.12.0 в Linux / amd64. Каждая программа была запущена 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
4 голосов
/ 31 августа 2010

Вы не должны переопределять op_array, вы должны заполнить его инструкциями, изменив его на месте так, чтобы оно совпадало с op_array, к которому ваши функции уже относятся.К сожалению, вы не можете динамически изменять размер массива в OCaml.

Я вижу два решения:

1), если вам не нужно изменять последовательность «инструкций»,определить их во взаимной рекурсии с массивом op_array.OCaml позволяет определять взаимно рекурсивные функции и значения, которые начинаются с применения конструктора.Что-то вроде:

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

2) Или используйте дополнительную косвенную ссылку: сделайте op_array ссылку на массив инструкций и обратитесь в функциях к (! Op_array). (Pc + 1).Позже, после того, как вы определили все инструкции, вы можете op_array указать на массив правильного размера, полный инструкций, которые вы намереваетесь.

let op_array = ref [| |] ;;
let op_plus pc data = ... ;;
op_array := [| ... |] ;;
2 голосов
/ 31 августа 2010

Еще один вариант (если размер известен заранее) - сначала заполните массив пустыми инструкциями:

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