Language agnostic 口译员如何加载他们的值?

Language agnostic 口译员如何加载他们的值?,language-agnostic,language-design,vm-implementation,Language Agnostic,Language Design,Vm Implementation,我的意思是,解释器处理指令列表,这些指令似乎或多或少由字节序列组成,通常存储为整数。通过执行逐位操作从这些整数中检索操作码,以便在所有操作所在的大开关语句中使用 我的具体问题是:如何存储/检索对象值? 例如,让我们(非现实地)假设: 我们的指令是无符号32位整数 我们已经为操作码保留了整数的前4位 如果我想将数据存储在与操作码相同的整数中,则只能使用24位整数。如果我想将其存储在下一条指令中,则限制为32位的值 像字符串这样的值需要更多的存储空间。大多数解释器是如何高效地解决这个问题的?我首先假

我的意思是,解释器处理指令列表,这些指令似乎或多或少由字节序列组成,通常存储为整数。通过执行逐位操作从这些整数中检索操作码,以便在所有操作所在的大开关语句中使用

我的具体问题是:如何存储/检索对象值?

例如,让我们(非现实地)假设:

  • 我们的指令是无符号32位整数
  • 我们已经为操作码保留了整数的前4位
  • 如果我想将数据存储在与操作码相同的整数中,则只能使用24位整数。如果我想将其存储在下一条指令中,则限制为32位的值


    像字符串这样的值需要更多的存储空间。大多数解释器是如何高效地解决这个问题的?

    我首先假设您主要(如果不是唯一)对字节码解释器或类似的东西感兴趣(因为您的问题似乎是这样假设的)。直接从源代码(以原始或标记形式)工作的解释器是相当不同的

    对于一个典型的字节码解释器,您基本上设计了一些理想化的机器。基于堆栈(或至少面向堆栈)的设计在这方面非常常见,所以让我们假设

    所以,首先让我们考虑OP码的4位选择。这里的很多内容将取决于我们想要支持多少数据格式,以及我们是否将其包含在运算代码的4位中。为了便于讨论,我们假设虚拟机本身支持的基本数据类型是8位和64位整数(也可用于寻址)以及32位和64位浮点

    对于整数,我们至少需要支持:加、减、乘、除和、或、异或、非、求反、比较、测试、左/右移位/旋转(逻辑和算术中的右移)、加载和存储。浮点将支持相同的算术运算,但删除逻辑/位运算。我们还需要一些分支/跳转操作(无条件跳转、零跳转、非零跳转等)。对于堆栈机器,我们可能还需要至少几个面向堆栈的指令(push、pop、dupe、可能的rotate等)

    这为数据类型提供了一个两位字段,为操作码字段提供了至少5位(很可能是6位)。与条件跳转作为特殊指令不同,我们可能只需要一条跳转指令和一些位来指定可应用于任何指令的条件执行。我们还需要指定至少几种寻址模式:

  • 可选:小立即数(指令本身中的N位数据)
  • 大立即数(指令后64位字中的数据)
  • 隐含(堆栈顶部的操作数)
  • 绝对(指令后64位指定的地址)
  • 相对(指令中或指令后指定的偏移量)
  • 我已经尽了最大努力,尽可能地将所有内容保持在合理的最低限度——您可能需要更多内容来提高效率

    无论如何,在这样的模型中,对象的值只是内存中的一些位置。同样,字符串只是内存中8位整数的序列。几乎所有对象/字符串的操作都是通过堆栈完成的。例如,假设您定义了一些类A和类B,如下所示:

    class A { 
        int x;
        int y;
    };
    
    class B { 
        int a;
        int b;
    };
    
    …还有一些代码,如:

    A a {1, 2};
    B b {3, 4};
    
    a.x += b.a;
    
    初始化意味着可执行文件中的值加载到分配给a和b的内存位置。然后,添加可以生成如下代码:

    push immediate a.x   // put &a.x on top of stack
    dupe                 // copy address to next lower stack position
    load                 // load value from a.x
    push immediate b.a   // put &b.a on top of stack
    load                 // load value from b.a
    add                  // add two values
    store                // store back to a.x using address placed on stack with `dupe`
    
    假设每个指令有一个字节,那么整个序列大约有23个字节,其中16个字节是地址。如果我们使用32位寻址而不是64位,我们可以减少8个字节(即总共15个字节)

    要记住的最明显的一点是,由典型字节码解释器(或类似解释器)实现的虚拟机与在硬件中实现的“真实”机器没有多大区别。您可能会添加一些对您试图实现的模型很重要的指令(例如,JVM包含直接支持其安全模型的指令),或者如果您只想支持不包含它们的语言,您可能会省略一些指令(例如,如果您真的想做的话,我想您可以省略一些指令,如
    xor
    ). 您还需要决定要支持哪种类型的虚拟机。我上面描述的是面向堆栈的,但是如果您愿意,您当然可以使用面向寄存器的机器


    无论哪种方式,大多数对象访问、字符串存储等都归结为内存中的位置。机器将从这些位置将数据检索到堆栈/寄存器中,根据需要进行操作,并存储回目标对象的位置。

    我熟悉的字节码解释器使用常量表执行此操作。当编译器为一个源代码块生成字节码时,它也会生成一个小的常量表,与该字节码一起运行。(例如,如果字节码被塞进某种“函数”对象中,常量表也会被放入其中。)

    每当编译器遇到字符串或数字之类的文本时,它都会为解释器可以使用的值创建一个实际的运行时对象。它将该值添加到常量表中,并获取添加该值的索引。然后它发出类似于
    LOAD\u CONSTANT
    的指令,该指令有一个参数,其值是常量表中的索引

    在运行时,要实现
    LOAD_CONSTANT
    指令,只需解码参数,并将对象从常量表中拉出

    对于小数字和经常使用的值,如
    true
    
    static void string(Compiler* compiler, int allowAssignment)
    {
      // Define a constant for the literal.
      int constant = addConstant(compiler, wrenNewString(compiler->parser->vm,
          compiler->parser->currentString, compiler->parser->currentStringLength));
    
      // Compile the code to load the constant.
      emit(compiler, CODE_CONSTANT);
      emit(compiler, constant);
    }
    
    CASE_CODE(CONSTANT):
      PUSH(frame->fn->constants[READ_ARG()]);
      DISPATCH();