我正在研究一种中间语言和一台虚拟机来运行一种带有一些“有问题”属性的函数式语言:
中间语言是基于堆栈的,具有当前命名空间的简单哈希表。只是为了让你了解它的外观,这里是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
“大循环”很简单:
除了sto
,rcl
以及更多内容外,还有三条函数调用说明:
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报告的前三项:
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
指令消失了(在其他更改后,加速不再可测量)eval
,我在编译时评估常量(编译字节码)。然后eval
只是推送对它们的引用。.if
的语义后,我可以摆脱这些伪函数。它现在是.if
,.else
和.endif
,隐含的getos和块语义类似于.sub
。 (some example code)在实现词法分析器,解析器和字节码编译器之后,速度有点下降,但并非如此。计算MC(-10000)16次使其在1.2秒内评估420万字节码指令。这是a sample of the code it generates(来自this)。
答案 0 :(得分:8)
对可以优化的事情进行了数十年的研究:
答案 1 :(得分:4)
您应该为解释器的各种概念提供有效的算法表示。在散列表上做深度复制看起来像一个可怕的想法,但我发现你已经删除了它。
(是的,使用数组切片的堆栈弹出操作看起来很可疑。你应该确保它们确实具有预期的算法复杂性,否则使用专用数据结构(...堆栈)。我一般都很谨慎使用通用数据结构(如Python列表或PHP哈希表)来实现此用途,因为它们不一定能很好地处理这个特定的用例,但可能是切片确保在所有情况下保证O(1)推送和弹出成本情况。)
处理环境的最佳方法,只要它们不需要具体化,就是使用数字索引而不是变量(de Bruijn指数(0表示最后一个变量)或de Bruijn水平(0表示变量绑定优先。这样你只能为环境保留一个动态调整大小的数组并且访问它非常快。如果你有一流的闭包,你还需要捕获环境,会更昂贵:你必须将它的一部分复制到一个专用的结构中,或者在整个环境中使用一个不可变的结构。只有实验会告诉我,但我的经验是,寻求一个快速可变的环境结构并支付一个封闭结构的高成本要比具有更多簿记的不可变结构更好;当然,您应该进行使用分析以仅捕获封闭中的必要变量。
最后,一旦你找到了与你的算法选择相关的低效率来源,那么热门领域将是:
垃圾收集(绝对是一个难题;如果你不想成为GC专家,你应该认真考虑重用现有的运行时);您可能正在使用主机语言的GC(解释语言中的堆分配被转换为实现语言中的堆分配,具有相同的生命周期),在您显示的代码段中并不清楚;这种策略非常适合以简单的方式获得合理有效的东西
数字实现;当你操纵的整数实际上很小时,有各种各样的黑客都是有效的。您最好的办法是重复使用已投入大量精力的人的工作,因此我强烈建议您重复使用the GMP library。然后,如果有bignum,你也可以重复使用你的宿主语言支持,例如Go的math/big包。
解释器循环的低级设计。在诸如你的“简单字节码”的语言中(每个字节码指令在少量本机指令中转换,而不是具有高级语义的复杂字节码,如Parrot字节码),实际的“字节码上的循环和调度” “代码可能成为瓶颈。关于编写这种字节码调度循环的最佳方法是什么,以避免if / then / else(跳转表)的级联,受益于主机处理器分支预测,简化控制流等,已经进行了大量研究。这被称为threaded code并且有很多(相当简单的)不同的技术:直接线程,间接线程......如果你想研究一些研究,例如Anton Ertl的工作,{ {3}}在2003年,后来The Structure and Performance of Efficient Interpreters。这些技术的好处往往对处理器敏感,所以你应该自己测试各种可能性。
虽然STG的工作很有意思(Peyton-Jones关于编程语言实现的书非常出色),但它有点倾向于懒惰的评估。关于严格函数式语言的高效字节码的设计,我的参考是Xavier Leroy 1990年在ZINC机器上的工作:Context threading: A flexible and efficient dispatch technique for virtual machine interpreters,这是实现ML语言的开创性工作,并且仍然在实现中使用OCaml语言:有字节码和本机编译器,但字节码仍使用美化的ZINC机器。