2013-09-25 90 views
6

的即时编译目前,我们正在对我们自己的Java虚拟机实现的JIT编译的一部分。现在我们的想法是将给定的Java字节码简单地转换为操作码,将它们写入可执行的内存并调用到方法的开头。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指令这样?像athrow/instanceof和类似的命令怎么能被翻译?
+1

这与C++有什么关系? –

+0

那么虚拟机,因此实际编译和生成的方法的调用是用C++实现的,可能不是最相关的标记,但也很重要。 – maxdev

+0

您发布的代码都不是C++。您正在询问如何在汇编中实现Java字节码。 –

回答

5

这种编译方法很容易启动和运行,至少可以消除解释开销。但它会产生大量的代码和非常糟糕的性能。一个大问题是,它音译堆栈操作1:1,即使在目标机器(86)是一个寄存器机。正如你可以在你发布的代码片段(以及的任何其他代码)中看到的那样,这总是会产生几个堆栈操作操作码,每个操作都有一个,所以它使用寄存器 - 就是整个ISA - 关于as尽可能无效。

可以还支持复杂的控制流程,如例外。与在翻译中实施它没有多大区别。如果您想要获得良好的性能,则每次输入或退出try块时都不希望执行任何工作。有一些方案可以避免这种情况,由C++和其他JVM(关键字:零成本或表驱动的异常处理)使用。这些实现,理解和调试相当复杂和复杂,所以你应该首先使用更简单的替代方案。只要记住它。

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

除此之外,集中在所生成的代码(相对于快速编译)的性能最JIT编译器使用一个或多个中间格式和优化在这种形式的程序。这基本上是你运行mill编译器优化套件的原因,包括像常量传播,值编号,重新关联,循环不变代码运动等等的退伍军人 - 这些东西不仅易于理解和实现,而且还被描述过在三十年的文学和包括教科书和维基百科。

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

如果你确实需要动态类加载,但是期望它很少,你也可以做得更好,尽管它需要更多的工作。优点是这种方法可以推广到其他方面,比如完全消除日志记录。基本思路是基于一些假设(例如,这个static不会改变或者没有加载新的类)来专门化代码,然后如果违反了这些假设,则去优化代码。这意味着您有时必须在运行时重新编译代码(这是,但并非不可能)。如果你走得更远,它的逻辑结论就是基于跟踪的JIT编译,其中已将应用于Java,但AFAIK并没有证明它优于基于方法的JIT编译器。当您需要做出数十或数百个假设才能获得优质代码时,它会更有效,因为它会发生在高度动态的语言中。

+0

+1,很好的答案!在那里我想开始写一个抖动:)。两个问题虽然 - 你说OP的建议仍然比解释更好。这与解释者的实际情况有何不同?其次,你提到常量传播,LICM等等 - 你能指出一个只有@runtime可用的JIT优化列表吗?什么会让JIT真正照耀静态编译器? – Leeor

+0

@Leeor OP的建议比普通的解释器更好,因为它消除了通常必须在各个操作之间执行的字节码分派和相关代码。这些操作,即代码的繁忙结束,实际上*做某事*的部分,非常像在解释器中。 – delnan

+0

@Leeor你的第二个问题是神圣战争的话题,一般来说很难回答。或多或少有些确定的是,在某些情况下,大多数甚至是*推测性优化所需的知识(是的,AOT编译器也可以这样做)在编译时不可用,但在运行时可用。哪些最佳化受此影响,以及这种影响的重要性各不相同,并且会受到激烈的争论。 – delnan

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

要实现的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”块必须关心局部变量和包含对象的堆栈(!)恢复堆栈指针)。