Mec*_*cki 21 performance interpreter opcode vm-implementation
不要被震惊.这是很多文字,但我害怕没有提供一些详细的信息,我无法真正展示这是什么(并可能得到很多答案,并没有真正解决我的问题).而这绝对不是一项任务(正如他的评论中有人可笑地声称的那样).
由于除非至少设置了一些先决条件,否则根本无法回答此问题,以下是先决条件:
此外,我们需要更好地定义"更好".必须考虑几个属性:
现在通过或多或少的操作码实际意味着我的例子.它可能看起来实际上设置了操作码的数量,因为每个操作需要一个操作码.然而,它并不那么容易.
你可以进行类似的操作
ADD R1, R2, R3
Run Code Online (Sandbox Code Playgroud)
添加R1和R2的值,将结果写入R3.现在考虑以下特殊情况:
ADD R1, R2, R2
ADD R1, 1, R1
Run Code Online (Sandbox Code Playgroud)
这些是您在许多应用程序中可以找到的常见操作.您可以使用现有的操作码来表达它们(除非您需要不同的操作码,因为最后一个操作码具有int值而不是寄存器).但是,您也可以为这些创建特殊的操作码:
ADD2 R1, R2
INC R1
Run Code Online (Sandbox Code Playgroud)
和之前一样.优势在哪里?ADD2只需要两个参数,而不是3个,INC甚至只需要一个参数.因此,这可以在磁盘和/或内存中更紧凑地编码.由于将任何一种形式转换为另一种形式也很容易,因此解码步骤可以在两种方式之间转换以表达这些语句.不过,我不确定这两种形式会影响执行速度.
现在让我们假设您有一个ADD_RRR(R表示寄存器)和一个LOAD来将数据加载到寄存器中.
LOAD value, R2
ADD_RRR R1, R2, R3
Run Code Online (Sandbox Code Playgroud)
您可以拥有这两个操作码并始终在整个代码中使用这样的构造...或者您可以将它们组合成一个名为ADD_RMR(M代表内存)的新操作码
ADD_RMR R1, value, R3
Run Code Online (Sandbox Code Playgroud)
假设您有16位整数和32位整数作为本机类型.寄存器为32位,因此数据类型适合.现在,当您添加两个寄存器时,可以将数据类型设为参数:
ADD int16, R1, R2, R3
ADD int32, R1, R2, R3
Run Code Online (Sandbox Code Playgroud)
例如,对于有符号和无符号整数也是如此.这样,ADD可以是一个短操作码,一个字节,然后你有另一个字节(或者可能只是4位)告诉VM如何解释寄存器(它们是否保持16位或32位值).或者您可以废弃类型编码,而是有两个操作码:
ADD16 R1, R2, R3
ADD32 R1, R2, R3
Run Code Online (Sandbox Code Playgroud)
有些人可能会说两者完全相同 - 只是解释第一种方式,因为16位操作码可行.是的,但是一个非常幼稚的翻译可能看起来很不一样.例如,如果每个操作码有一个函数并使用switch语句调度(不是最好的方式,函数调用开销,switch语句也许不是最优的,我知道),这两个操作码可能如下所示:
case ADD16: add16(p1, p2, p3); break; // pX pointer to register
case ADD32: add32(p1, p2, p3); break;
Run Code Online (Sandbox Code Playgroud)
并且每个函数都以某种添加为中心.第二个可能看起来像这样:
case ADD: add(type, p1, p2, p3); break;
// ...
// and the function
void add (enum Type type, Register p1, Register p2, Register p3)
{
switch (type) {
case INT16: //...
case INT32: // ...
}
}
Run Code Online (Sandbox Code Playgroud)
将子交换机添加到主交换机或子分派表到主调度表.当然,一个解释可以做两种方式不管类型是明确的或没有,但无论哪种方式会感觉更加天然取决于操作码的设计开发.
由于缺乏一个更好的名字,我会这样称呼他们.这些操作码本身没有任何意义,只是改变了后续操作码的含义.像着名的WIDE运营商一样:
ADD R1, R2, R3
WIDE
ADD R1, R2, R3
Run Code Online (Sandbox Code Playgroud)
例如,在第二种情况下,寄存器是16位(所以你可以添加更多它们),在第一种情况下只有8位.或者你不能拥有这样的元操作码,并且有一个ADD和一个ADD_WIDE操作码.像WIDE这样的元操作码避免使用SUB_WIDE,MUL_WIDE等,因为你总是可以在WIDE之前添加所有其他普通操作码(总是只有一个操作码).缺点是单独的操作码变得毫无意义,如果它是元操作码,你总是必须检查操作码.此外,VM必须为每个线程存储额外的状态(例如,我们现在是否处于宽模式)并在下一条指令之后再次移除状态.甚至CPU都有这样的操作码(例如x86 LOCK操作码).
当然,你拥有的操作码越多,开关/调度表就越大,在磁盘或内存中表达这些代码所需的位数就越大(尽管你可以将它们更有效地存储在数据不存在的磁盘上)必须由VM直接执行); VM也会变得更加复杂并且拥有更多的代码行 - 另一方面,操作码的功能越强大:您越来越接近每个表达式,即使是复杂的表达式,最终会出现在一个操作码中.
选择小操作码可以很容易地对VM进行编码,并且会导致非常紧凑的操作码 - 另一方面,这意味着您可能需要非常多的操作码才能执行简单的任务,并且每个非常常使用的表达式都必须成为某种(本机)函数调用,因为没有操作码可用于它.
我在互联网上阅读了很多关于各种虚拟机的内容,但没有任何消息来源确实能够做出良好公平的权衡.设计VM就像设计一个CPU,有很少的操作码的CPU,它们很快,但你也需要很多.并且有些CPU有许多操作码,有些非常慢,但是你需要更少的代码才能表达相同的代码.看起来"更多操作码更好"CPU已经完全赢得了消费者市场,"更少操作更好"的服务器只能在服务器市场或超级计算机业务的某些部分生存.虚拟机怎么样?
说实话,我认为这主要取决于VM的用途,类似于处理器设计在很大程度上取决于处理器的主要用途.
换句话说,您最好能够确定VM的常见用例场景,以便您可以建立可能需要的功能,并建立那些不太常见的功能.
当然我明白,您可能正在设想一个抽象的,非常通用的虚拟机,可以用作其他编程语言的内部/后端实现?
但是,我觉得,重要的是要意识到并强调没有任何事物的"通用理想"实现,即一旦你保持通用和抽象,你将不可避免地面临需要妥协的情况.
理想情况下,这些妥协将基于您的代码的实际使用场景,因此这些妥协实际上基于您可以做出的明智的假设和简化,而不会出现问题.
换句话说,我会考虑你的VM的目标是什么?它是如何主要用于您的愿景的?你想实现的目标是什么?
这将帮助您提出要求并帮助您进行简化,以便您可以根据合理的假设设计指令集.
如果您希望您的VM主要由编程语言用于数字运算,那么您可能希望通过提供大量低级基元来支持宽数据类型来寻找数学运算的相当强大的基础.
另一方面,如果您将服务器作为OO语言的后端,您将需要考虑优化相应的低级指令(即散列/字典).
一般来说,我建议在开始时尽可能简单直观地保持指令集,并且只有在证明具有它们的确实有用(即配置文件和操作码转储)并且确实导致性能之后才添加特殊指令获得.因此,这将主要取决于您的VM将拥有的第一批"客户".
如果您真的非常渴望研究更多涉及的方法,您甚至可以考虑在运行时动态优化指令集,使用模式匹配来查找字节码中常见的操作码,以便派生更抽象的实现,以便您可以转换您的字节码动态地使用自定义的,运行时生成的操作码.
为了提高软件性能,如果所有操作码的长度都相同,则比较容易,因此您可以使用一个巨大的switch语句,而不必检查前面的修饰符操作码可能设置的各种选项位。
我想您没有问过的两个问题是:易于编写将编程语言转换为VM代码的编译器,以及易于编写执行VM代码的解释器。只需较少的操作码,这两种方法都更容易实现。(但不要太少。例如,如果省略了除法操作码,那么您就有机会学习如何编写良好的除法函数。良好的函数比简单的函数难得多。)