用汇编语言编写JIT编译器

26

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

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

因此,我的目标是为这个C虚拟机编写一个JIT编译器,并且我想在x86汇编中完成。(我使用NASM作为汇编器)我不太确定如何做到这一点。我熟悉汇编语言,并查看了一些自修改代码示例,但我还没有弄清楚如何进行代码生成。

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

有人告诉过我一些库,可以使这个任务变得更容易,例如GNU闪电和LLVM。但是,我想先手写它,以了解它的工作原理,然后再使用外部资源。

这个社区能提供任何资源或示例来帮助我开始这项任务吗?展示两三条指令(例如“add”和“mov”)用于动态生成带有参数的可执行代码的简单示例,将会很有帮助。


11
仅仅因为一个抖动生成机器码并不意味着它本身需要用汇编语言编写。这样做没有任何意义。 - Hans Passant
尝试的一个中间步骤是使用GCC的计算跳转扩展进行线程分派(使用void *optable [] = {&&op_add,&&op_subtract,...}和每个操作数为op_add:... goto * optable [* ip ++];)。我已经看到了像你描述的切换解释器中的大幅增益。 - Ben Jackson
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的某些部分)。

  • 虽然有些JIT使用上述技术而不是解释器,但它们随后采用更复杂的优化机制针对频繁执行的代码。这涉及将执行的字节码转换为中间表示(IR),然后对其进行其他优化。

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

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


  • 1
    在每个基本块的末尾,您必须返回到一个决定分支位置的例程。这将导致一个新的字节码地址,必须映射到相应机器代码的地址。有一种称为上下文线程化(PDF)的技术,可以更平滑地集成解释器和JIT。主要思想是将分支转换为实际的机器指令,以便分支预测器可以看到它们。 - nominolo

    8

    我建议你看一下项目http://code.google.com/p/asmjit/。通过使用它提供的框架,你可以节省很多精力。如果你想要手动编写所有内容,只需阅读源代码并自己重写,我认为这并不难。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接