前面说到,虚拟机是真机的一种模拟,而栈虚拟机模拟的是基于栈计算的机器,和现在常见的基于寄存器的硬件机器不同,于是相应的也有基于寄存器的虚拟机,不过这个虚拟机可能跟真机差别比较大
首先可以看看真机用寄存器的原因,计算机的存储有一个规律,访问速度越快的存储,单价(单位容量的成本)越高,因此实际实现的时候,容量会很受限,反之因为便宜而容量可以很大的存储,储存速度就慢,访问速度(或单价)从高到底大致是寄存器,cpu cache,内存,机械磁盘,磁带等,当然也有像ssd这种,介于内存和机械磁盘之间,为了减少耦合和简化设计,cpu的计算是面向寄存器(或类似的计算芯片,例如x86的FPU是基于栈实现,当然这个栈不是内存上的,是硬件内置),对于内存,主要是load和store操作,也就是说,做运算时将数据从内存load到寄存器,计算过后再store到内存里,编译器会分析出某些变量如果store后会被再次load,就不会load,因为已经在寄存器了;或者发现某些变量在一段代码中反复store到内存再load出来使用,可能直接把这些操作都优化了,只保留寄存器操作,可以大幅度提高执行速度。当然,寄存器越多,这种优化的空间就越大
寄存器虚拟机的指令,和汇编指令差距不大,就不详细说明了,和栈虚拟机的指令的差别在于,运算大都是二地址或三地址指令
如果和基于栈的模型比较(比如上面说的FPU),可以看到大致就是把栈换成寄存器堆,假设不考虑寄存器数量的问题,以及寄存器存取速度,则区别似乎在于:
1 栈是单向增长的,只能对栈顶做push和pop操作,如果pop中间的内容,则需要O(N)时间的移动(实际上也可以通过标记删除来避免移动,但增加了复杂性)
2 寄存器的数据store后,可以继续使用
3 栈只能运算栈顶的若干数据,如果运算的数据可能重复,需要拷贝,比如a=a+a:
1 | load a |
而寄存器架构的就可以:
1 | load r0, a |
看上去似乎是寄存器架构优势,但是如果我们考虑具体情况,差距并没有那么大,对于1,大多数运算在理论上是基于AST的,就算用了寄存器,也是按栈的顺序计算罢了,而对于2,我们只要给栈增加一个store_no_pop指令即可,两者在数据存取和运算上差别不是很大
如果考虑到虚拟机,那么连存取速度优势都不存在了,因为是用内存来模拟寄存器,其实还是内存拷来拷去。不过,由于寄存器虚拟机的架构是模拟真机,因此在具体的虚拟机实现时,可以将虚拟寄存器映射到真实寄存器(运行时修改字节码为机器码之类的)以提高运行速度,但反过来说,栈虚拟机架构的栈也是能映射的,我们只需要将栈顶n个元素映射到n个寄存器,剩下无法映射的留在内存里,如果有入栈,则将逻辑上最下面的寄存器压入内存栈,然后寄存器轮转
那么,如果不考虑硬件映射,纯粹讨论虚拟机实现,两者究竟谁有优势呢,一般认为从执行速度来看,寄存器虚拟机还是有些优势的,原因在于,寄存器虚拟机可以充分利用寄存器做临时结果存储和结果复用,减少load和store的次数,另外,栈虚拟机的指令虽然更紧凑(参数少,一条指令甚至可以不超过两个字节),占用内存少,但是相应的会执行更多次的解释循环,而基于寄存器的虚拟机能用更少的指令数量来完成同样的工作(尽管每条指令都比较大),具体的,可以参考这篇VEE的论文,解释比较清楚,VEE是ACM旗下一个专门研究虚拟执行环境的会议:
http://www.usenix.org/events/vee05/full_papers/p153-yunhe.pdf
这篇论文显示,基于寄存器架构的虚拟机执行速度还是能比栈架构的快不少,即便用上threaded code技术(后面再讲)
不过话说回来,寄存器架构省略load和store在栈架构上也是能部分实现的,可以增加一些复杂指令,比如a=a+a:
1 | load_add_result a a |
反正读进来也是在内存做运算,不如直接算好load进来,对于复杂指令a=a+b-c*d/e:
1 | load_add_result a b |
只是这样做的似乎不多
既然寄存器虚拟机执行快些,而且硬件映射也简单,那为何java,python,ruby等主流实现都是基于栈的虚拟机呢,个人觉得有几点原因:
一,栈虚拟机字节码的编译器实现简单,不需要考虑太多的临时空间分配问题
二,不同机器的寄存器数量甚至硬件架构都不同,用栈架构可以做到最大移植性,比如,代码要运行在一个基于栈的硬件架构上,则栈虚拟机可以直接映射,就算在寄存器硬件架构上也可以映射,但反过来寄存器架构的虚拟代码映射到栈架构硬件上可能不是那么容易,这是一个包容性的问题,就好像前面说的java转C++容易,C++转java就不太容易
三,目标机器的指令cache可能比较小,栈架构的代码紧凑比较适合
四,真要为了效率的话,与其为了30%的效率提升而将编译过程复杂化,倒不如把精力投在jit上,收益更大
基于栈虚拟机和基于寄存器虚拟机的比较
1、指令条数:栈式>寄存器式,例如一个加法运算 a = b + c 的字节码指令:
栈式:
I1: LOAD C
I2: LOAD B
I3: ADD
I4: STORE A
寄存器式:
I1: add a, b, c
2、代码尺寸:栈式<寄存器式,这个在 Android 开发中会经常遇到,众所周知传统的 JVM 是基于栈的,而 Android 中的Dalvik 虚拟机则是基于寄存器的,对于同一段 Java 代码,在 Android 上的 dex 文件会大一些。
3、可移植性:栈式 > 寄存器式,对于不同的平台,例如 ARM,x86,x64 等,栈的概念是相同的,但是寄存器在不同的平台上,有着不同的实现。
4、指令优化:栈式 < 寄存器式。
5、解释器执行速度:栈式 < 寄存器式。
6、代码生成速度:栈式 > 寄存器式。
7、简易实现中的数据移动次数:栈式 > 寄存器式,这个很好理解,栈式寄存器需要不断的更新栈,而寄存器式则不需要。
8、两种虚拟机实现的不同主要在于对操作数和结果的存储、检索机制不一样。
基于Stack的虚拟机会通过中断处理来获取操作数,其操作数保存在Stack数据结构中。从栈中取出数据、计算然后再将结果存入栈中(LIFO,Last in first out)。
基于Register的虚拟机,其操作数存放在CPU寄存器中。没有入栈和出栈的操作,但是执行的指令因而需要包含操作数的地址,不可像栈那样用栈指针去操作。
【[译] 400 行 C 代码实现一个虚拟机(2018)】https://arthurchiao.art/blog/write-your-own-virtual-machine-zh/
【JVM架构 |栈式指令集与寄存器指令集有什么区别?】http://t.csdn.cn/n0JQn