基于堆栈的虚拟机之外的解释器替代方案

3
构建另一种语言的解释器时,通常建议创建一个基于堆栈的虚拟机来解释实际解释器生成的字节码。因此,解释器将由两部分组成:翻译器将高级语言指令转换为虚拟机的字节码,而虚拟机本身则用于解释这些字节码。
我的问题是:是否存在解释语言的其他选择?例如,是否有可能(并且实际可行)跳过虚拟机,使用C函数实现所有指令?在某种程度上,我认为这应该是可能的,但对于更复杂的功能,您可能最终仍需要实现某种最小化的虚拟机。还有其他选择吗?

1
您可以直接发出线程代码,跳过“解释器”部分。 - SK-logic
@SK-logic:这并不是消除解释器,而只是使其变得微不足道。 - Ira Baxter
1
@IraBaxter,使用直接线程代码时,没有解释器,它是100%的机器码。 - SK-logic
1
@SK-logic:即使可以编码为1-2个机器指令,跳转到线程字中的指令仍然是解释器。我同意这很小,但它仍然存在,并且仍然会产生开销,例如未编译的代码。 - Ira Baxter
1
我不会把单个的“call”或“jump”指令称为解释器。如果我们必须实现基于蹦床的延续,那么是的,我们将有一个“迷你解释器”,但在直接线程代码的情况下,没有专用的循环,这就是编译和解释之间的边界。我不认为调用指令是开销,所有现代CPU都始终认为分支被执行,因此调用或跳转没有开销,只有在条件分支未被执行时才有开销。 - SK-logic
1
顺便说一下,我也遇到过很多情况,直接线程化的代码比任何经典的优化方法都更有效率,因为它具有更好的缓存本地性。 - SK-logic
2个回答

3
建议制作基于堆栈的虚拟机,因为它们更容易制作。
另一种常见的虚拟机类型是基于寄存器的,在其中值存储在寄存器中而不是在堆栈上。
还有许多其他变体的解释器和虚拟机。您可以有一个生成解析树的编译器,以及解释这些解析树的解释器(但如果使用递归函数实现,则可以认为它仍然是基于堆栈的虚拟机)。
此外,制作编译器的做法也很常见,这些编译器不是生成某种机器代码(用于VM或实际机器),而是生成另一种语言的代码。 C是这些编译器的常见目标语言,因为C语言及其编译器无处不在。但是,这时您就没有虚拟机或解释器了,只有编译器/翻译器。

1
你建议的做法有点可行。C语言并不真正允许你操作栈,当你调用一个函数时,它并不知道周围的局部变量,因此你需要在堆上分配一块内存来保留一些假的“栈空间”,用于你的脚本语言的局部变量,并将其传递给每个函数(或将其塞入线程全局变量中)。你还需要为该栈分配一个基指针以供你的脚本语言函数调用。
一旦你这样做了,你已经完成了大部分使得语言基于栈的工作。所以你可以继续完善。要使用实际的栈和基指针,你必须降到机器语言级别。
如果你的语言是基于寄存器的,它仍然需要一个栈来访问局部变量(只是使用频率较低),你只是不太会在指令参数中使用它。简单地说,三地址寄存器VM基本上是基于栈的VM的特殊情况。
对于字节码解释器的另一种方法是,让指令包含指令ID,然后将其用作函数指针数组的索引,每个函数指针都实现一个指令。
显然,这样做会影响性能。如果你的指令足够简单,你可以直接在机器代码中实现它们,避免(通常可以忽略不计的)函数调用开销,甚至可以使用真正的堆栈而不是虚拟堆栈来节省CPU周期。
这完全取决于你的需求。对于大多数情况,特别是如果这是你的第一个解析器/解释器/虚拟机,我建议使用一个函数指针数组和一个虚拟堆栈。它很简单,不太难调试,并且在现代机器上足够快。你总是可以稍后进入并编写一个优化版本,以不同的方式处理事情。
例如,一种方法是仅生成足够的机器代码以进行函数调用,然后在生成的机器代码中插入指向这样一个函数的指针。因此,每个脚本都成为一块已编译代码的块,但你不必编写完整的编译器。从那里开始,你可以通过为它们生成汇编程序来改进单个关键指令,通过将不经常使用的内容作为函数留下。这可以稍微提高代码的局部性,这是一种微小的微观优化,可以帮助你。但只是其中之一。

大约一个月前,我在博客上发表了一篇关于如何从初学者的角度制作编译器(和字节码解释器)的文章,这可能会有所帮助:http://orangejuiceliberationfront.com/how-to-write-a-compiler/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接