我不建议在汇编语言中编写JIT。存在编写解释器的最频繁执行部分用汇编语言的好处。有关此内容的示例请参见LuaJIT的作者Mike Pall在该评论中的说明。
至于JIT,有许多不同的级别和复杂程度:
通过简单复制解释器的代码来编译基本块(一系列非分支指令)。例如,几个(寄存器为基础)字节码指令实现可能如下所示:
; 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,R2
,SUB R3,R3,R4
,一个简单的JIT可以将解释器实现的相关部分复制到新的机器代码块中:
mov ecx, 1
mov edx, 2
mov ebx, 3
mov eax, [ebp + ecx * 4]
add eax, [ebp + edx * 4]
mov [ebp + ebx * 4], eax
mov ecx, 3
mov edx, 4
mov ebx, 3
mov eax, [ebp + ecx * 4]
sub eax, [ebp + edx * 4]
mov [ebp + ebx * 4], eax
这只是复制相关代码,因此我们需要相应地初始化所使用的寄存器。一种更好的解决方案是将其直接翻译成机器指令mov eax,[ebp + 4]
,但现在您已经必须手动编码请求的指令。
这种技术消除了解释的开销,但从效率上讲并没有太大改进。如果代码仅执行一两次,则可能不值得将其首先转换为机器代码(这需要至少刷新I-cache的某些部分)。
虽然有些JIT使用上述技术而不是解释器,但它们随后采用更复杂的优化机制针对频繁执行的代码。这涉及将执行的字节码转换为中间表示(IR),然后对其进行其他优化。
根据源语言和JIT的类型,这可能非常复杂(这就是为什么许多JIT将此任务委托给LLVM的原因)。基于方法的JIT需要处理加入控制流图,因此它们使用SSA形式并在其上运行各种分析(例如Hotspot)。
跟踪JIT(例如LuaJIT 2)仅编译直线代码,这使得许多实现变得更容易,但是您必须非常小心地选择如何拾取跟踪以及如何有效地链接多个跟踪。 Gal和Franz在此论文(PDF)中描述了一种方法。对于另一种方法,请参见LuaJIT源代码。这两个JIT都是用C(或者可能是C ++)编写的。
void *optable [] = {&&op_add,&&op_subtract,...}
和每个操作数为op_add:... goto * optable [* ip ++];
)。我已经看到了像你描述的切换解释器中的大幅增益。 - Ben Jackson