口译员如何加载他们的价值观?

时间:2013-11-13 20:41:50

标签: language-agnostic language-design vm-implementation

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

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

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

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

    像字符串这样的值需要比这更多的存储空间。大多数口译员如何以有效的方式解决这个问题?

2 个答案:

答案 0 :(得分:2)

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

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

因此,首先让我们考虑选择4位用于操作码。这里有很多将取决于我们想要支持多少数据格式,以及我们是否将4位包含在操作码中。仅仅为了论证,我们假设虚拟机支持的基本数据类型是8位和64位整数(也可以用于寻址),以及32位和64位浮点。

对于整数,我们至少需要支持:加,减,乘,除,和,或者,xor,not,negate,compare,test,left / right shift / rotate(逻辑和算术中的右移)品种),装载和储存。浮点将支持相同的算术运算,但删除逻辑/按位运算。我们还需要一些分支/跳转操作(无条件跳转,如果为零则跳转,如果不为零则跳转等)对于堆栈机器,我们可能还需要至少一些面向堆栈的指令(push,pop,dupe,可能旋转等。)

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

  1. 可选:小立即数(指令本身中的N位数据)
  2. 大型立即数(指令后面的64位字中的数据)
  3. 暗示(堆栈顶部的操作数)
  4. 绝对(指令后64位指定的地址)
  5. relative(指令中指定的偏移量)
  6. 我已尽最大努力保持一切尽可能小的合理性 - 您可能希望更多地提高效率。

    无论如何,在这样的模型中,对象的值只是内存中的某些位置。同样,字符串只是内存中的一些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。您还需要确定要支持的虚拟机类型。我上面描述的是面向堆栈的,但如果你愿意,你当然可以做一个面向寄存器的机器。

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

答案 1 :(得分:1)

我熟悉的字节码解释器使用常量表来做这件事。当编译器为一大块源生成字节码时,它还会生成一个与该字节码一起使用的小常量表。 (例如,如果字节码被填充到某种“函数”对象中,那么常量表也会进入。)

只要编译器遇到像字符串或数字这样的文字,它就会为解释器可以使用的值创建一个实际的运行时对象。它将它添加到常量表并获取添加值的索引。然后它发出类似LOAD_CONSTANT指令的东西,该指令有一个参数,其值是常量表中的索引。

Here's an example:

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);
}

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

Here's an example:

CASE_CODE(CONSTANT):
  PUSH(frame->fn->constants[READ_ARG()]);
  DISPATCH();

对于像truenull这样的小数字和常用值这样的内容,您可以专门为它们指示,但这只是一种优化。