漫谈处理器体系结构

来源:互联网 发布:淘宝数据分析表 编辑:程序博客网 时间:2024/05/18 01:25

漫谈处理器体系结构

前言:

这篇博客本应该是《深入理解计算机体系结构》(第二版)中第一部分第4章处理器体系结构的读后感,但是感觉这样的名字有点low,因为毕竟加入了自己的理解和总结。

ISA(Instruction-Set Architecture)

几乎所有讲体系结构的书都会讲到这个指令集。指令集确实应该是最先说明的问题。一句话概括起来指令集就是说CPU能干什么事。基本常用的指令集包括:传送指令、算术逻辑指令、跳转指令等。是体系结构需要实现的功能。指令集对人类来说是友好的,可阅读的。但对于只认识01的机器来说,还是需要对其进行编码的。编码的方式有定长编码和可扩展编码,现代处理器使用的基本上都是可扩展编码。编码方式上来说我觉得有一点值得考虑一下的就是Huffman编码,它是一种前缀编码,不可能有一个编码是另一个编码的前缀。Huffman编码可以实现减少编码的长度,节省存储空间。具体的实现可以参见1,上面的解释和代码都很详细。

硬件基础

当然,我是个写软件的,肯定不会去深入的讲解物理实现去(我也不会)。从一个程序猿的角度看,体系结构采用的肯定是时序电路。但是组合电路是时序电路的基础,与布尔代数息息相关。我理解的组合逻辑就是给定输入,就可以给定输出。加法器就是组合逻辑,输入两个值就会得到和。时序电路就是在组合逻辑的基础上加上时钟信号。什么是时钟信号?怎么加?相信大家都应该听说过石英表,通过石英的周期震荡来获得时钟信号我们就有了时间的概念。时钟对于同步来说是非常重要的,说白了同步就是什么时间该干什么事,不要乱了。就像人类社会一样,现代社会的繁荣就是因为时间积累下来了知识。同样,计算机这么智能的东西有了时钟的概念之后,自然是要把事情分为很多步骤(即你写的一条条指令)。组合逻辑就是实现其中的一步,时序逻辑就是实现好几步。那么这好几步需要做的自然而然就是要存储上一步的结果。这就是时钟寄存器的概念。时钟寄存器用来存储组合逻辑的结果,也成为划分各个操作的分界点,因为它在每次时钟改变(处于上升沿)的时候才会更新寄存器的存储值。除了时序电路和中间结果的寄存,基于冯诺依曼体系结构的中心就是存储,存储结构包括大家熟悉的主存、cache和寄存器。
对,概念计算机主要就是这些部件,一个时钟(用于同步计时),总线(用于通信),算术逻辑单元(ALU,组合电路),寄存器(Cache,主存等用于存储),时钟寄存器(用于传递逻辑电路结果)。你可能会说大学课本里面有一个很重要的控制单元(CU),它去哪了,CU实际上是上述部件的一些组合,就是下面我们要重点说的处理器实现。

处理器顺序实现

书中是从处理器的顺序实现开始讲的,也确实这样很容易理解。顺序实现就是没有流水线的实现,一条指令一个周期完成。虽然书上面讲一条指令的执行分为了很多阶段,但是在这里我觉得如果用很多模块来讲更加的清晰。区别就是阶段代表了时间,感觉顺序实现就是一个噱头,还是流水线的概念;模块代表了一条指令执行的不同部分。
一条指令就用一个组合逻辑实现了。这里我觉得有两个重要的问题需要说明:

指令集有很多的操作,岂不高射炮打了蚊子

这里的理解很正确,指令集有很多操作,需要很多不同的硬件实现,每次我们只能根据指令编码选择(可以使用经典的选择器逻辑电路实现)其中之一使用,很多时间部件基本都是空闲的。这就是流水线化的初衷,让所有的部件尽可能的忙起来,提高资源的使用率。

两条指令中间的结果怎么传递

前面已经说过了,当然是使用时钟寄存器了。常用的时钟寄存器有两个PC,CC。程序计数器(PC)是时钟寄存器是无可厚非的,因为下次执行的地址应该是上一条执行地址的下一个指令(对于顺序程序结构来说),但是这里条件码寄存器(CC)为什么也会成为时钟寄存器呢?答案在书中已经解释的很清楚了。
处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态。
这是处理器很重要的一条规则,熟悉汇编语言的同学都知道,在汇编语言里面,条件判断是分为两步执行的。第一步进行计算,第二步取值(条件码的值)。而且对于我当前的语句来说,我有可能用到上一次的条件码的值,所以我当次是不能改变条件码的,只有等到下一条指令执行的时候(下一个时钟周期)才能对CC的值进行改变。

具体的实现可以从书中看到,就不在详述了。

处理器流水线实现

前面已经做了很多铺垫,由于资源的利用率太低,所以说需要采用流水线的方式来提高资源利用率。流水线的概念大家应该都了解,不了解可以从书中看,它可以提高系统的吞吐率。但是流水线也不是没有不好的地方,就是它会增加单条指令的运行时间。为什么呢?因为在把指令的执行划分为多个段之后,需要增加时钟寄存器来存储中间的状态,这些状态的存储更新过程是需要花费时间的。但是对于大规模的系统处理来说,当然还是用流水线处理比较方便了,小作坊式的生产只能应对很小的产量。

流水线段划分

流水线在设计的时候肯定是需要分段的,那么划分的时候有两个原则:
- 划分的段所用时间尽量相同或相近。因为流水线运行周期是由最慢阶段的延迟限制的,如果段有长有短,自然就会浪费很多时间。
- 段划分不能太细,因为中间的时钟寄存器需要花费一定的时间,这实际上为指令的执行增加了时间。如果我们划分的太细,甚至增加的时间比原来消耗的时间都多,那就得不偿失了。
上面两条原则实际上是在告诉我们段划分既不能太大也不能太小(太大肯定不行,我们划分流水线就是为了变小,不然直接用顺序好了)。常用的分段方式就是把流水线划分为:取指、译码、执行和访存四个阶段。那这里自然而然有个问题,就是执行阶段有很多非常复杂的操作,比如说浮点数操作,可能大概需要二十几个时钟周期的操作,这样就拖慢了每一条指令的执行周期,流水线的段划分就会收到很大的影响。针对这种
多周期指令的特殊问题,我们通常采用以下两种方法解决:
- 最简单的实现方法自然就是在执行阶段停留几个周期,但是这样做肯定就会导致很多的空操作。但是这个实现简单,当然效率就不高了。
- 现在普遍采用的方法就是采用一个独立于流水线的特殊硬件单元来处理较为复杂的操作,这样性能会更好一些。比如专门浮点运算处理单元。将特殊指令发射到特殊的硬件处理单元,这个单独的硬件处理单元也可以采用流水线化的处理方式进行处理,但是一般来说这个特殊的硬件处理单元会有自己的时钟周期。原来的流水线继续执行流水线指令,两个硬件并发执行。(当然如果存在数据相关,后面的指令就可能得等待当前的指令执行结束了)

虽然我们把处理器做成了流水线的方式,但是经过这种方式产生的指令执行方式还得是要跟顺序处理器的执行结果要一致。这就需要解决流水线带来的问题:流水线冒险。

流水线冒险

流水线冒险就是相邻的指令之间存在相关依赖性。流水线冒险包括1.数据相关,下一条指令用到该条指令的结果。2.控制相关,该条结果确定下一条指令的位置(跳转指令)。

用暂停的方式处理数据相关

这种方法就是暂停流水线,结果跟顺序结果一样。(产生气泡操作bubble,跟nop空操作一样)

用转发来避免数据冒险

当前指令的执行结果应该在指令执行结束之后才能写回到寄存器或者存储器中,但是如果流水线的下一条指令就用到了该结果,这里采用转发或者叫做数据旁路技术把当前的指令执行结果传递到流水线较早阶段。也就是没等这条指令执行结束,下一条指令就可以默认该结果有值了。一般来说,产生结果的只有执行和访存阶段,转发的方式就是从执行结果和访存阶段的结果直接添加一条回到译码阶段的回路。

第一种方法简单效率不高,第二种方法效率高但是有些问题也不能完全解决数据冒险问题。例如load加载冒险,因为访存阶段在比较后面,如果第二条指令直接使用load的结果,在译码阶段执行时不能完成转发。所以只有使用转发和暂停的联合方式,才能解决所有的冒险问题。

用预测技术解决控制冒险

当遇到跳转指令的时候使用预测技术对其进行预测,预测正确,流水线继续执行。预测错误,恢复现场,删掉流水线中的指令。ret指令是个特例,当执行ret指令时,必须要暂停,因为它没法进行预测下一次地址是什么了。

异常处理

顺序结构异常处理就是halt停机,然后向操作系统报告异常就行了。
流水线处理的时候,如果流水线中的多条指令同时出现异常,那么只向操作系统报告流水线中最深的指令引起的异常,就是运行最多的指令。当预测指令出现异常时,应该等到预测正确与否才报告是否异常。如果一条指令引起了异常,那么它之后的指令不应该影响系统状态。比如说CC的状态,所以不仅要暂停后续指令改变状态的操作,还需要专门的异常处理程序对状态进行恢复。

存储接口

从主存中读取需要3~20个周期,可以等待,暂停流水线。
但是当需要的数据不在主存中,而在磁盘中,因为读取一个磁盘的操作需要几百个时钟周期,所以不能等待。这个时候采用的操作系统采用缺页中断的处理方式进行读取。
总结来说主存读取用暂停,磁盘读取用中断方法。

欢迎同行批评指正,互相交流!

[1]http://blog.csdn.net/qyee16/article/details/6664377

0 0
原创粉丝点击