1

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

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

例如,让我们(不切实际地)假设:

  1. 我们的指令是无符号的 32 位整数。
  2. 我们为操作码保留了整数的前 4 位。

如果我想将数据存储在与操作码相同的整数中,则仅限于 24 位整数。如果我想将它存储在下一条指令中,我将被限制为 32 位值。

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

4

2 回答 2

2

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

对于典型的字节码解释器,您基本上设计了一些理想化的机器。为此目的,基于堆栈(或至少面向堆栈)的设计非常普遍,所以让我们假设一下。

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

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

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

  1. 可选:小立即数(指令本身包含 N 位数据)
  2. 大立即数(指令后 64 位字中的数据)
  3. 隐含的(堆栈顶部的操作数)
  4. 绝对(在指令后的 64 位中指定的地址)
  5. 相对的(在或遵循指令中指定的偏移量)

我已尽我最大的努力将这里的所有内容保持在合理范围内——您可能需要更多来提高效率。

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

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

于 2013-11-14T06:15:01.603 回答
1

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

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

这是一个例子:

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指令,您只需解码参数,并将对象从常量表中拉出。

这是一个例子:

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

对于小数字和经常使用的值(如trueand null),您可以对它们进行专门的说明,但这只是一种优化。

于 2013-12-03T21:55:53.123 回答