CSAPP学习笔记(六)

Posted by HK on February 8, 2019

第四章

存储器和时钟

组合电路从本质上来讲不存储任何信息,只是简单地响应输入信号,产生等于输入的某个函数的输出。

按位存储信息的设备:由同一个时钟控制的。(时钟是一个周期性信号,决定什么时候要把新值加载到设备中。)

  • 时钟寄存器(寄存器)存储单个位或字。时钟信号控制寄存器加载输入值。
  • 随机访问存储器(内存)存储多个字,用地址来选择该读或该写哪个字。

处理器从来不需要为了完成一条指令的执行而去读由该指令更新了的状态。这是处理器很重要的一条规则,熟悉汇编语言的同学都知道,在汇编语言里面,条件判断是分为两步执行的。第一步进行计算,第二步取值(条件码的值)。而且对于我当前的语句来说,我有可能用到上一次的条件码的值,所以我当次是不能改变条件码的,只有等到下一条指令执行的时候(下一个时钟周期)才能对CC的值进行改变。

Y86-64的顺序实现

一条指令,对于处理器来说,它会有以下操作:

1.取指(fetch):取指阶段从内存读取指令字节,地址为程序计数器的值。从指令中抽取出指令指示符字节的两个部分,称为icode(指令代码)和ifun(指令功能)。它可能取出一个寄存器指示符字节,指明一个或两个寄存器操作数指示符rA和rB。它还可能取出一个四字节常数字valC。它按顺序方式计算当前指令的下一条指令的地址valP。也就是说,valP等于PC的值加上已取出指令的长度。

2.译码(decode):译码阶段从寄存器文件读入最多两个操作数,得到值valA和valB。通常,它读入指令rA和rB字段指明的寄存器,不过有些指令是寄存器%esp的。

3.执行(execute):在执行阶段,算术/逻辑单元(ALU)要么执行指令指明的操作(根据ifun的值),计算内存引用的有效地址,要么增加或减少栈指针。得到的值我们称为valE。在此,也可能设置条件码。对一条条件传送指令来说,这个阶段会检验天骄码和传送条件(由ifun给出),如果条件成立,则更新目标寄存器。同样,对一条跳转指令来说,这个阶段会决定是不是应该选择分支。

4.访存(memory):访存阶段可以将数据写入内存,或者从内存读出数据。读出的值为valM。

5.写回(write back):写回阶段最多可以写两个结果到寄存器文件。

6.更新PC(PC update):将PC设置成下一条指令的地址。

处理器流水线实现

由于资源的利用率太低,所以说需要采用流水线的方式来提高资源利用率。流水线可以提高系统的吞吐率,但会增加单条指令的运行时间。因为在把指令的执行划分为多个段之后,需要增加时钟寄存器来存储中间的状态,这些状态的存储更新过程是需要花费时间的。流水线多适用于大规模的系统处理

流水线段划分

流水线设计时分段原则:

  • 划分的段所用时间尽量相同或相近。因为流水线运行周期是由最慢阶段的延迟限制的,如果段有长有短,会浪费时间。
  • 段划分不能太细,因为中间的时钟寄存器需要花费一定的时间,这实际上为指令的执行增加了时间。如果我们划分的太细,甚至增加的时间比原来消耗的时间都多,那就得不偿失了。

上面两条原则实际上是在告诉我们段划分既不能太大也不能太小(太大肯定不行,我们划分流水线就是为了变小,不然直接用顺序好了)。常用的分段方式就是把流水线划分为:取指、译码、执行和访存四个阶段。那这里自然而然有个问题,就是执行阶段有很多非常复杂的操作,比如说浮点数操作,可能大概需要二十几个时钟周期的操作,这样就拖慢了每一条指令的执行周期,流水线的段划分就会收到很大的影响。

针对这种多周期指令的特殊问题,我们通常采用以下两种方法解决:

  • 最简单的实现方法自然就是在执行阶段停留几个周期,但是这样做肯定就会导致很多的空操作。但是这个实现简单,当然效率就不高了。
  • 现在普遍采用的方法就是采用一个独立于流水线的特殊硬件单元来处理较为复杂的操作,这样性能会更好一些。比如专门浮点运算处理单元。将特殊指令发射到特殊的硬件处理单元,这个单独的硬件处理单元也可以采用流水线化的处理方式进行处理,但是一般来说这个特殊的硬件处理单元会有自己的时钟周期。原来的流水线继续执行流水线指令,两个硬件并发执行。(当然如果存在数据相关,后面的指令就可能得等待当前的指令执行结束了)