背景

经常看到说Dalvik和ART是基于寄存器的,而JVM是基于栈的,那么由此引出了如下问题。

有如下问题

  • 基于寄存器这个概念指的是什么?
  • 在基于寄存器的架构下,方法运行是否产生栈帧?
  • 基于栈和基于寄存器有什么区别?

基于寄存器这个概念指的是什么?

有如下代码:

a = b + c;

如果变成

add a,b,c

这样看起来就更像机器指令了,这就是三地址指令,一般形式为:

op dest, src1, src2

许多操作都是二元运算加赋值,三地址指令恰好可以指定两个源和一个目标,能非常灵活的支持二元操作与赋值。ARM处理器的主要指令集就是三地址形式。

如下代码:

a += b

变成

add a,b

这就是二地址指令,一般形式为:

op dest, src

它要支持二元操作,就必须把其中一个源同时也作为目标。add a,b在执行过后就会破坏a原有的值,而b的值保持不变。x86系列的处理器就是二地址形式的。

二地址和三地址指令集,一般就是通过基于寄存器的架构来实现的。

指令集可以是n地址的。n如果等于0的话呢?有如下一段Java字节码:

iconst_1  
iconst_2  
iadd  
istore_0  

iadd表示整型加法。指令没有任何参数,源与目标都是隐含参数,实际上是依赖于一种数据结构,就是栈。

iconst_1iconst_2分别向一个叫做求值栈的地方压入整型常量,iadd指令则从栈顶弹出两个值,将值相加,并将结果压回栈顶。istore_0指令从求值栈顶弹出一个值,并将值保存到局部变量区。

零地址指令集一般通过基于栈的结构来实现。这个栈为求值栈,而不是系统调用栈。

由于零地址指令的源和目标都是隐含的,所以指令密度可以非常高,即可以用更少的空间存放更多的指令。但是零地址要完成一件事,一般会比二地址和三地址需要更多条指令,这也意味着更多的指令分派次数和内存访问次数。访问内存是执行速度的一个重要瓶颈,因此,一般认为基于寄存器的架构对VM来说是更快的。

在基于寄存器的架构下,方法运行是否产生栈帧?

当然会。

从可移植性的角度考虑:假如一个VM采用基于寄存器的架构(它接受的指令集大概就是二地址或者三地址形式的),为了高效执行,一般会希望能把源架构中的寄存器映射到实际机器上寄存器上。但是VM里有些很重要的辅助数据会经常被访问,例如一些VM会保存源指令序列的程序计数器(program counter,PC),为了效率,这些数据也得放在实际机器的寄存器里。如果源架构中寄存器的数量跟实际机器的一样,或者前者比后者更多,那源架构的寄存器就没办法都映射到实际机器的寄存器上;这样VM实现起来比较麻烦,与能够全部映射相比效率也会大打折扣。像Dalvik VM的解释器实现,就是把虚拟寄存器全部映射到栈帧(内存)里的,这跟把局部变量区与操作数栈都映射到内存里的JVM解释器实现相比实际区别不太大。

如果一个VM采用基于栈的架构,则无论在怎样的实际机器上,都很好实现——它的源架构里没有任何通用寄存器,所以实现VM时可以比较自由的分配实际机器的寄存器。于是这样的VM可移植性就比较高。作为优化,基于栈的VM可以用编译方式实现,“求值栈”实际上也可以由编译器映射到寄存器上,减轻数据移动的开销。

基于栈和基于寄存器有什么区别?

基于栈与基于虚拟机的指令集,用在解释器里,有以下对比:

  • 源码生成难度:基于栈 < 基于寄存器(差别不大)
  • 同样程序逻辑的代码大小:基于栈 < 基于寄存器
  • 同样程序逻辑的指令条数:基于栈 > 基于寄存器
  • 实现中数据移动次数:基于栈 > 基于寄存器
  • 采用同等优化程度的解释器速度:基于栈 < 基于寄存器
  • 交由同等优化程度的JIT编译器编译后生成的代码速度:基于栈 = 基于寄存器

因此,要追求

  • 尽量实现简单:选择基于栈
  • 传输代码尽量小:选择基于栈
  • 纯解释执行的解释器速度:选择基于寄存器
  • 带有JIT编译器的执行引擎的速度:两者一样。

Dex和JAR大小比较

基于栈的设计没有让Java的代码传输大小减少多少,主要因为:Java代码是以class文件为单位来传输与存储的。Java设计之初就坚持支持分离编译与按需动态加载,导致Java的class文件必须独立,即每个class都必须携带自己的常量池,主要信息是字符串与其他常量的值,以及用于符号链接的符号引用信息。class文件里表示程序逻辑的代码部分(字节码)只占class文件的一小部分,而大部分都被常量池占了。而且多个class常量池之间常常有重叠,所以多个class文件就存在冗余信息,不利于减少传输、存储代码的大小。

那么dex为什么比jar要小呢?

一个dex相当于一个或多个jar包,里面包含多个class文件对应的内容,一个dex文件里所有的class都共享同一个常量池,因而不会导致class之间存在冗余。dex就会比jar小很多。

所以dex小不小和Dalvik基于寄存器没有关系,并且在字节码部分,Dalvik的字节码要比JVM的字节码更大。

而在Java世界中也发现了这个问题,也有相应的解决办法,有一个传输、存储格式叫做pack200,采用的和dex类似的压缩思路,将jar包里全部的class文件的常量池汇总为一个,这样打包就会小很多了。

参考文章:虚拟机随谈栈式虚拟机和寄存器式虚拟机