漫谈指令集与CPU(三)

150 阅读6分钟

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


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


指令设计

在正式设计之前,聪明的你准备梳理一下需求,将目的明确为8位数据的以下的一些运算:

  • 加:两个运算数一个结果
  • 减:两个运算数一个结果
  • 乘:两个运算数一个结果
  • 除:两个运算数一个结果
  • 与:两个运算数一个结果
  • 或:两个运算数一个结果
  • 非:一个运算数一个结果。

同时,需求也从加法器(add)变成了算逻单元(alu)。在确定了需求以后,聪明的你决定用以下的方式来表示命令:

运算命令
0000
0001
0010
0011
0100
0101
0110

之所以使用四位来表示命令,是因为聪明的你考虑到了未来可能会有新的命令需要加入,提前留出了空间。

所以对于类似加法的指令就变成了:

0000-00000000-00000001   // 1+1

那么自然而然的,类似非运算就变成了:

0110-00000000   // ~1

Verilog描述

那么落实到Verilog上,可以用如下的方式来进行描述:

module  alu(out, opcode, a, b);
        output[7:0]      out;  //结果
        input[3:0]       opcode; //命令
        input[7:0]       a, b;  //运算数
        always@(opcode or a or b) begin
              case(opcode)
                     4'b0000:       out = a + b;
                     ……  //此处省略类似加的二元运算
                     4'b0110:       out = ~a;
              endcase
        end
endmodule

从代码上看,我们可以看到,通过一个case语句来判断了opcode的类型,然后进行相关的运算再进行输出。

如果从电路图上看,大致是这样的:

image.png

如果我们尝试去窥探alu内部的实现,我们需要比较多的数电的知识。首先数电并不存在加这类的运算,都是通过与、或、非这些运算来实现的,例如我们看一个加法器的电路图:

image.png

可以看到这里都是由一些与或非门来实现的,我们的alu也是通过这种组合电路的方式实现的。

那么一个很自然的问题就会出现,随着我们的指令的变多,我们的电路必然会越来越复杂,不论是为了进行命令的判断,还是为了执行命令,都需要更多的电路做支撑。

假设我们需要一条特别专用的指令,我们也可以用命令加数据的方式实现,例如我们可以在这里加一个计算函数x+1的命令,命名为addOne,opcode为0111,单输入单输出。

CISC

通过不断地叠加新的命令和电路的方式来拓展指令,是传统的CISC(Complex Instruction Set Computer:复杂指令集)的做法,通过硬件的方式来实现指令,即使会出现像我们前面提到的专用指令的情况。我们经常说的x86就属于CISC。

那么随着指令的发展,电路就会越来越复杂。聪明的你想到虽然在学习代码的过程中学习了很多的特性和关键词,但是只有一些会被经常使用,例如:for、while等。

是的,这就是经常说的马太效应:可能有百分之20的指令在百分之80的时间里被使用,而剩下的百分之80的指令只在百分之20的时间里使用。为了百分之20的指令而让架构不断的臃肿,这显然是一种巨大的浪费。

此外,随着指令的变多,硬件也会越来越复杂,需要的成本就越高,于是聪明的你在想了有没有其他的指令设计方式呢?

CISC?RISC?

在数电里我们说,其实大部分的电路也是通过逻辑电路的组合实现的,比如我们的加法,其实也是由很多的与或非运算来实现,那么我们是不是可以把加法指令转变成这些运算呢?

那么我们可以怎么实现这样的转变呢?

一种方式是通过微指令的方式来实现,当我们的计算机获取到一条指令时,在硬件上对这个指令进行解码并转换成微指令,再由计算机去运行这些微指令。

另一种方式则是通过指令的方式来实现,在编译的时候就直接把比较复杂的指令转换成多条简单的指令,再传入到计算机中进行运行。

前者是CISC的做法,而后者则是RISC(Reduced Instruction Set Computers:精简指令集)的做法,我们经常听到的ARM、Mips都从属于这一阵营。

从指令的角度来看,RISC指令集是一种很学院派的指令集,它是由很少的一系列定长的指令组成,通过指令的组合来实现复杂的操作,这就使得RISC指令集编译后的长度普遍偏长。但是因为RISC在编译的时候就直接将指令变成了多条简单指令,在运行的时候并不需要去做一个解码的工作,所以运算速度会快一些。但是RISC仍然在很长的一段时间里面临着商业上的失败。

然而随着时间的推移,两种指令集也互相学习,不断地进行更新,这是后话。

偏长的指令集?

那么为什么RISC指令集编译后的长度普遍偏长呢?聪明的你陷入了沉思……

后记

今天的文章只是对RISC和CISC做了一些简单的阐述,如果想了解更多的同学可以自行检索相关资料。未来我们将会更多的基于RISC做讲述,在整体的架构讲完以后,会加入一些微架构的知识。

从整理的讲述思路上,个人希望能够以设计者的角度来讲述,但是因为能力问题,在整体的讲述过程中还是有些磕磕绊绊,希望大家可以多提一些建议。

我们明天再见!