我正在开发一种中间语言和虚拟机,用于运行一个具有一些"棘手"特性的函数式语言:
- 词法命名空间(闭包)
- 动态增长的调用栈
- 缓慢的整型(大数)
这个中间语言是基于栈的,使用一个简单的哈希表来存储当前命名空间。为了让你了解它是什么样子,这里有一个麦卡锡91函数的例子:
# 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秒钟(在优化去除深度复制之后,它会添加近乎1秒)。我的问题是:有哪些常见的技术可用于优化这种语言的解释?是否有任何易于实施的特别有效方法?
我在列表(参数、各种堆栈、命名空间的映射切片等)中使用了切片,所以我在很多地方都做了类似的事情:
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
,具有类似于.sub
的隐式goto和块语义。(一些示例代码)
在实现词法分析器、解析器和字节码编译器之后,速度略有下降,但并不太糟糕。在1.2秒内计算MC(-10000)16次,使其评估420万个字节码指令。这里是它生成的代码样例(来自这里)。