基于寄存器与基于栈的虚拟机

什么是虚拟机

虚拟机是借助于操做系统对物理机器的一种模拟。可是咱们今天所讲述的虚拟机概念比较狭义,与vmware或者virtual-box不一样,而是针对具体语言所实现的虚拟机。例如在JVM或者CPython中,JAVA或者python源码会被编译成相关字节码,而后在对应虚拟机上运行,JVM或CPython会对这些字节码进行取指令,译码,执行,结果回写等操做,这些步骤和真实物理机器上的概念都很类似。相对应的二进制指令是在物理机器上运行,物理机器从内存中取指令,经过总线传输到CPU,而后译码、执行、结果存储。html

虚拟机为了可以执行字节码,须要模拟出物理CPU可以执行的相关操做,与虚拟机实现相关的概念以下:node

(1)将源码编译成VM所能执行的具体字节码。 
(2)字节码格式(指令格式),例如三元式,树仍是前缀波兰式。 
(3)函数调用相关的栈结构,函数的入口,出口,返回以及如何传参。还有为了可以顺利返回所需的相关栈帧信息如何布置。 
(4)一个“指令指针”,指向下一条待执行的指令(内存中),对应物理机器的EIP。 
(5)一个虚拟“CPU”-指令调度器,python

  • 获取下一条指令
  • 对操做数进行解码
  • 执行这条指令

这三点是解释器执行字节码最重要的开销。数组


虚拟机的实现方式

现在虚拟机的实现方式有两种,基于栈的和基于寄存器的,这两种实现方式各有优劣,也都有标志性的产品。基于栈的虚拟机,有JVM,CPython以及.Net CLR。基于寄存器的,有Dalvik以及Lua5.0,另外Perl据说也要改成基于寄存器方式。不管这两种方式实现机制如何,都要实现如下几点:架构

  • 取指令,其中指令来源于内存
  • 译码,决定指令类型(执行何种操做)。另外译码的过程要包括从内存中取操做数
  • 执行。指令译码后,被虚拟机执行(其实最终都会借助于物理机资源)
  • 存储计算结果

其实这和物理机CPU的执行是很类似的,都包括取值,译码,执行,回写等步骤。可是不一样的一点是虚拟机应该模仿不出流水线,例如在当前指令译码完成以后,CPU中的译码部件处于空闲状态,能够用来对下一条指令进行译码,因此流水线有多少级就至关于能够并行执行多少指令。固然中间还有些指令相关和乱序的概念,这里就不详说了。函数

下图中一个典型的指令流水线结构,因为虚拟机在操做系统上经过程序模拟,遵循冯诺依曼结构顺序执行的,应该很难实现出流水线结构。布局

这里写图片描述

基于栈的虚拟机

基于栈的虚拟机有一个操做数栈的概念,虚拟机在进行真正的运算时都是直接与操做数栈(operand stack)进行交互,不能直接操做内存中数据(其实这句话不严谨的,虚拟机的操做数栈也是布局在内存上的),也就是说无论进行何种操做都要经过操做数栈来进行,即便是数据传递这种简单的操做。这样作的直接好处就是虚拟机能够无视具体的物理架构,特别是寄存器。但缺点也显而易见,就是速度慢,由于不管什么操做都要经过操做数栈这一结构。性能

因为执行时默认都是从操做数栈上取数据,那么就无需指定操做数。例如,x86汇编”ADD EAX, EBX”,就须要指定此次运算须要从什么地方取操做数,执行完结果存放在何处。可是基于栈的虚拟机的指令就无需指定,例如加法操做就一个简单的”Add”就能够了,由于默认操做数存放在操做数栈上,直接从操做数栈上pop出两条数据直接执行加法运算,运算后的结果默认存放在栈顶。其中操做数栈(operand stack)的深度由编译器静态肯定,方便给栈帧预分配空间。这个和不能再栈上定义变长数组类似(其实这句话不严谨的,栈上分配变长数组,须要编译器的支持,分配在栈顶),因为局部变量的地址只能在编译期(compile time)肯定针对当前栈帧的offset,若是中间有一个变量是一个变长数组的话,那么后面变量的offset就没法肯定了(vector的数据是分配在堆上的,本身控制)。优化

例如执行”a = b + c”,在基于栈的虚拟机上字节码指令以下所示:网站

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
I1: LOAD C I2: LOAD B I3: ADD I4: STORE A

因为操做数都是隐式地,因此指令能够作的很短,通常都是一个或者两个字节。可是显而易见就是指令条数会显著增长。而基于寄存器虚拟机执行该操做只有一条指令,

 
 
 
 
  • 1
  • 1
I1: add a, b, c

其中a,b,c都是虚拟寄存器。操做数栈上的变化以下图所示:

首先从符号表上读取数据压入操做数栈,

这里写图片描述

而后从栈中弹出操做数执行加法运算,这步操做有物理机器执行,以下图所示:

这里写图片描述

从图示中能够看出,数据从局部变量表中还要通过一次操做数栈的操做,注意操做数栈和局部变量表都是存放在内存上,内存到内存的数据传输在x86的机器上都是要通过一次数据总线传输的。能够得出一次简单的加法基本上须要9次数据传输,想一想都很慢。

可是基于栈的虚拟机优势就是可移植,寄存器由硬件直接提供。使用栈架构的指令集,用户程序(编译后的字节码)不会直接使用硬件中的寄存器,同时为了提升运行时的速度,能够将一些访问比较频繁的数据存放到寄存器中以获取尽可能好的性能。另外,基于栈的虚拟机中指令更加紧凑,一个字节或者两个字节便可存储,同时编译器实现也比较简单,不用进行寄存器分配。寄存器分配是一门大学问

基于寄存器的虚拟机

前面提到过基于栈的虚拟机,这里咱们简要介绍一下基于寄存器的虚拟机运行机制。

基于寄存器的虚拟机中没有操做数栈的概念,可是有不少虚拟寄存器,通常状况下这些寄存器(操做数)都是别名,须要执行引擎对这些寄存器(操做数)的解析,找出操做数的具体位置,而后取出操做数进行运算。

既然是虚拟寄存器,那么确定不在CPU中(想一想也不该该在CPU中,虚拟机的根本目的就是跨平台和兼容性),其实和操做数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。

新的虚拟机也用栈分配活动记录,寄存器就在该活动记录中。当进入Lua程序的函数体时,函数从栈中分配一个足以容纳该函数全部寄存器的活动记录。函数的全部局部变量都各占据一个寄存器。所以,存取局部变量是至关高效的。

上面就是Lua虚拟机对寄存器的相关描述,示意图以下:

这里写图片描述

从上图中咱们能够看到,其实“寄存器”的概念只是当前栈帧中一块连续的内存区域。这些数据在运算的时候,直接送入物理CPU进行计算,无需再传送到operand stack上而后再进行运算。例如”ADD R3, R2, R1”的示意图就以下所示:

这里写图片描述

其实”ADD R3, R2, R1”还要通过译码的一个过程,固然当前这条指令的种类和操做数由虚拟机进行解释。后面咱们会看到,在有些实现中,有一个很大的switch-case来进行指令的分派及真正的运算过程。

下图是Lua虚拟机的一些指令,该图片来自这篇文章,中译文这里

这里写图片描述

使用寄存器式虚拟机没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈(push/pop)指令。同时指令更紧凑更简洁。可是因为显示指定了操做数,因此基于寄存器的代码会比基于栈的代码要大,可是因为指令数量的减小,其实没有大多少。

栈式虚拟机 VS 寄存器式虚拟机

(1)指令条数:栈式虚拟机多 
(2)代码尺寸:栈式虚拟机 
(3)移植性:栈式虚拟机移植性更好 
(4)指令优化:寄存器式虚拟机更能优化

栈式 VS 寄存器式 对比
指令条数 栈式 > 寄存器式
代码尺寸 栈式 < 寄存器式
移植性 栈式优于寄存器式
指令优化 栈式更不易优化
解释器执行速度 栈式解释器速度稍慢
代码生成难度 栈式简单
简易实现中的数据移动次数 栈式移动次数多

解释器最重要的开销在于指令调度(instruction dispatch),指令调度主要操做包括从内存中取出指令,而后跳转到解释器相对应的代码段,而后执行这条指令。其中一个简易实现就是使用switch-based的方式来进行,这种方式简单易实现,另外任何语言都有相应的switch语句。switch-based的指令调度,经过一个死循环不断的从内存取出指令来执行,针对不一样的指令选择不一样的执行方式。

一种JVM基于SBD实现方式以下图所示:

这里写图片描述 
注:图片来自这里

这种方式实现加单,代码移植性好,可是有一个缺点就是分支预测失效的几率比较高。

如今的CPU都是基于流水线结构的,间接跳转指令的跳转结果须要等到执行级才能知晓,若是预测失败须要排空流水线,流水线级数越多分支预测失败致使流水线排空的时间越长。

因为编译后的指令是随机的,不太可能提取出预测模式。《》

相关文章
相关标签/搜索