我正在研究一种中间语言和一个虚拟机来运行一种具有几个“有问题”属性的功能语言:
- 词法命名空间(闭包)
- 动态增长的调用栈
- 一个慢整数类型(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
“大循环”很简单:
- 取指令
- 增加指令指针(或程序计数器)
- 评估指令
除了 之外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 报告的前 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)。