我不想优化VM,只想要eval

2018-03-13

之前觉得,语言的巴别塔应该通过建立一层公共抽象层去消除,所以我是支持 .NET/CLR 这类事物的。如果不管用什么语言,写出来的库,都可以相互调用,天下大同,挺美好的。

最近有了一点新的感悟:不需要VM,只要eval就够了。这种感悟就类似于突然理解了 "lisp不需要机器" 这句话。

先解释什么是可运行。真正的硬件,只懂得0和1,所以只有生成 native binary 了才是可运行的。如果实现了一个解释器,然后去解释脚本语言,虽然这个脚本语言不是二进制的,但它还是可以算作可运行。对于一门编译型语言,源代码不能算是可运行的,因为不经过编译,它并不能直接跑。函数是可运行的,因为只要调用它,它就被执行了。

什么是编译呢?编译就是把不可运行的东西,转化成可运行的。比如 C 或者 Go 的源代码,都是不可运行的,需要经过编译,生成二进制文件。其实编译并不是一个绝对的概念,我觉得只要把不可运行的东西,翻译到能够被运行,这个过程在某种程度上都算作编译。

写编译器很难。解释器性能差。VM是折中方案。

这里不得不说的,就是在软件开发里面,分层的重要性。“计算机科学领域的任何问题都可以通过增加一个简介的中间层来解决”。合理分层可以控制复杂度。比如说 VM + jit 的方案,挺复杂的,但是复杂性度上升是平滑的,这一点很重要!

通过 VM 这一层,屏蔽更下面的细节。对上面只要暴露字节码,而下层实现怎么做优化,就变成另外的问题了,上层是不用关心的。每一层只解决特定的问题,事情被拆小之后,就简单多了。坏处当然也不是没有。过多的层,也会引入混乱。比如本来希望在VM层能实现天下大同,可实际上各个语言自己撸自己的一套,白白浪费了很多人力物力。想想这个世界上有多少的 VM 实现?

引入 VM 之后,下一个要考虑的就是优化性能了。

VM 层引入的抽象,就是性能损失的根源。因为在某个语言中实现 VM,没法直接利用硬件那一层:寄存器分配,内存次数访问,缓存友好性,实际执行的指令条数...

func (v *VM) add() {
    v.stk[v.sp-1] = v.stk[v.sp-1] + v.stk[v.sp]
}

在 VM 里面一条虚拟的指令,生成出来变成了这么多的机器指令:

MOVQ    "".VM+16(SP), AX
MOVQ    24(AX), CX
LEAQ    -1(CX), DX
MOVQ    8(AX), BX
MOVQ    (AX), AX
CMPQ    DX, BX
JCC 68
MOVQ    -8(AX)(CX*8), DX
CMPQ    CX, BX
JCC 68
MOVQ    (AX)(CX*8), BX
ADDQ    BX, DX
MOVQ    DX, -8(AX)(CX*8)
MOVQ    (SP), BP
ADDQ    $8, SP
RET

而实际上在硬件层面做同样的事情,一条就够了

ADDQ    BX, DX

为了弥补 VM 这一层抽象带来的开销,引入 JIT 导致过多的复杂度。

看个 C 和 lua 的例子。出发点是很好的:并不是所有地方都需要性能,开发效率,灵活性,可以兼顾。发现一个函数不够快的时候,总是能够通过改用 C 实现,来提升性能。这是典型的 VM 带来的好处。再看 JIT。通过实现 JIT 让 lua 更快时,一个矛盾就出现了:JIT 优化,和使用 C 写关键函数优化,二者不可兼得。就像是实现 JIT 的方式来优化 VM,投入产出比似乎变低了(编译器实现者的角度)?

有没有什么方式,不做 JIT,又能得到性能上的全部好处呢?那就是 eval!

编译一门语言,做代码分析,生成 SSA,消除死代码,做内联,分配寄存器等等等,费了那多的精力去优化,然而实现另一门语言,这些过程又得重新来一遍(虽然 LLVM 的出现会缓解这种问题)。换个角度思考,假设一门语言支持 eval,那么实现一门新的语言,只需要翻译成宿主语言,调用 eval 就行了。宿主语言所有的编译优化,都被得以利用。写解释器得到原生性能,想着都很美好。

支持 eval 好处很明显,它可以达得到宿主语言的性能;达到宿主语言的可移植性;可以调用宿主语言丰富的库,充分利用已有的生态环境。不过语言到语言翻译,没有 eval 不行。假设我写一个 lisp,翻译成 Go,会有一个问题:翻译成 Go 代码不是可运行的。因为 Go 语言并没有提供 eval。只有 eval 才能打破了什么是"可运行的"界线。源代码不算可运行,除非源代码加上编译器,并且这门语言可以动态生成和执行代码,才是可运行的。

eval 之上,可以有强大的宏。宏很容易用 eval 实现,而且更灵活。至于讨论宏这个 feature 是否邪恶,我觉得只要编译期不依赖运行期,就没问题。

整个 idea 最初的出发点是,能不能有某种办法,用写解释器的实现难度,得到编译器性能。

我真的不需要 VM,只是想要一种 eval 机制。如果真要说清楚 eval 机制到底是什么,那应该是:"Eval as universal machine"

eval