Performance VM设计:更多操作码还是更少操作码?什么更好?

Performance VM设计:更多操作码还是更少操作码?什么更好?,performance,interpreter,opcode,vm-implementation,Performance,Interpreter,Opcode,Vm Implementation,不要感到震惊。这是一篇很长的文章,但我担心如果不提供一些详细的信息,我就无法真正说明这是怎么回事(并且可能会得到很多没有真正解决我的问题的答案)。这绝对不是一项任务(正如有人在评论中荒谬地宣称的那样) 先决条件 由于这个问题可能根本无法回答,除非至少设置了一些先决条件,以下是先决条件: 应解释虚拟机代码。不禁止存在JIT编译器,但设计应以解释器为目标 虚拟机应基于寄存器,而不是基于堆栈 答案可能既不假设存在一组固定的寄存器,也不假设存在数量不限的寄存器,任何一种情况都可能是这样 此外,我们需

不要感到震惊。这是一篇很长的文章,但我担心如果不提供一些详细的信息,我就无法真正说明这是怎么回事(并且可能会得到很多没有真正解决我的问题的答案)。这绝对不是一项任务(正如有人在评论中荒谬地宣称的那样)

先决条件 由于这个问题可能根本无法回答,除非至少设置了一些先决条件,以下是先决条件:

  • 应解释虚拟机代码。不禁止存在JIT编译器,但设计应以解释器为目标
  • 虚拟机应基于寄存器,而不是基于堆栈
  • 答案可能既不假设存在一组固定的寄存器,也不假设存在数量不限的寄存器,任何一种情况都可能是这样
此外,我们需要更好地定义“更好”。必须考虑以下几个特性:

  • 磁盘上VM代码的存储空间。当然,您可以在这里放弃所有优化,只压缩代码,但这对(2)有负面影响
  • 解码速度。如果将代码转换为可以直接执行的代码需要很长时间,那么存储代码的最佳方法是无用的
  • 内存中的存储空间。此代码必须可直接执行,无论是否进一步解码,但如果涉及进一步解码,则在执行期间和每次执行指令时都会执行此编码(将代码计数加载到项目2时仅执行一次解码)
  • 代码的执行速度(考虑到常见的解释器技术)
  • 虚拟机的复杂性以及为其编写解释器的难度
  • VM自身需要的资源量。(如果虚拟机运行的代码大小为2 KB,执行速度比眨眼的速度快,那么这不是一个好的设计,但是它需要150 MB才能执行,并且它的启动时间远远高于它执行的代码的运行时间)
  • 现在举例说明我所说的或多或少的操作码。看起来操作码的数量实际上已经设置好了,因为每个操作都需要一个操作码。然而,这并不是那么容易

    同一操作的多个操作码 你可以做这样的手术

    ADD R1, R2, R3
    
    将R1和R2的值相加,将结果写入R3。现在考虑以下特殊情况:

    ADD R1, R2, R2
    ADD R1, 1, R1
    
    这些是许多应用程序中常见的操作。您可以使用已经存在的操作码来表示它们(除非您需要一个不同的操作码,因为最后一个操作码具有int值而不是寄存器)。但是,您也可以为以下各项创建特殊操作码:

    ADD2 R1, R2
    INC R1
    
    和以前一样。优势在哪里?ADD2只需要两个参数,而不是3,INC甚至只需要一个参数。因此,可以在磁盘和/或内存中对其进行更紧凑的编码。由于将其中一种形式转换为另一种形式也很容易,因此解码步骤可以在两种方式之间转换以表达这些语句。不过,我不确定这两种形式对执行速度的影响有多大

    将两个操作码组合成一个操作码 现在让我们假设您有一个ADD_RRR(R表示寄存器)和一个LOAD来将数据加载到寄存器中

    LOAD value, R2
    ADD_RRR R1, R2, R3
    
    您可以拥有这两个操作码,并在整个代码中始终使用这样的结构。。。或者您可以将它们组合成一个新的操作码,名为ADD_RMR(M表示内存)

    数据类型与操作码 假设您有16位整数和32位整数作为本机类型。寄存器是32位的,所以两种数据类型都适合。现在,当添加两个寄存器时,可以将数据类型设置为参数:

    ADD int16, R1, R2, R3
    ADD int32, R1, R2, R3
    
    例如,有符号整数和无符号整数也是如此。通过这种方式,ADD可以是一个短操作码,一个字节,然后有另一个字节(或者可能只有4位)告诉VM如何解释寄存器(它们是否包含16位或32位值)。或者,您可以放弃类型编码,改为使用两个操作码:

    ADD16 R1, R2, R3
    ADD32 R1, R2, R3
    
    有人可能会说两者完全相同——只需将第一种方式解释为16位操作码即可。是的,但是一个非常幼稚的口译员可能看起来很不一样。例如,如果每个操作码有一个函数,并且使用switch语句进行调度(我知道这不是最好的方式,函数调用开销,switch语句也可能不是最优的),那么这两个操作码可能如下所示:

    case ADD16: add16(p1, p2, p3); break; // pX pointer to register
    case ADD32: add32(p1, p2, p3); break;
    
    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: // ...
        }
    }
    
    每个函数都围绕着一种特定的加法。第二个可能看起来像这样:

    case ADD16: add16(p1, p2, p3); break; // pX pointer to register
    case ADD32: add32(p1, p2, p3); break;
    
    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: // ...
        }
    }
    
    将子交换机添加到主交换机或将子调度表添加到主调度表。当然,无论类型是否显式,解释器都可以采用这两种方式,但根据操作码设计,开发人员会觉得这两种方式都更为自然

    元操作码 因为没有更好的名字,我就这样称呼他们。这些操作码本身没有任何意义,它们只是在下面更改操作码的意义。就像著名的广域网运营商:

    ADD R1, R2, R3
    WIDE
    ADD R1, R2, R3
    
    例如,在第二种情况下,寄存器为16位(因此您可以添加更多的寄存器),在第一种情况下仅为8位。或者,您不能有这样一个元操作码,也不能有一个ADD和一个ADD_-WIDE操作码。像WIDE这样的元操作码避免使用SUB_-WIDE、MUL_-WIDE等,因为您总是可以使用WIDE(始终只有一个操作码)预先编写其他所有普通操作码。缺点是一个操作码本身就变得毫无意义,如果它是元操作码或不是元操作码,您必须在它之前检查操作码。此外,VM必须为每个线程存储一个额外的状态(例如,无论我们现在是否处于广域模式),并在下一条指令后再次删除该状态。甚至CPU也有这样的操作码(例如x86锁操作码)

    如何找到一个好的交易??? 当然,您拥有的操作码越多,交换机/调度表就越大,在磁盘或内存中表达这些代码所需的位也就越多(尽管您可以更有效地将它们存储在磁盘上,因为磁盘上的数据不必由VM直接执行);此外,虚拟机将