26

我用 C 语言编写了一个虚拟机,它对于非 JIT 虚拟机具有不错的性能,但我想学习一些新东西并提高性能。我当前的实现只是使用一个开关将 VM 字节码转换为指令,然后编译为跳转表。就像我说的那样,性能不错,但我遇到了只能通过 JIT 编译器克服的障碍。

不久前我已经问过一个关于自修改代码的类似问题,但我意识到我没有问对正确的问题。

所以我的目标是为这个 C 虚拟机写一个 JIT 编译器,我想用 x86 汇编来做。(我使用 NASM 作为我的汇编程序)我不太确定如何去做。我对汇编很满意,并且查看了一些自修改代码示例,但我还没有弄清楚如何进行代码生成。

到目前为止,我的主要工作是将指令复制到可执行内存中,并带有我的参数。我知道我可以在 NASM 中标记某行,并使用静态参数从该地址复制整行,但这不是很动态,并且不适用于 JIT 编译器。我需要能够解释来自字节码的指令,将其复制到可执行内存,解释第一个参数,将其复制到内存,然后解释第二个参数,并将其复制到内存。

我被告知有几个库可以使这项任务变得更容易,例如 GNU Lightning,甚至 LLVM。但是,在使用外部资源之前,我想先手动编写它,以了解它是如何工作的。

该社区是否可以提供任何资源或示例来帮助我开始执行此任务?一个简单的例子展示了两个或三个指令,如“add”和“mov”,用于在内存中动态生成可执行代码,并带有参数,会产生奇迹。

4

2 回答 2

19

我根本不建议在汇编中编写 JIT。在汇编中编写解释器最常执行的位有很好的论据。有关其外观的示例,请参见LuaJIT 的作者Mike Pall的评论。

至于 JIT,有许多不同的级别,具有不同的复杂性:

  1. 通过简单地复制解释器的代码来编译一个基本块(一系列非分支指令)。例如,一些(基于寄存器的)字节码指令的实现可能如下所示:

    ; ebp points to virtual register 0 on the stack
    instr_ADD:
        <decode instruction>
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        <dispatch next instruction>
    instr_SUB:
        ... ; similar
    

    因此,给定指令序列ADD R3, R1, R2SUB R3, R3, R4一个简单的 JIT 可以将解释器实现的相关部分复制到一个新的机器代码块中:

        mov ecx, 1
        mov edx, 2
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        mov ecx, 3
        mov edx, 4
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        sub eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
    

    这只是简单地复制了相关代码,因此我们需要相应地初始化使用的寄存器。更好的解决方案是将其直接翻译成机器指令mov eax, [ebp + 4],但现在您必须手动对请求的指令进行编码。

    这种技术消除了解释的开销,但在其他方面并没有提高效率。如果代码只执行一两次,那么首先将其转换为机器代码可能不值得(这需要刷新至少部分 I-cache)。

  2. 虽然一些 JIT 使用上述技术而不是解释器,但它们随后对频繁执行的代码采用了更复杂的优化机制。这涉及将执行的字节码转换为中间表示(IR),在该中间表示上执行额外的优化。

    根据源语言和 JIT 的类型,这可能非常复杂(这就是许多 JIT 将此任务委托给 LLVM 的原因)。基于方法的 JIT 需要处理连接控制流图,因此它们使用 SSA 形式并对其进行各种分析(例如,Hotspot)。

    跟踪 JIT(如 LuaJIT 2)只编译直线代码,这使得许多事情更容易实现,但您必须非常小心如何选择跟踪以及如何有效地将多个跟踪链接在一起。Gal 和 Franz 在本文 (PDF)中描述了一种方法。另一种方法参见 LuaJIT 源代码。两种 JIT 都是用 C(或者可能是 C++)编写的。

于 2011-02-18T00:11:21.093 回答
8

我建议您查看项目http://code.google.com/p/asmjit/。通过使用它提供的框架,您可以节省大量能源。如果你想手写所有的东西,只需阅读源代码并自己重写,我认为这不是很难。

于 2011-07-28T01:30:25.490 回答