节:区分栈的指令集架构和寄存器的指令集架构

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了节:区分栈的指令集架构和寄存器的指令集架构相关的知识,希望对你有一定的参考价值。

这一讲,我们来说一下JVM的架构模型。

JVM的架构模型

首先,大家要知道一点,就是Java编译器输入的指令流基本上是一种基于栈的指令集架构,而另外一种指令集架构则是基于寄存器的指令集架构。从这点也能看出,指令集的架构模型一共分为两种,一种是基于栈的指令集架构,一种是基于寄存器的指令集架构

我之所以会讲Java编译器输入的指令流基本上是一种基于栈的指令集架构,是因为HotSpot虚拟机当中的任何操作都得经过一个入栈和出栈的过程,而这就是我们通常意义上所说的栈管运行,因此,不难知道HotSpot虚拟机中的执行引擎架构其实就是一种基于栈的指令集架构

此外,大家还要明确的一点是HotSpot虚拟机除了PC寄存器(英文的叫法是Program Counter Register)之外,它就再没有包含其他的寄存器了。

当然,接下来我们也会对比一下基于栈的指令集架构和基于寄存器的指令集架构这两种架构之间的区别。

基于栈式架构的特点

首先来看第一个特点,即设计和实现更简单,适用于资源受限的系统

看到这里,有些小伙伴可能心想,完了,这什么鸡儿意思啊,我也看不懂啊😊!看不懂,很正常,谁也不是什么天才,不过要是经过我下面的解释你还不懂,那你趁早该干嘛就干嘛去吧!

之所以基于栈式的架构的设计和实现更简单,是因为Java程序的运行都是通过一个一个的方法来实现的,而每执行一个方法,我们就可以理解成是一个入栈操作,很显然,位于栈顶的就是我们当前正在执行的方法,而每当一个方法执行完毕之后,那就意味着是做了一个出栈操作了。

上面还说了,基于栈式的架构还能适用于资源受限的系统,我想这里我不得不举一个例子来给大家说一下了。就拿嵌入式的一些小型设备来说,例如机顶盒、打印机等等,它们就属于一个资源受限的场景,最初高斯林这个团队在设计Java语言的时候,也是希望它能应用在资源受限的系统当中。

然后,我们来看一下第二个特点,即避开了寄存器的分配难题,使用零地址指令方式进行分配

既然这里提到了零地址指令,那么对应的肯定就会有一地址指令、二地址指令以及三地址指令了。读到这里,大家肯定会一脸懵逼,心想这他妈都是些什么啊😊!还是那句话,看不懂,非常正常,正是鉴于此,下面我就必须得给大家解释解释了。

一个指令正常被执行的时候,它是需要具有两部分的,一部分是它的地址,例如地址是1,而另一部分则是它的操作数,例如操作数是3,画图表示如下。

以上就是一个一地址指令,即只有一个地址和一个操作数。

相应地,二地址指令就很好理解了,二地址指令指的无非就是有两个地址和一个操作数罢了。同理,三地址指令指的就是有三个地址和一个操作数了。至于所谓的零地址指令,那就是没有地址,只有操作数了。

为什么栈它不需要地址呢?我想大家都知道内存中栈的内存结构吧!栈是只有一个入栈/出栈操作的,而且它只针对栈顶的数据进行操作,对于栈中其他的数据则暂时不操作,所以栈就不怎么需要地址了。

接着,我们来看一下第三个特点,即指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。此外,指令集更小,编译器容易实现

零地址指令上面我已经给大家解释过了,正是由于基于栈式的架构使用的是零地址指令方式来进行的分配,所以它就不像其他地址指令那样还得分配地址搞得那么麻烦了,那显然编译器就会比较容易实现了。

最后,我们来看一下最后一个特点,即不需要硬件支持,可移植性更好,更好实现跨平台

以上是一个非常重要的特点,所以我得给大家好好唠唠。

相信大家都知道JVM具有跨语言的平台的这样一个特征,就是可以在不同的平台上去安装JVM,然后使得同一个Java源文件能实现跨平台的执行,而正是鉴于此,我们才需要去考虑基于栈式的这样一种架构。而且,由于栈是内存层面上的一个概念,所以基于栈式的这样一种架构就不需要和硬件打交道了,当然,可移植性也就会变得更好了。

基于寄存器架构的特点

基于寄存器架构的典型应用便是x86的二进制指令集了,比如传统的PC以及android的Davlik虚拟机等选择的就是基于寄存器的这样一种架构。

那基于寄存器的这样一种架构有什么特点呢?带着这样一个疑问,大家就继续往下读吧!

基于寄存器架构的第一个特点是指令集架构完全依赖硬件,可移植性差。很明显,这也是基于寄存器架构的一个缺点。

基于寄存器架构的第二个特点是性能优秀和执行更高效。很明显,这也是基于寄存器架构的一个优点。

之所以基于寄存器架构有以上优缺点,是因为它需要完全依赖于CPU,由CPU来直接执行指令,而且,指令还得在高速缓存区当中进行执行。这当然就会导致基于寄存器架构的指令执行速度就要比基于栈式架构的快了,但是,由于指令直接是由CPU来执行的,所以基于寄存器的这样一种架构就跟硬件的耦合度比较高了,可移植性当然就会比较差了。

基于寄存器架构的第三个特点是花费更少的指令去完成一项操作。关于这一特点,下面我得给大家好好唠唠才行,一定要讲得让大家理解透彻才罢手。

上面我们说了,基于栈式架构的其中一个特点便是指令集更小,其原因就是因为基于栈式的这样一种架构指令流中的指令大部分都是零地址指令,而零地址指令的字节码文件又是以每8位为一个基本单位来进行对齐的。关于这个零地址指令更加详细的内容,我会留到下一篇(即字节码与类的加载篇)中来专门给大家进行讲解,到时候我肯定会带着大家一起去分析一些具体的字节码文件,所以还请大家拭目以待!

相比基于栈式架构而言,基于寄存器的这样一种架构采用的便是16位双字节的对齐方式来进行对齐了。

虽然基于栈式架构的指令集更小,但是完成同样的操作,它用的指令只会比基于寄存器架构的多,也就是说基于寄存器的这样一种架构用的指令会更少。

为了说明以上这点,这里我会给大家举一个例子。

如果我们想执行2+3这样一个非常简单的逻辑操作,那么以上两种指令集架构模型所用到的指令又会分别是什么呢?这里,我直接给大家答案吧!也不为难大家了。

基于栈的计算流程如下,当然,这里肯定是以Java虚拟机为例来说的。

iconst_2    // 常量2入栈
istore_1
iconst_3    // 常量3入栈
istore_2
iload_1
iload_2
iadd        // 常量2、3入栈,执行相加
istore_0    // 结果5入栈

可以看到以上一共写了有8行指令,注意,以上基于栈的计算流程会涉及到一个操作数栈的问题,这里先暂且不表,后续我再给大家具体展开讲解。

而基于寄存器的计算流程却是下面这样子的。

mov eax,2   // 将eax寄存器的值设为2
add eax,3   // 将eax寄存器的值加3

可以看到以上一共只写了2行指令。

大家现在能感受到了吧!就是从所用的指令上来说,基于寄存器架构用的指令更少,而基于栈式架构用的指令更多,但是它的指令集小,究其原因,无非就是零地址指令的字节码文件是以每8位为一个基本单位来进行对齐的,与之相反的是基于寄存器架构采用的是16位双字节的对齐方式来进行对齐的。

如果以上例子还不足以说明问题的话,那么下面我会再给大家举一个例子,大家一定要知道这套JVM系列教程虽然理论会讲得比较多,但是代码也是多多少少会涉及的,就拿内存与垃圾回收篇来说,在它所囊括的每一章节中我们都会有编写代码。

而对应JVM与Java体系架构这一章节,我们则会新建一个如下Java源文件。

编写完如上Java源文件之后,记得直接运行一下就行。运行完毕之后,相信大家不难找到编译生成的字节码文件,如下图所示。

接下来,我们便要看一下以上这个字节码文件是如何来生成相应的指令了。

首先,切换到out/production/chapter01/com/meimeixia/java目录下,并对StackStruTest.class这个字节码文件进行一个反编译操作。注意,反编译操作所使用命令的是javap -v StackStruTest.class,关于这些指令后续我会专门来给大家讲解,这里大家先了解一下即可。

回车之后,相信大家就能看到StackStruTest.class这个字节码文件反编译操作之后所生成的相应指令了。

那生成的相应指令在哪呢?要想知道问题的答案,那么大家就要找一下main方法了,如下图所示,可以看到生成的相应具体指令就在main方法当中的Code这块。

仔细看上图,你会发现每一条指令的前面都有一个数字,例如0、1、2,看到这,可能有童鞋会问,它们究竟是些什么东东呢?这里我也不绕弯弯了,直接告诉大家吧!它们就是PC寄存器要识别的地址,关于这个PC寄存器,后续我再来给大家详细讲解。

不知大家看到iconst_5这样一条指令没?其中,5就是2 + 3操作之后的结果,注意,程序编译期间就会直接将2 + 3这样一个操作的结果识别成5,而不会在解释执行的时候才去计算,这是因为23都是常量。而且,这个现象还被我们称之为是编译期的常量折叠

下面,我们再把以上程序稍微变一下,将代码写成下面这样。

package com.meimeixia.java;

/**
 * @author liayun
 * @create 2022-08-19 10:43
 */
public class StackStruTest 
    public static void main(String[] args) 
        // int i = 2 + 3;
        int i = 2;
        int j = 3;
        int k = i + j;
    

代码写完以后,我们就要重新编译一下以上程序了,这里大家就不要再通过运行程序这种笨方式去编译了,按照如下操作来重新编译我觉得会更好点。

重新编译之后,那就会生成一个新的字节码文件了。

这时,我们再来对生成的新的字节码文件进行一个反编译操作,如下图所示,这便是反编译出来的指令。

对于以上指令,目前大家肯定是看不明白的,你要是能看明白,那才是见鬼了,所以这里我会给大家详细解释每一条指令的意思,希望大家能有所感悟。

stack=2, locals=4, args_size=1
   0: iconst_2     // 众所周知,2是一个常量
   1: istore_1     // 将常量2保存到1号操作数栈当中,注意,这个1其实是操作数栈的索引位置
   2: iconst_3     // 3也是一个常量
   3: istore_2     // 将常量3保存到2号操作数栈当中
   4: iload_1      // 将1号操作数栈取出,加载进来
   5: iload_2      // 将2号操作数栈取出,加载进来
   6: iadd         // 两者相加
   7: istore_3     // 相加之后的结果存储到索引为3的操作数栈当中
   8: return

从上可以看到,基于栈式架构的JVM,需要8条指令才能完成上面的变量相加操作。

而要是采用基于寄存器架构的话,那么所需的指令就非常少了,仅仅只需要2条指令即可完成同样的变量相加操作,从上面相信大家不难知道需要的是如下这2条指令。

mov eax,2   // 将eax寄存器的值设为2
add eax,3   // 将eax寄存器的值加3

接下来,我还会给大家举一个例子,当然,这是最后一个例子了,我知道大家看得很累,但我也写得很累啊,总之,大家也不要嫌我啰嗦,因为我的衷心无非就是希望大家能理解得更透彻一点而已。

先来看这样一个程序。

package com.meimeixia.java;

/**
 * @author liayun
 * @create 2022-08-19 10:43
 */
public class StackStruTest 
    public static void main(String[] args) 
        // int i = 2 + 3;
        int i = 2;
        int j = 3;
        int k = i + j;
    

    public int calc() 
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    

以上程序经过编译之后肯定是要生成一个字节码文件的,继而我们就可以对生成的字节码文件做一个反编译操作了,反编译出来的指令有很多,但是这里我们只关注calc方法对应的反编译之后的指令,如下图所示。

而对应如上指令,下面的三张图就很形象地给大家做了一个解释。

先来看第一张图,如下所示。

再来看第二张图,如下所示。

最后再来看第三张图,如下所示。

可以看到,以上三张图涉及到了操作数栈、局部变量表以及程序计数器这些内容,目前这些内容大家是还没学到的,所以对于以上三张图所描绘的内容看不懂是非常正常的现象,大家也不要过度恐慌,有个感性认识即可,至于这些内容,后续我都会给大家介绍到,这里就先暂且不表了。

最后的最后,我们再来看一下基于寄存器架构的最后一个特点,即在大部分情况下,基于寄存器架构的指令集往往都是以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主

讲完基于寄存器架构的特点之后,相信大家对其优缺点也是无比清楚了,因为它的优缺点就特别明显!

总结

由于跨平台性的设计,Java的指令都是根据栈来设计的。又由于不同平台CPU架构不同,所以指令集的架构模型又不能设计为基于寄存器的。这便是我为大家总结的第一点,无论如何,大家都一定要给我牢牢记住这一点。

接下来,我再来给大家总结一下基于栈的指令集架构和基于寄存器的指令集架构这两种架构的优缺点。

先来看一下基于栈的指令集架构的优缺点。

  • 优点:
    • 跨平台性。

      之所以要考虑跨平台这一特性,是因为我们希望Java程序要能在任何的平台上都可以被执行。

    • 设计和实现更简单,适用于资源受限的系统。

    • 指令集更小,编译器容易实现。

  • 缺点:
    • 性能下降。

      任何事物都有利有弊,相较于基于寄存器的指令集架构而言,它的执行性能要稍微差一些。

    • 实现同样的功能需要更多的指令。

      相较于基于寄存器的指令集架构而言,同样一项操作,它所需要的指令更多。

然后再来看一下基于寄存器的指令集架构的优缺点。

  • 优点:
    • 性能优秀和执行更高效。
  • 缺点:
    • 指令集架构完全依赖硬件,可移植性差。

一个有趣的问题

时至今日,尽管嵌入式平台已经不是Java程序的主流运行平台了(准确来说应该是HotSpot虚拟机的宿主环境已经不局限于嵌入式平台了),那么为什么不将架构更换为基于寄存器的架构呢?

大家好好想一想以上这个问题,看是否能找到该问题的答案,这里我给大家提示一下,大家不妨从基于栈式架构的优点出发去寻找答案,你找到了吗?要是你真找不到,那就看我下面的讲述吧!

首先,大家应该知道,基于栈式的架构在设计和实现上要更简单一些,因为栈无非也就是一个入栈/出栈的操作,你要执行哪个方法,哪个方法就以栈帧为单位加载到栈当中,注意,栈帧是栈当中的一个结构,一个栈帧就代表一个方法。至于最上面的栈顶元素,那就是当前要被执行的方法了,方法执行完以后肯定就是要出栈了。所以,从这个角度看,基于栈式的架构在设计和实现上是要更简单一些的。

其次,虽然基于栈式的架构适用于资源受限的场景,例如嵌入式平台我们就可以认为是一个资源受限的场景(平台),但是在非资源受限的场景下基于栈式的架构也是可以用的,这完全没有问题。所以,从这个角度看,我们也没有必要再去将架构做一个更换了。

以上我就异常详细地给大家介绍了一下基于栈的指令集架构和基于寄存器的指令集架构这两种架构方式,并且还对它俩做了一个对比,希望大家能从中找到自己需要的东东!

以上是关于节:区分栈的指令集架构和寄存器的指令集架构的主要内容,如果未能解决你的问题,请参考以下文章

节:区分栈的指令集架构和寄存器的指令集架构

基于栈的指令集与基于寄存器的指令集

JVM基础认知篇(下)

STM32学习-嵌入式微处理器指令集架构

虚拟机栈

基于栈的指令集与基于寄存器的指令集的区别