6

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

假设给定的 Java 代码是:

int a = 13372338;
int b = 32 * a;
return b;

现在,采用了以下方法(假设给定的内存从 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

这将像虚拟机本身一样简单地使用堆栈。关于这个解决方案的问题是:

  • 这种编译方法可行吗?
  • 是否有可能以这种方式实现所有 Java 指令?如何翻译诸如throw/instanceof 和类似命令之类的东西?
4

2 回答 2

5

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

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

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

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

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

如果你确实需要动态类加载,但预计它很少见,你也可以做得更好,尽管它需要更多的工作。优点是这种方法可以推广到其他事情,比如完全消除日志语句。基本思想是基于一些假设(例如,这static不会改变或没有加载新类)对代码进行专门化,然后在违反这些假设时进行反优化。这意味着您有时必须在代码运行时重新编译代码(这很难,但并非不可能)。

如果你沿着这条路走得更远,它的逻辑结论是基于跟踪的 JIT 编译,它应用于 Java,但 AFAIK 并没有证明它优于基于方法的 JIT 编译器。当您必须做出数十或数百个假设才能获得好的代码时,它会更有效,因为它发生在高度动态的语言中。

于 2013-09-25T20:39:37.813 回答
2

关于您的 JIT 编译器的一些评论(我希望我不要写“delnan”已经写过的东西):

通用注释

我确信“真正的”JIT 编译器与您的编译器类似。但是,您可以进行一些优化(例如:“mov eax,nnn”和“push eax”可以替换为“push nnn”)。

您应该将局部变量存储在堆栈上;通常“ebp”用作本地指针:

push ebx
push ebp
sub esp, 8 // 2 variables with 4 bytes each
mov ebp, esp
// Now local variables are addressed using [ebp+0] and [ebp+4]
  ...
pop ebp
pop ebx
ret

这是必要的,因为函数可能是递归的。将变量存储在固定位置(相对于 EIP)会导致变量表现得像“静态”变量。(我假设在递归函数的情况下你没有多次编译一个函数。)

试着抓

要实现 Try/Catch,您的 JIT 编译器不仅需要查看 Java 字节码,还需要查看存储在 Java 类的单独属性中的 Try/Catch 信息。Try/catch 可以通过以下方式实现:

  // push all useful registers (= the ones, that must not be destroyed)
 push eax
 push ebp
  ...
  // push the "catch" pointers
 push dword ptr catch_pointer
 push dword ptr catch_stack
  // set the "catch" pointers
 mov catch_stack,esp
 mov dword ptr catch_pointer, my_catch
  ... // some code
  // Here some "throw" instruction...
 push exception
 jmp dword ptr catch_pointer
  ... //some code
  // End of the "try" section: Pop all registers
 pop dword_ptr catch_stack
  ...
 pop eax
  ...
  // The "catch" block
my_catch:
 pop ecx // pop the Exception from the stack
 mov esp, catch_stack // restore the stack
  // Now restore all registers (same as at the end of the "try" section)
 pop dword_ptr catch_stack
  ...
 pop eax
 push ecx // push the Exception to the stack

在多线程环境中,每个线程都需要自己的 catch_stack 和 catch_pointer 变量!

可以通过以下方式使用“instanceof”处理特定的异常类型:

try {
    // some code
} catch(MyException1 ex) {
    // code 1
} catch(MyException2 ex) {
    // code 2
}

...实际上是这样编译的...:

try {
    // some code
} catch(Throwable ex) {
    if(ex instanceof MyException1) {
        // code 1
    }
    else if(ex instanceof MyException2) {
        // code 2
    }
    else throw(ex); // not handled!
}

对象

不支持对象(和数组)的简化 Java 虚拟机的 JIT 编译器将非常容易,但 Java 中的对象使虚拟机非常复杂。

对象简单地存储为指向堆栈上或局部变量中的对象的指针。典型的 JIT 编译器会像这样实现:对于每个类,都存在一段内存,其中包含有关类的信息(例如,存在哪些方法以及方法的汇编代码位于哪个地址等)。一个对象是一段包含所有实例变量的内存和一个指向包含类信息的内存的指针。

“Instanceof”和“checkcast”可以通过查看指向包含类信息的内存的指针来实现。此信息可能包含所有父类和已实现接口的列表。

然而,对象的主要问题是 Java 中的内存管理:与 C++ 不同,有一个“新”但没有“删除”。您必须检查对象的使用频率。如果一个对象不再被使用,它必须从内存中删除并且必须调用析构函数。

这里的问题是局部变量(同一个局部变量可能包含一个对象或一个数字)和 try/catch 块(“catch”块必须在恢复堆栈指针之前注意局部变量和包含对象的堆栈(!) )。

于 2013-09-26T06:48:03.433 回答