即时编译Java字节码

max*_*dev 6 c++ assembly jit jvm vm-implementation

我们目前正在研究自己的Java虚拟机实现的JIT编译部分.我们现在的想法是将给定的Java字节码简单地转换为操作码,将它们写入可执行内存并直接调用方法的开头.

假设给定的Java代码是:

int a = 13372338;
int b = 32 * a;
return b;
Run Code Online (Sandbox Code Playgroud)

现在,进行了以下方法(假设给定的内存从0x1000开始,并且在eax中预期返回值):

0x1000: first local variable - accessible via [eip - 8]
0x1004: second local variable - accessible via [eip - 4]
0x1008: start of the code - accessible via [eip]

Java bytecode | Assembler code (NASM syntax)
--------------|------------------------------------------------------------------
              | // start
              | mov edx, eip
              | push ebx
              |         
              | // method content
ldc           | mov eax, 13372338
              | push eax
istore_0      | pop eax
              | mov [edx - 8], eax
bipush        | push 32
iload_0       | mov eax, [edx - 8]
              | push eax
imul          | pop ebx
              | pop eax
              | mul ebx
              | push eax
istore_1      | pop eax
              | mov [edx - 4], eax
iload_1       | mov eax, [edx - 4]
              | push eax
ireturn       | pop eax
              |         
              | // end
              | pop ebx
              | ret
Run Code Online (Sandbox Code Playgroud)

这就像虚拟机本身一样只使用堆栈.有关此解决方案的问题是:

  • 这种编译方法是否可行?
  • 是否有可能以这种方式实现所有Java指令?怎样才能翻译像athrow/instanceof和类似的命令?

小智 5

这种编译方法很容易启动和运行,至少可以消除解释开销.但它会导致相当大量的代码和非常糟糕的性能.一个大问题是,即使目标机器(x86)是寄存器机器,它也会以1:1的顺序进行音译操作.正如您在发布的代码段(以及任何其他代码)中所看到的,这总是会导致每个操作的几个堆栈操作操作码,因此它使用寄存器 - heck,整个ISA - 尽可能无效.

可以支持复杂的控制流程,例如异常.它与在解释器中实现它没有什么不同.如果您想获得良好的性能,则不希望每次进入或退出try块时都执行工作.C++和其他JVM都使用了避免这种情况的方案(关键字:零成本或表驱动的异常处理).这些实现,理解和调试都非常复杂和复杂,因此您应该首先使用更简单的替代方案.请记住它.

至于生成的代码:第一个优化,你几乎肯定需要的是,将堆栈操作转换为三个地址代码或其他使用寄存器的表示.有几篇关于此的论文和实现,所以除非你要我,否则我不会详细说明.然后,当然,您需要将这些虚拟寄存器映射到物理寄存器.寄存器分配是编译器构造中研究最多的主题之一,并且至少有六种启发式算法在JIT编译器中使用起来相当有效且足够快.我头脑中的一个例子是线性扫描寄存器分配(专门为JIT编译创建).

除此之外,大多数JIT编译器专注于生成代码的性能(与快速编译相反)使用一种或多种中间格式并优化此形式的程序.这基本上是你的磨机编译器优化套件的运行,包括常数传播,值编号,重新关联,循环不变代码运动等等老手 - 这些东西不仅易于理解和实现,它们也被描述在三十年的文学作品中,包括教科书和维基百科.

使用基元,数组和对象字段的straigt-line代码对上面提到的代码非常有用.但是,您根本无法优化方法调用.每个方法都是虚拟的,这意味着内联甚至移动方法调用(例如循环外)基本上是不可能的,除非在非常特殊的情况下.你提到这是针对内核的.如果你可以接受使用没有动态类加载的Java子集,你可以通过假设JIT知道所有类来做得更好(但它将是非标准的).然后,您可以检测叶类(或更常见的方法,从不覆盖)并内联这些类.

如果你确实需要动态类加载,但期望它很少,你也可以做得更好,虽然它需要更多的工作.优点是这种方法可以推广到其他方面,例如完全消除日志记录.其基本思路是专门基于一些假设的代码(例如,这static不会改变或没有新的类加载),然后去优化,如果这些假设被违反.这意味着你有时必须在代码运行时重新编译代码(这很难,但并非不可能).

如果你进一步走这条路,它的逻辑结论是基于跟踪的JIT编译,它被应用到Java,但据我所知它并没有变成优于基于方法的JIT编译器.当你需要做几十个或几百个假设来获得好的代码时,它会更有效,就像高动态语言一样.