漫谈指令集与CPU(四)

166 阅读5分钟

这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战


大家好,我是程栩,一个即将入职鹅厂的校招新人。本系列文章将会以一个演进的视角介绍计算机组成原理,并以一个采取Mips指令集的CPU作为范例。


指令长度

为什么RISC的指令长度会长于CISC呢?

首先我们要考虑到RISC的指令是定长的,例如所有的指令都是32位,其中假设有10位是数据位,其他的位数用来表示是什么命令。基于我们前面考虑到的多运算数的情况,RISC为了保证指令的定长,总是会以多运算数为基准。例如我们现在最多允许两个运算数,那么10个数据位可以分成两个5位的数据,如下图:

image.png

中间的两个五位数是作为数据位而存在,那么无论这两个位是否被使用,这10位的空间总是存在的。

反观CISC,因为允许变长的存在,所以如果有些变量不需要的话,大可以将其丢弃,那么这样整体的码长就变短了。

同时,在设计指令码的时候,我们可以引入权重的概念,聪明的你一定会想到哈夫曼编码。是的,我们可以根据指令命令码的出现频率构建一棵带权重的哈夫曼树,从而实现出现频率高的操作码长相对较短,而频率低的操作码码长相对较长,这样也可以降低整体的码长。

指令与硬件

在设计指令的过程中,聪明的你意识到了很多问题,譬如我们的alu只能够进行8位二进制的运算,不能保存数据,也不知道指令的来源,这和我们生活中的计算机相差甚远。于是你决定为这个alu添加更多的部件让它变成更强大。

作为软硬件的接口,指令集的变化和硬件的变化是一致的,所以随着硬件的变化,指令集也需要有相应的变化。

为了能够更好地设计,我们在这里假设这是一个32位的机器,无论是指令的长度还是数据的长度。

指令存储顺序执行

聪明的你首先想到了冯诺依曼结构中的指令存储与顺序执行的想法,那么指令该存在哪里呢?

当然是给它找个地方存储啦。比如像下面这样的地方:

image.png

我们给这样一个存储指令的地方命名为指令存储器(IM:Instruction Memory)

那么自然而然的,为了能够做到顺序执行,我们需要有一个指向地址的门牌,这样才能够一条条的获取到指令。我们将这样的数据存储在一个专门的模块里,每次执行拿去以后就根据运行的情况跳转到下一条指令。我们将它命名为程序计数器(PC:Program Counter)。如下图:

image.png

如图所示,PC根据上次的指令计算出下一次的指令地址,然后将这个地址传输给我们的IM,在IM里获取到对应地址的数据,将这个数据再传出去,交给计算机进行运算。

问题来了:下一条指令是存储上的下一条指令吗?

我们不妨想想平常写的代码,难道都是一行一行顺序运行的吗?当然不是这样的,我们总是会有if、for这样的语句来控制语句的执行,所以其实我们的PC并不仅仅受到上一条指令的地址影响,还需要考虑到当前运行的指令,如下图所示:

image.png

数据存储

我们写的程序,总是会存储很多的数据,比如如下的代码:

a = 1;
b = 2;
a = a + b;

如果我们不能存储a和b的值的话,第三句话就无法正确的运行,所以我们自然需要一个地方来存储数据。我们模仿IM的设计,设计一个数据存储器(DM:data memory),在这里面存储的是数据。如下图:

image.png

需要注意的是,我们在前面设计的数据长度也是32位,所以自然这里的位宽也是32位,而里面的数字而可以看成是存储的二进制的十进制的值。

命令

现在我们加入了三个模块,可以分别进行指令存储、数据存储、指令获取。那么这样的计算器可以运行了吗?

我们不妨用以下的代码尝试一下:

#include<stdio.h>
int main(){
    int a = 0;
    if (a == 1){
        return 1;
    }
    return 0
}

似乎出现问题了,我们不能正确的处理if这样的跳转语句。因为我们根本没有规定这样的指令。

而同时我们想到前面我们的PC不止受到上一条指令影响,所以我们应该有更多的指令,同时似乎应该有更多的部件。一方面我们需要能够解析更多的指令,因为我们的alu并不需要应该处理这些指令;另一方面,我们针对指令做更多的分类,不止是运算指令,还应该有像跳转这样的指令。

那么我们该怎么做呢?


后记

事实上,昨天是写的最没有感觉的一天,今天没有被推荐上首页也是一种证明。从今天开始,会尝试更多的以具体的例子去进行讲解,希望能和大家一起进步。

我们明天再见!