6

我用 C 语言编写了一个虚拟机作为一个爱好项目。此虚拟机执行与 Intel 语法 x86 程序集非常相似的代码。问题是这个虚拟机使用的寄存器只是名义上的寄存器。在我的 VM 代码中,寄存器的使用与 x86 寄存器一样,但机器将它们存储在系统内存中。在 VM 代码中使用寄存器而不是系统内存没有性能改进。(我认为仅局部性会在一定程度上提高性能,但实际上并没有改变。)

在解释程序时,此虚拟机将指令的参数存储为指针。这允许虚拟指令将内存地址、常量值、虚拟寄存器或几乎任何东西作为参数。

由于硬件寄存器没有地址,我想不出一种方法将我的虚拟机寄存器实际存储在硬件寄存器中。在我的虚拟寄存器类型上使用 register 关键字不起作用,因为我必须获取指向虚拟寄存器的指针才能将其用作参数。有没有办法让这些虚拟寄存器的性能更像它们的本地对应物?

如有必要,我非常乐意钻研组装。我知道 JIT 编译此 VM 代码可以让我利用硬件寄存器,但我也希望能够将它们与我的解释代码一起使用。

4

6 回答 6

9
  1. 机器寄存器没有索引支持:你不能用运行时指定的“索引”访问寄存器,不管这意味着什么,没有代码生成。由于您可能会从指令中解码寄存器索引,因此唯一的方法是进行巨大的切换(即switch (opcode) { case ADD_R0_R1: r[0] += r[1]; break; ... })。这可能是个坏主意,因为它过多地增加了解释器循环的大小,因此会引入指令缓存抖动。

  2. 如果我们谈论的是 x86,另一个问题是通用寄存器的数量非常少;其中一些将用于簿记(存储 PC、存储您的 VM 堆栈状态、解码指令等) - 您不太可能拥有多个用于 VM 的空闲寄存器。

  3. 即使可以使用寄存器索引支持,它也不可能给您带来很多性能。通常在解释器中最大的瓶颈是指令解码;x86 支持基于寄存器值(即mov eax, dword ptr [ebx * 4 + ecx])的快速和紧凑的内存寻址,因此您不会赢得太多。检查生成的程序集是值得的——即确保“寄存器池”地址存储在寄存器中。

  4. 加速解释器的最好方法是 JITting;即使是简单的 JIT(即没有智能寄存器分配 - 基本上只是发出与指令循环和 switch 语句执行的代码相同的代码,指令解码除外)可以将性能提高 3 倍或更多(这些是简单 JITter 的实际结果在类似 Lua 的基于寄存器的 VM 之上)。最好将解释器保留为参考代码(或用于冷代码以降低 JIT 内存成本 - JIT 生成成本对于简单的 JIT 来说不是问题)。

于 2011-01-25T07:39:41.080 回答
3

即使您可以直接访问硬件寄存器,将代码包装在使用寄存器而不是内存的决定上也会慢得多。

要获得性能,您需要预先设计性能。

几个例子。

通过设置所有陷阱来准备 x86 VM,以捕获离开其虚拟内存空间的代码。直接执行代码,不要模拟,分支到它并运行。当代码超出其内存/i/o 空间以与设备等进行通信时,捕获并模拟该设备或它所到达的任何东西,然后将控制权返回给程序。如果代码受处理器限制,它将运行得非常快,如果受 I/O 限制,则速度会很慢,但不如模拟每条指令那么慢。

静态二进制翻译。运行前反汇编和翻译代码,例如指令 0x34,0x2E 将在 .c 文件中转换为 ascii:

人^= 0x2E;=0; cf=0;sf=al

理想情况下执行大量的死代码删除(如果下一条指令也修改了标志,那么不要在此处修改它们,等等)。并让编译器中的优化器完成其余的工作。您可以通过这种方式在模拟器上获得性能增益,性能增益的好坏取决于您可以优化代码的程度。作为一个新程序,它在硬件上运行,注册内存等等,因此处理器绑定的代码比 VM 慢,在某些情况下,您不必处理处理器执行异常以捕获内存/io,因为您已经模拟了内存在代码中访问,但这仍然需要成本,并且无论如何都会调用模拟设备,因此那里没有节省。

动态翻译,类似于 sbt 但您在运行时执行此操作,我听说过此操作,例如在其他处理器上模拟 x86 代码时,例如 dec alpha,代码从 x86 指令慢慢更改为本机 alpha 指令,所以下一次它直接执行 alpha 指令,而不是模拟 x86 指令。每次通过代码程序执行得更快。

或者,也许只是重新设计你的模拟器,从执行的角度来看更高效。以 MAME 中的模拟处理器为例,代码的可读性和可维护性已经牺牲了性能。写这点很重要,今天使用多核千兆赫处理器,您不必费力地模拟 1.5ghz 6502 或 3ghz z80。像在表格中查找下一个操作码并决定不模拟指令的部分或全部标志计算这样简单的事情可以给您带来明显的提升。

底线,如果您有兴趣在运行程序时使用 x86 硬件寄存器、Ax、BX 等来模拟 AX、BX 等寄存器,那么唯一有效的方法是实际执行指令,而不是执行和陷阱就像在单步调试器中一样,但在执行长串指令的同时防止它们离开 VM 空间。有不同的方法可以做到这一点,性能结果会有所不同,这并不意味着它会比性能高效的模拟器更快。这限制了您将处理器与程序匹配。使用高效的代码和非常好的编译器(好的优化器)模拟寄存器将为您提供合理的性能和可移植性,因为您不必将硬件与正在运行的程序相匹配。

于 2011-01-25T15:00:46.310 回答
1

在执行之前(提前)转换复杂的、基于寄存器的代码。一个简单的解决方案是类似双栈 vm 的执行,它提供了在寄存器中缓存栈顶元素 (TOS) 的可能性。如果您更喜欢基于寄存器的解决方案,请选择捆绑尽可能多的指令的“操作码”格式(拇指规则,如果选择 MISC 样式设计,最多可以将四个指令捆绑到一个字节中)。这样,虚拟寄存器访问可以在本地解析为每个静态超指令的物理寄存器引用(clang 和 gcc 能够执行这种优化)。作为副作用,无论特定的寄存器分配如何,降低的 BTB 错误预测率都会导致更好的性能。

基于 C 的解释器的最佳线程技术是直接线程(标签作为地址扩展)和复制切换线程(符合 ANSI)。

于 2011-12-29T17:54:37.910 回答
0

因此,您正在编写一个 x86 解释器,它肯定比实际硬件慢 1 到 3 次方 10。在真正的硬件中,sayingmov mem, foo将花费比 更多的时间mov reg, foo,而在您的程序中将mem[adr] = foo花费大约一样长的时间myRegVars[regnum] = foo(模缓存)。所以你期待相同的速度差异?

如果你想模拟寄存器和内存之间的速度差异,你将不得不做类似于 Cachegrind 所做的事情。也就是说,保留一个模拟时钟,当它进行内存引用时,它会增加一个大数字。

于 2011-01-25T13:46:27.013 回答
0

您的 VM 似乎太复杂而无法进行有效的解释。一个明显的优化是拥有一个“微代码”VM,带有寄存器加载/存储指令,甚至可能是基于堆栈的。您可以在执行之前将您的高级虚拟机转换为更简单的虚拟机。另一个有用的优化取决于 gcc 可计算标签扩展,请参阅 Objective Caml VM 解释器以获取此类线程 VM 实现的示例。

于 2011-01-25T15:04:26.017 回答
0

要回答您提出的具体问题:

您可以指示您的 C 编译器留下一堆免费的寄存器供您使用。通常不允许指向内存第一页的指针,它们保留用于 NULL 指针检查,因此您可能会滥用初始指针来标记寄存器。如果您有一些本机寄存器可用,这会有所帮助,因此我的示例使用 64 位模式来模拟 4 个寄存器。开关的额外开销很可能会减慢执行速度而不是使其更快。另请参阅其他答案以获取一般建议。

/* compile with gcc */

register long r0 asm("r12");
register long r1 asm("r13");
register long r2 asm("r14");
register long r3 asm("r15");

inline long get_argument(long* arg)
{
    unsigned long val = (unsigned long)arg;
    switch(val)
    {
        /* leave 0 for NULL pointer */
        case 1: return r0;
        case 2: return r1;
        case 3: return r2;
        case 4: return r3;
        default: return *arg;
    }
}
于 2011-01-25T15:14:01.827 回答