1.1.3 CPU是如何执行指令的
接下来,让大家通过一个最基本的例子管窥一下CPU大致的工作流程:
1+2=3
1.准备工作
加数1和2分别存储于内存的两个“抽屉”中,预期把相加的结果存入紧挨着它们的“抽屉”。图1.2为包含16个“抽屉”的内存示例,假设加数分别存储于8号和9号“抽屉”,10号“抽屉”用于存储结果,没有用到的“抽屉”中内容默认为0(图中标为浅灰色)。
图1.2 数据在内存中的存储示例
整个运算过程需要用到如表1.2所示的4种指令。
表1.2 示例所需指令列表
此外,人们还需要两个数据寄存器(Data Register,DR)用于暂存两个加数,其中一个DR还将用于暂存结果,这种设计可大大节省寄存器的开支。不妨将这两个寄存器称为DRA和DRB,地址分别编码为01和10。
至此,可以列出实现相加运算的所有步骤,也就是完整的程序了,如表1.3所示。
表1.3 示例程序[2]
[2] 用M(x)表示第x号内存“抽屉”。
现在,将这段包含5条指令的程序放在内存的0~4号“抽屉”,如图1.3所示。由于地址码的长度和数量不一,指令码显得参差不齐,因此对不足8位的指令码进行补零。
2.过程解析
这段程序的运行由控制器和运算器协作完成,前者“对付”指令,后者“专攻”运算。除了前面提到的DR,它们还需用到几个特殊的寄存器,才能顺利开展工作。
1)程序计数器(Program Counter,PC),保存当前需要执行的指令所在的内存地址,默认情况下,每当从内存中取出这条指令,PC就进行自增,更新为下一条指令的内存地址。
图1.3 指令在内存中的存储示例
2)指令寄存器(Instruction Register,IR),保存从内存中获取的指令。
3)内存地址寄存器(Memory Address Register,MAR),保存CPU需要读写的内存地址。
4)内存数据存储器(Memory Data Register,MDR),保存从内存获取的数据或准备写入内存的数据。
5)程序状态字(Program Status Word,PSW)寄存器,保存一系列“是否”信息,用于标识CPU控制和运算过程中的各种状态,如是否允许中断(允许打断当前任务),运算结果是否为零、是否为负值、是否溢出(值太大存不下)等。
控制器、运算器和这些寄存器之间的数据流关系大致如图1.4所示,初看有点不明所以,没关系,在看完程序的整个运行过程后就一目了然了。
图1.4 数据流关系
大体上,可以把指令的处理过程切分为获取、解析和执行3个阶段。下面从第一条指令LOAD DRA M(8)开始,分析CPU在每个阶段的工作。
指令获取阶段:将指令从内存中读取至IR。如图1.5所示,PC的初始值为0000 0000,该值被传入MAR,控制器根据MAR将0号“抽屉”中的指令0001 1000取出,存入MDR,顺势再存入IR,PC自增,更新为0000 0001。
图1.5 LOAD DRA M(8)取指令阶段数据流图与流程图
指令解析阶段:识别操作码和地址码。本例中,操作码只有两位,因此控制器将识别IR中的前两比特。不同的操作码将激活不同的电路完成下一阶段的工作,这一选择可以用与门和非门的简单组合来实现,如图1.6所示。
地址码除了数量不定,种类也很多,它既可以是操作数,也可以是寄存器地址或内存地址,而在这个地址所指的位置,可能存放着操作数,也可能存放着另一个地址。控制器需要把这些不同的地址码区分开。
图1.6 操作码解析电路
指令执行阶段:由运算器进行运算,必要时读写内存。如图1.7所示,控制器将地址码1000传入MAR,而后根据MAR将8号“抽屉”中的操作数0000 0001取出,存入MDR,顺势再存入DRA。
至此,第一条指令执行完毕,DRA获取到操作后的值。以同样的过程执行第二条指令LOAD DRB M(9),DRB也可以获取到操作后的值。
图1.7 LOAD DRA M(8)执行指令阶段数据流图与流程图
由于针对每条指令,CPU在前两个阶段总是做着同样的事情,到第三阶段才开始做出不同处理,因此下面省去了一些重复描述,仅讨论指令的后续执行阶段。
到了第三条指令ADD DRA DRB,如图1.8所示,控制器发现需要运算,于是激活运算器,把DR中的操作数传给它。由于运算器可以完成各种算术和逻辑运算,控制器还需要指定运算类型——加法。运算器在完成运算后将结果交还给控制器,控制器转而存入DRA。此外,运算器还将更新PSW,由于此次运算结果不为负值,不为零,也没有溢出,因此将PSW中相应的3比特置0。至于是哪3比特,暂且不用关注。
图1.8 ADD DRA DRB指令执行阶段数据流图
第四条指令STORE DRA M(10)的执行过程是LOAD指令的反演,如图1.9所示,控制器将地址码1010传入MAR,将DRA中的待写数据传入MDR,而后根据MAR将MDR中的数据存入10号“抽屉”。
第五条指令HALT仅告知控制器一段程序终结了,毕竟内存是连续的,如果没有HALT,CPU将尝试处理毫无意义的0000 0000或乱码,后果不堪设想。
3.更高级的程序控制
上例中的程序是相对简单的顺序结构,每加载一条指令,PC总是自动更新为下一个“抽屉”的地址。其实程序往往是复杂的,其运行很少会像乘坐地铁或火车那样顺序地从第一站走到最后一站,而是更像走一个包含分支、循环等岔路的迷宫。PC的值时不时就会被跳转指令改写。
图1.9 STORE DRA M(10)执行指令阶段数据流图与流程图
于是,CPU可能需要跳过若干指令而直接执行它们之后的指令,或者返回到前面已经执行过的某条指令,再执行一遍。当然也可能在判断PSW中的某个状态后决定不跳转,又或者临时进入另一个任务,执行完后再返回,这就需要CPU用寄存器记住跳转前的位置。几种常见的程序流程如图1.10所示。
图1.10 几种常见的程序流程
这样看来,CPU除了“埋头苦干”,还得“眼观六路,耳听八方”。
程序控制中还有值得一提的概念——中断,是否允许中断由专门的中断开关指令决定。想象一位领导,正在办公室赶着一份明天要用的报告,却时不时被秘书急促的敲门声给打断,这次是某个员工转岗要签字,下次是某个工会活动要审批。CPU就像这位领导,工作很忙,但使用者还是喜欢时不时地打断它,比如点一下鼠标,敲一下键盘,这些操作都会向CPU发送一个中断信号,CPU收到中断信号后往往不得不停下手中的“活”,优先处理这些事件,要是稍有怠慢,使用者会觉得“很卡”。而当PSW中相应的中断标志呈现禁用状态时,便相当于在领导办公室的门上挂出了“勿扰”的牌子,此时CPU便可以专心致志地做完手头的工作了。
4.指令的流水线技术
经过上面的描述,大家已经对CPU的运作过程有了最基本的了解,它按部就班地读取着指令,并依次解析、执行它们。其整个过程井然有序,足以应对人类交托的所有任务。唯一美中不足的是,资源没有得到充分利用,速度还有提升空间。当大家将CPU中的电路切分为3个模块,它们分别负责指令的获取、解析和执行,就会发现,任意时刻都只有一个模块在工作,而另两个模块在等待着。其效率非常糟糕。假设指令处理的3个阶段耗时相等,都为1个单位时间,那么单条指令就需要3个单位时间去完成,如图1.11所示。
图1.11 非流水线指令序列处理过程
这就好比某家装配厂雇佣了3名员工,分别负责组装、测试和包装,老板期待着他们高效协同,结果却总有两个人闲着。大家都知道现实中的工厂是不可能这么做的,而是采用流水线(Pipelining)的模式让每个员工都忙起来。CPU的设计者也意识到了这一点:当解析模块获取到指令之后,获取模块完全没有必要等着,它可以马上去取下一条指令;当执行模块开工之后,解析模块可以解析下一条指令,而获取模块可以去取下一条指令,如图1.12所示。这样一来,平均每条指令的处理就只需要1个单位时间,效率是之前的3倍。
图1.12 流水线指令序列处理过程
事实上,上面对指令处理的阶段划分过于粗糙,取指令的一次内存访问需要消耗不少时间,而指令的解析可以很快完成,到了执行阶段,不同任务的耗时更是千差万别。3个阶段耗时不等,出于木桶效应,上述的单位时间要取它们中最长的那一个,最终,相互等待的情况还是十分严重。为此,CPU的设计者们将指令的处理切分成更多小阶段,形成了超流水线(Superpipelining)技术,如图1.13所示。
图1.13 超流水线指令序列处理过程
这些小阶段的个数被称为流水线的级数,英特尔在1978年推出的8086和8088处理器只有2级,随后不断增加级数,到2004年的Pentium 4处理器已有31级。但由于流水线的实现需要在级与级之间增设临时存放阶段处理结果的寄存器,级数过深后,这些额外的存取操作反而会拖累CPU的整体效率。计算机的设计真是一种权衡的艺术,意识到问题的英特尔很快对产品进行“降级”,把之后的处理器流水线级数稳定在14~20。
超流水线技术对应到工厂中,相当于雇佣了更多员工,每个员工做更简单的事,但重复的次数增加了,整体产能也上去了。但工厂的老板可能还是不满意,他准备复制现有模式,增加流水线,使产能进一步翻番。这就是CPU中的超标量(Superscalar)技术。通过增加相同的处理电路,可以实现2条,乃至多条指令的同时处理,如图1.14所示。
图1.14 超标量流水线指令序列处理过程