0%

《自己动手写CPU》读后小结

本书的前11章是教学版的OpenMIPS CPU设计过程,后面的章节是实践版的设计和实物的实验。对于本书的名字《自己动手写CPU》来说,我认为前12章就已经完成了任务。后面部分的实验需要DE2开发板的支持,所以暂时做不了。

当初看这本书的目的是进一步了解计算机组成与体系结构,因为上学期上了一门《计算机组成与体系结构》的课,感觉学得很模糊,于是想着干脆通过自己写一个CPU来增强一下对计算机的理解。网上推荐的书有《自己动手写CPU》和《CPU自制入门》,比较了一下感觉前者更适合我,便开始着手看了。

看完这本书的首要感想是,原来CPU并没有那么神秘!

在这里我想讲一下两方面的收获:

  1. CPU的结构与设计
  2. 计算机组成与体系结构

首先说一说我之前对CPU的一些好奇和疑惑:

以前的我只知道CPU是按照时钟的频率的工作的,但是不知道为什么要由时钟来控制,以及如何控制,同时也会想,既然是按照时钟来工作,那为什么不“无限制”提高频率以加快CPU的处理速度呢?

看完本书后,我知道了这些问题的答案。

首先,为什么要有时钟?是为了保证CPU内部的正常秩序,“到了什么时间就做什么事”,没有一个良好的秩序,一个团队的效率是不可能高的,甚至乱作一团,导致不能工作。

那么,时钟是如何控制(组织)CPU内部的工作的呢?时钟会产生一个周期信号,我们可以规定在信号的每一个上升沿or下降沿开始执行一个操作,CPU中的不同模块能同时进行操作,但CPU中的一个模块在同一个时刻只能做一件事,这样就能保证各个模块之间不冲突,各个功能正常执行。

那么是不是一直提高时钟的频率,就能一直无限制地提高CPU的执行速度呢?显然是不可能的,如果真是这样的话那么为啥CPU厂商自己不大幅提高时钟频率呢。第一个事实是由于CPU电路中的信号是有延迟的,一个操作的结果不可能立刻反映在输出电路上,如果无限制提高时钟频率,那么会导致“运算”跟不上时钟,从而导致CPU内部的秩序又消失了,变得混乱。第二就是由于电路中电子的运动是要消耗能量的,执行地越快,单位时间内消耗的能量就越多,发热量就越大,散热就会跟不上,从而导致内部电路“失效”,电脑蓝屏、死机or自动关机。

By the way,时钟频率并不是唯一决定处理器性能的东西,这也就是为什么AMD的主频一般没有同级别的Intel家的主频高,但是性能却不相上下甚至超过Intel的原因(AMD YES!)。

CPU的结构及设计

CPU的本质就是一个只会“解释指令”,并且死板地执行指令的莫得感情的机器,有一个比喻说得好:硬件是计算机的躯体,OS才是灵魂。它的主要结构(或者说最基本的结构)包括译码器、ALU以及各种寄存器。

我们会给具体指令集中的指令规定固定的格式,以便让译码器理解代码,其实原理很简单,CPU译码的过程相当于就是查字典,在规定的有限的指令集内找到与输入的机器码相匹配的情况,并执行相应的操作。

ALU(Arithmetic Logic Unit)的中文名是算术逻辑单元,顾名思义,主要是执行算术/逻辑操作的单元,计算机的各种“计算”都是在这里完成的。

寄存器是用来保存运算的输入数据、中间结果and结果的存储单元,其位于CPU内部,访问速度最快(在计算机体系中)。但也有一些特殊的寄存器并不用于计算,比如PC(Program Counter)程序计数器,它的值是下一个将要取到的指令的地址。

前面说过CPU中的各个模块按照时钟来运行的,为了进一步提高效率,人们使用了流水线结构,类似于工厂众多的流水线,将一条指令(商品)的执行过程分为多个步骤(或阶段),每一个步骤由一个专门的模块来负责,这个模块的任务就是接收从上一个阶段传来数据,经过它自己的处理后,再传递给下一个模块。

本书的Open MIPS CPU采用的是五级流水线,将一条指令分为五个步骤:取指、译码、执行、访存(RAM)、回写(寄存器)。具体实现流水线的方式就是通过组合电路和时序电路的组合级联而,由组合电路完成具体的操作,由时序电路完成按照时钟传递数据的任务,以保证指令的有序执行。

同时,由于采用了流水线结构,也会相应地带来相邻指令间的相关问题(具体见笔记第五、八、九章),简单来说就是:假如一条指令的结果是在最后一个流水线阶段才写入的,此时流水线的前几个阶段已经在处理后面几条指令了,如果这几条指令需要的结果正好是该指令将要写入的(但是还没写入),那么就会产生相关问题。对应的解决方案在笔记中也介绍过,简单来说有:数据前推、暂停流水线、编译器调度(不属于CPU的能力范围)。其中数据前推就是在流水线中新增一些从流水线后面几个阶段到前面几个阶段的数据反馈回路,然流水线的前几个阶段提前获取前面指令的结果。暂停流水线是通过新增ctrl模块实现的。

在运行过程中,CPU难免会遇到一些出错的情况(例如:溢出,除数是0等),这就需要一个异常处理机制了。为了实现精确异常,采用的策略是在前面几个流水线阶段检测异常但不立即操作,在一个固定的流水线阶段统一请求异常处理。

至于协处理器,就是辅助CPU的处理器,其中包含一些特殊寄存器,辅助CPU的判断/异常处理,有的协处理器也能帮助CPU完成一部分计算工作(例如:浮点运算协处理器)。

计算机组成与体系结构

什么是计算机体系结构?那本黑皮书《计算机组成与体系结构(性能设计)》中定义为

计算机体系结构是程序员所看到的计算机的属性,即概念性结构与功能特性

对于不同层次的程序员来说,他们看到的计算机的属性也不相同。例如,对现在的许多高级语言程序员来说,他们看到的计算机的属性大多数是操作系统(OS)提供的,而对于OS程序员来说,他们看到的体系结构就是ISA(或许还有其他我目前不知道的…)提供的。

至于计算机组成,就是由五个经典的部分组成:输入、输出、存储器、数据通路(运算器)、控制器。后面两个部分组成CPU,通过本书的前11章,我们也实现了这两个部分。

首先,任何一个ISA都会提供给底层程序员几个类别的指令,本书实现的Open MIPS就包括了:逻辑指令、位移指令、(数据)移动指令(寄存器之间相互移动)、算术指令、转移指令、加载存储指令(寄存器与ram和rom之间移动)、协处理器访问指令、异常相关指令。ISA所提供的指令类别需要保证通过不同指令的组合就能完成任何程序的任何逻辑,现在同一个类(RISC或CISC)中的不同ISA的指令类别应该大同小异了。

具体的指令设计可是一个大学问,这需要考虑到指令有哪些格式,什么样功能的指令是必要的,什么是冗余的,如何设计执行过程的方法才能提升指令的执行效率。当然,这些都是那些专门设计指令集的大佬们干的事,本书实现的Open MIPS仅仅是把现有的开源指令集拿来实现(并且还是简化版的)。

不同的指令执行所需要的时间也不一定一样,在Open MIPS中,大部分指令执行阶段都只需要一个时钟周期,少部分需要两到三个,最花时间的是除法指令,它需要至少32个时钟周期,这是由除法本身的算法(试商法)决定的。

实现同一个功能可以由多种不同的指令组合完成,如何提高程序的执行效率,也就可以通过寻找所需最短时间的指令组合来实现,这个过程是由编译器(无论是高级语言编译器还是汇编器)实现的。实现这个我想并不容易,否则编译原理也就不会是CS系学生的噩梦了(顺便膜拜一波写编译器的大佬们)。同时这也给我们编写高级语言程序的时候一些启示:了解一些高级语言语句的汇编实现是有用的,因为这可以让我们判断哪一种实现方法更有效率(所需的时钟周期更少)/汇编语句更少(节省内存空间),这是让我们编写出更好的程序的一种技巧(个人观点)。举个栗子,调用函数操作的汇编语言实现一般都是跳转指令配合标签,同时会用到栈(将调用前的寄存器值压入栈),如果使用递归的话,就会多次调用函数,多次将寄存器的值压入栈,如果递归深度太深,那么栈就可能溢出,导致程序错误,同时相比非递归函数,这个压入栈的操作会被执行很多次,就浪费了时间,同时也浪费了空间。这也是为什么一般不轻易采用递归结构的原因,当然,对于现代的CPU/PC机来说,我们平时写的那些递归的深度及操作复杂度都不是太大的问题(因为执行速度够快而浪费的时间可以接受,因内存够大而导致栈空间的浪费可以接受,不过这个栈的大小是有限度的,因而也可能溢出,但也可以手动设置栈的大小)。

在《计算机组成与体系结构(性能设计)》中,还讲到过指令发射策略,本质上是一种改变指令执行顺序来提高执行效率的过程(前提是程序逻辑不变),不过这里我看地有些似懂非懂……也就不详细说了。

为了方便CPU与外部设备的交互,我们采用了总线的结构,具体原因在十二章的笔记中有说过,进一步总结就是,为了减少不同功能模块之间的耦合,因此通过一个中间模块(在这里就是总线)来使需要连接的模块的接口标准化,使模块之间的互联逻辑变得清晰,易于维护修改。

最后

总的来说,这本书所实现的CPU很简单,内容也不是很多(就是代码多了点),与当今时代个人PC机的CPU差别挺大的,有许多高级设计并没有使用到(这也是为了降低难度),例如:超标量流水线、指令发射、指令预测等等。对这些感兴趣的话可以看看上面提到的那本黑皮书,不过说实话。。。中文翻译得确实不是很流畅,有些地方不慢慢地多读多想不易理解(当然,这也与知识本身的难度有关)。

话说回来,看这本书的收获也挺多,可以算是入了CPU和计算机硬件的门,我想,如果是先看完本书之后再去看那本黑皮书或者《计算机组成与设计(硬件/软件接口)》(该书正好就是用MIPS指令集来做例子的)也是极好的,能帮助理解其中的许多概念(流水线、延迟槽、相关性问题、异常处理等等),也不会像我当初上课时那样摸不着头脑了。

PS:建议看本书之前最好有数电基础。