【问题标题】:Writing a JIT compiler in assembly在汇编中编写 JIT 编译器
【发布时间】:2011-06-29 11:45:31
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 仅仅因为抖动会生成机器码,并不意味着它本身需要用汇编语言编写。这样做毫无意义。
  • 尝试的中间步骤是使用 GCC 的计算 goto 扩展进行线程分派(使用 void *optable[] = { &&op_add, &&op_subtract, ... } 并且每个操作数是 op_add: ... goto *optable[*ip++];)。我已经看到您所描述的转换口译员取得了巨大的进步。

标签: c assembly code-generation jit self-modifying


【解决方案1】:

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

对于 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 在this paper (PDF) 中描述了一种方法。另一种方法参见 LuaJIT 源代码。两种 JIT 都是用 C(或者可能是 C++)编写的。

【讨论】:

  • 对,在每个基本块结束时,您必须退回到决定分支位置的例程。然后这会产生一个新的字节码地址,该地址必须映射到相应机器码的地址。有一种称为context threading (PDF) 的技术可以更顺畅地集成解释器和 JIT。主要思想是将分支翻译成实际的机器指令,以便分支预测器可以看到它们。
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 2010-10-06
    • 2012-06-05
    • 1970-01-01
    • 1970-01-01
    • 2011-07-17
    • 2011-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多