技术开发 频道

Java虚拟机会比硬件快吗?



在如上情况中,我并没有说明iload_1字节码的解释器段,因为它本质上与iload_0字节码的解释器段是相同的,除了一个不同的对段指针的偏移量外。
在如上代码中,我也指出了一些指令是用来计算下一个段的。我的意思是这个解释器段的指令地址用来执行下一个字节码是有效的。因此,注释“分出下一个段”实际上的意思是“跳到下一个字节码”。
因此,为了执行上面的a = a + b语句,使用了虚拟机解释器。
指令的数量:
6 (iload_0) + 6 (iload_1) + 7 (iadd) + 6 (istore_0) = 25 条指令
内存访问数量
5 (iload_0) + 5 (iload_1) + 4 (iadd) + 5 (istore_0) = 19 次内存访问量
对于内存访问,我仅仅计算了加载和存储的指令,而没有计算转向语句。
基于加强的ARM 1110处理器,在非常好的的条件下(所有的内存访问都是在缓存中准备),每一条指令将会至少执行一个机器循环(如果我没记错的话)。如果有一个错过了缓存,那么它的内存访问将会占据4个机器周期。另外,它将占用更多的时间。注意:Idmia指令占用两个周期的原因是因为它要载入两次。因此,在理想的情况下:
机器周期个数
1 + 5*1 (iload_0) + 1 + 5*1 (iload_1) + 4 + 4*1 (iadd) + 1 + 5*1 (istore_0) = 26 个机器周期
作为对比,让我们看看所有的内存访问会占用4个机器周期来完成的情况。这是最不理想的情况:
机器周期个数(有缓存丢失的情况):
1 + 5*4 (iload_0) + 1 + 5*4 (iload_1) + 4 + 4*4 (iadd) + 1 + 5*4 (istore_0) = 83个机器周期
Java硬件处理器
作为硬件处理器,指令作为它们自己的字节码执行。但是,执行它们的机器周期个数能基于解释器来估计。
对于一个JPU,它没有任何解释器。因此,这里没有任何的“下一段”,“下一段”变成了下一个字节码指令,它被JPU的程序计数器注册器所指向。
我也假定它是一个假定的注册器,它指向了当前的Java栈的起点。有了它,获得本地变量的地址的步骤能够被排除。
留下来的必要操作是JPU仍然必须执行(即使它在场景后面)。在如上的解释器段中,这个必要的操作是表示asm代码的行数。JPU的指令数如下:
ASM指令”的数量:
2 (iload_0) + 2 (iload_1) + 3 (iadd) + 2 (istore_0) = 9 条指令
内存访问数量:
2 (iload_0) + 2 (iload_1) + 3 (iadd) + 2 (istore_0) = 9次内存访问
机器周期个数 (没有内存丢失的情况):
2*1 (iload_0) + 2*1 (iload_1) + 1 + 3*1 (iadd) + 2*1 (istore_0) = 10个机器周期
机器周期个数(有内存丢失的情况):
2*4 (iload_0) + 2*4 (iload_1) + 1 + 3*4 (iadd) + 2*4 (istore_0) = 37个机器周期
   注意:JPU不能在执行字节码之前将其翻译成等价的ARM指令。但是,从内部来看,JPU仍然需要做一些等量的工作。我将会使用ARM指令作为一个被JPU硬件工作的一个近似值。
   另外还要注意,Idmia指令与两个load指令等价。如果你想要核对如上的数据要记住这一点。
   JIT
对于JITa = a + b语句将会编译成如下的ARM指令:
    ldr   r7, [rJFP, #-140]   //加载本地变量0
    ldr   r8, [rJFP, #-136]   //加载本地变量1
    add   r7, r7, r8           //将两者相加
    str   r7, [rJFP, #-140]   //将计算结果存储到本地变量0
rJFP是一个注册器,它被JIT用来包含Java框架指针。
实际点讲,最后的存储指令可能需要排除,这取决于字节码是否在它之后。因此,在某些时候,本地变量01的值可能已经在注册器中存在了,这取决于字节码是如何执行这些代码段的。在这种情况下,这2个载入指令可能也需要除去。因此,在理想的JIT条件下,a = a + bJIT编译成一个单独的ARM相加指令。但是在JPU情况下做一个保守的估计,我将会包括我们比较的载入和存储指令。因此,JIT对应的数量如下所示:
指令数量: 4条指令
内存访问的数量: 3次内存访问

机器周期数量 (无内存丢失的情况):  1 + 3*1 = 4个机器周期
机器周期数量(有内存丢失的情况): 1 + 3*4 = 13 个机器周期
   JIT下的理想情况下,我们仅仅将其编译成一条ARM相加指令。
指令数量: 1条指令
内存访问的数量: 0次内存访问

机器周期数量 (无内存丢失的情况):  1个机器周期
机器周期数量(有内存丢失的情况): 1个机器周期
数量的比较
下面是两者耗时度的比较:
机器周期数
解释器
JPU
JIT
理想的JIT
无内存丢失的情况
26
10 (2.60x)
4 (6.50x)
1 (26.0x)
有内存丢失的情况
83
37 (2.24x)
13 (6.38x)
1 (83.0x)
   表格中的x指明对解释器的提速率。基于这个试验例子,JIT在表现上明显强于JPU。原因在于JIT能够除去某些或全部内存访问(通过保存工作数据在注册器中)处理时间。对比来说,JPU受虚拟机说明的堆栈机器架构(它招致了很多的内存访问)的约束。
   但是在你为这些数字变得疯狂之前,让我们给你降降温。在真实世界中,你可能看不到理论上的JIT在如上环境中的26x83x的加速。这是因为JIT并不总是能够保持所有的工作数据都存储在寄存器中,因为这里只有有限的寄存器集合。这个限制被称为寄存器压力。这在某些时候确实是存在的,但是并不是在所有的情况下,因此,JIT时间的真实数据更接近于JIT在非理想情况下的数据,而不是理想情况下的数据。
   为了对JPU公平一点,围绕堆栈架构问题,一个JPU将可能实现一个堆栈缓存,这在一些注册器将会映像在一些高层的N值的Java操作栈上。它也存在一些本地的注册器映像。这点有效的允许JPUJIT的理想情况一样避免内存访问,因此理论上允许它获得对JIT理想情况下的内存访问速度。但是,注册器压力也像JIT一样限制了JPU
   另外,JPU仅仅能够映像栈中的顶部的大部分操作数,并且可能是最先的少数本地变量。在注册器中的堆栈操作数和/或本地变量获得映像,这通常在运行过程中是不可配置的。但是JIT能够动态的处理这种情况,并且能够任意的选择哪一个参数和/或本地变量能够保存在注册器中。因此,JIT相对JPU来说,具有降低内存访问率的更高的可能性。
0
相关文章