12

我正在研究一种中间语言和一个虚拟机来运行一种具有几个“有问题”属性的功能语言:

  • 词法命名空间(闭包)
  • 动态增长的调用栈
  • 一个慢整数类型(bignums)

中间语言是基于堆栈的,具有当前命名空间的简单哈希表。为了让您了解它的外观,这里是McCarthy91函数:

# McCarthy 91: M(n) = n - 10 if n > 100 else M(M(n + 11))
.sub M
    args
    sto n

    rcl n
    float 100
    gt
    .if
        .sub
            rcl n
            float 10
            sub
        .end
        .sub
            rcl n
            float 11
            add
            list 1
            rcl M
            call-fast
            list 1
            rcl M
            tail
        .end
    call-fast
.end

“大循环”很简单:

  • 取指令
  • 增加指令指针(或程序计数器
  • 评估指令

除了 之外storcl还有更多函数调用指令:

  • call复制命名空间(深拷贝)并将指令指针压入调用堆栈
  • call-fast是一样的,但只会创建一个浅拷贝
  • tail基本上是一个'goto'

实现非常简单。为了给你一个更好的主意,这里只是“大循环”中间的一个随机片段(更新,见下文)

    } else if inst == 2 /* STO */ {
        local[data] = stack[len(stack) - 1]
        if code[ip + 1][0] != 3 {
            stack = stack[:len(stack) - 1]
        } else {
            ip++
        }
    } else if inst == 3 /* RCL */ {
        stack = append(stack, local[data])
    } else if inst == 12 /* .END */ {
        outer = outer[:len(outer) - 1]
        ip = calls[len(calls) - 1]
        calls = calls[:len(calls) - 1]
    } else if inst == 20 /* CALL */ {
        calls = append(calls, ip)
        cp := make(Local, len(local))
        copy(cp, local)
        outer = append(outer, &cp)
        x := stack[len(stack) - 1]
        stack = stack[:len(stack) - 1]
        ip = x.(int)
    } else if inst == 21 /* TAIL */ {
        x := stack[len(stack) - 1]
        stack = stack[:len(stack) - 1]
        ip = x.(int)

问题是这样的:用 -10000 的值调用 McCarthy91 16 次,几乎没有区别,3 秒(在优化掉深层复制后,增加了近一秒)。

我的问题是:优化这种语言的解释有哪些常用技术?有没有低垂的果实?

我在列表中使用了切片(参数、各种堆栈、命名空间的映射切片......),所以我到处都在做这种事情:call_stack[:len(call_stack) - 1]. 现在,我真的不知道是什么代码让这个程序变慢了。任何提示将不胜感激,尽管我主要是在寻找一般的优化策略。

在旁边:

通过规避我的调用约定,我可以大大减少执行时间。该list <n>指令获取堆栈的 n 个参数并将它们的列表推回堆栈,该args指令从该列表中弹出并将每个项目推回堆栈。这首先是检查是否使用正确数量的参数调用函数,其次是能够调用具有可变参数列表(即(defun f x:xs))的函数。删除它,并添加一个sto* <x>替换的指令sto <x>; rcl <x>,我可以将它缩短到 2 秒。仍然不聪明,无论如何我必须有这个list/生意。args:)

另一个问题(我知道这是一个很长的问题,抱歉):

用 pprof 分析程序告诉我的很少(我是 Go 新手,以防不明显):-)。这些是 pprof 报告的前 3 项:

  16   6.1%   6.1%       16   6.1% sweep               pkg/runtime/mgc0.c:745
   9   3.4%   9.5%        9   3.4% fmt.(*fmt).fmt_qc   pkg/fmt/format.go:323
   4   1.5%  13.0%        4   1.5% fmt.(*fmt).integer  pkg/fmt/format.go:248

这些是我到目前为止所做的更改:

  • 我删除了哈希表。相反,我现在只传递指向数组的指针,并且只在需要时有效地复制本地范围。
  • 我用整数操作码替换了指令名称。以前,我浪费了相当多的时间来比较字符串。
  • 指令消失了(在call-fast其他更改之后加速不再可测量)
  • 我没有“int”、“float”和“str”指令,而是只有一个eval,我在编译时评估常量(即编译字节码)。然后eval只是推送对它们的引用。
  • 更改 的语义后.if,我可以摆脱这些伪函数。现在是.if,.else.endif, 隐式 goto 和块语义类似于.sub. (一些示例代码

在实现了词法分析器、解析器和字节码编译器之后,速度下降了一点,但不是很严重。计算 MC(-10000) 16 次使其在 1.2 秒内评估 420 万条字节码指令。这是它生成的代码示例(来自this)。

整个东西都在github上

4

2 回答 2

8

对于可以优化的事物,已有数十年的研究:

于 2012-06-12T12:55:25.040 回答
5

您应该对解释器的各种概念有有效的算法表示。在哈希表上进行深度复制看起来是个糟糕的主意,但我看到您已经删除了它。

(是的,您使用数组切片的堆栈弹出操作看起来很可疑。您应该确保它们确实具有预期的算法复杂性,或者使用专用的数据结构(......堆栈)。我通常对使用 all- 持谨慎态度用于这种用途的数据结构,例如 Python 列表或 PHP 哈希表,因为它们不一定被设计为很好地处理这个特定的用例,但切片可能在所有情况下都保证 O(1) 的推送和弹出成本。)

处理环境的最佳方法,只要它们不需要被具体化,就是使用数字索引而不是变量(de Bruijn 索引(0 表示变量界限最后),或 de Bruijn 级别(0 表示变量界限首先)。这样你只能为环境保留一个动态调整大小的数组并且访问它非常快。如果你有一流的闭包,你还需要捕获环境,这将更加昂贵:您必须将它的一部分复制到专用结构中,或者为整个环境使用非可变结构。只有实验才能证明,但我的经验是,选择一个快速可变的环境结构并为闭包构建支付更高的成本,比拥有一个一直有更多簿记的不可变结构要好;当然,您应该进行使用分析以仅捕获闭包中的必要变量。

最后,一旦您根除与您的算法选择相关的低效率来源,热门领域将是:

  • 垃圾收集(绝对是一个很难的话题;如果你不想成为 GC 专家,你应该认真考虑重用现有的运行时);您可能正在使用宿主语言的 GC(您的解释语言中的堆分配被翻译成您的实现语言中的堆分配,具有相同的生命周期),在您显示的代码片段中并不清楚;这种策略非常适合以简单的方式获得相当有效的东西

  • 数字实现;当您操作的整数实际上很小时,有各种技巧可以提高效率。最好的办法是重用为此投入大量精力的人的工作,因此我强烈建议您重用例如GMP 库。再说一次,你也可以重用你对 bignum 的宿主语言支持,如果它有的话,在你的情况下是 Go 的math/big包。

  • 解释器循环的低级设计。在诸如您的具有“简单字节码”的语言中(每个字节码指令都被翻译成少量的本机指令,而不是具有高级语义的复杂字节码,例如 Parrot 字节码),实际的“循环和分派字节码" 代码可能是一个瓶颈。关于编写这种字节码调度循环的最佳方式,避免 if/then/else(跳转表)的级联,受益于主机处理器分支预测,简化控制流等,已经有相当多的研究。这被称为线程代码,有很多(相当简单)不同的技术:直接线程,间接线程......如果你想研究一些研究,例如 Anton Ertl 的工作,The Structure and Performance of Efficient Interpreters in 2003, and later Context threading: 一种灵活高效的虚拟机解释器调度技术。这些技术的好处往往对处理器相当敏感,因此您应该自己测试各种可能性。

虽然 STG 的工作很有趣(Peyton-Jones 关于编程语言实现的书也很出色),但它在某种程度上倾向于惰性求值。关于为严格的函数式语言设计高效字节码,我的参考是 Xavier Leroy 1990 年在 ZINC 机器上的工作:ZINC 实验:ML 语言的经济实现,这是实现 ML 语言的开创性工作,并且是在 OCaml 语言的实现中仍在使用:既有字节码也有本机编译器,但字节码仍然使用美化的 ZINC 机器。

于 2012-06-16T17:33:02.583 回答