教材参考计算机组织与结构——性能设计(第九版)第14章
12.1 Processor Organization处理器组织
为了理解处理器的组织,我们先来看一看处理器需要做的事情有哪些?
-
取指
-
译码
-
取操作数
-
处理数据
-
写回执行的结果
为了做到上面的这些事情,处理器必然需要保存一些数据,比如在取指时,处理器需要知道到哪里取指令,那么就需要保存下一条指令的地址;再比如,处理器执行时,比如处理器在运算,也需要暂存一些数据。
那么这些数据又存储在哪里呢,处理器内部有一些小的存储单元——寄存器,这些处理器需要暂存的数据或者指令就存储在寄存器内。
回忆一下,在之前的章节中说过,CPU内部的组成部分有4个,分别为:CU(控制单元Control Unit)、ALU(算数逻辑单元Arithmetic Logic Unit)、Registers(寄存器)和内部总线。下图展示了CPU和系统总线的关系:
将上图中的CPU细化一下,CPU的内部结构如下图所示:
从上图中可以看到,寄存器和ALU通过内部总线进行数据的传输,这是因为ALU只能够从处理器内部的存储获取数据进行运算。
12.2 寄存器组织
寄存器大致可以分为两类,当然,实际上这两类并没有清晰的界限。
-
用户可见寄存器
-
控制和状态寄存器
首先,我们来讲讲用户可见寄存器,用户可见寄存器可以分成下面这4种,分别为通用寄存器、数据寄存器、地址寄存器和条件码寄存器。
通用寄存器(general purpose):通用寄存器可以包含任意指令的操作数。对比专用的(dedicated)来说,专用寄存器有只用来保存浮点数的,也有专门用于栈操作的寄存器。有些时候,通用寄存器会被用在寄存器间接寻址和偏移寻址(详见寻址方式一节)。
数据寄存器:只能用来存储数据,不能用来进行操作数地址的计算,如累加寄存器(Accumulator)。
地址寄存器:可能在某些寻址模式中被使用,比如索引寻址,还有可能被用在支持分段的机器上被用作段基址寄存器(见操作系统中关于内存管理的部分),或者,也可以被用在栈指针(用来进行栈的操作,如ARM中的R13也叫SP)。
条件码寄存器:可以被用来存储最近一次运算结果的信息,比如进位(carry)、溢出(overflow)、负数(negative)、零(zero)。例如,当进行了减法运算,结果如果是负数,那么条件码寄存器中表示负数的一位就会被置位。在进行跳转的时候经常会使用到该寄存器,当然,是隐式的使用,比如当结果为零时跳转,就会读条件码寄存器看是否为零。
条件码寄存器很多时候不能被程序设置,而且是对于程序员部分可见的。
讲完了用户可见寄存器,我们再来看看另一类——控制状态寄存器(Control and Status Register),显然,这些寄存器是用户不可见的(不然为什么不也叫用户可见寄存器呢)。
对于不同的系统有不同的控制状态寄存器,但是基本上都会包含这么几个对于程序执行至关重要的寄存器(在不同的系统中可能名字不同):
-
程序计数器
PC
(Program Counter):存储了下一条要执行的指令的地址 -
指令寄存器
IR
(Instruction Register):存储了最近一次取的指令 -
内存地址寄存器
MAR
(Memory Address Register):存储了一个内存地址(用来读内存) -
内存缓冲寄存器
MBR
(Memory Buffer Register):存储了最近一次从内存中取到的数据或者是要写入内存的数据
不是所有的处理器都会有MAR和MBR,但是会有类似的缓存机制。
常见的取指过程是:首先将PC的值传给MAR,然后然后MAR将内存地址通过地址总线发给内存,同时CPU发出存储器读命令,在内存中找到要取得指令,然后内存将指令放到数据总线上传给MBR,接着MBR的值会被传送给IR。
除了以上的这4个寄存器,通常还会有一个或多个寄存器被称为程序状态字PSW
(Program Status Word),操作系统中也会提到这个寄存器,因为PSW中包含了CPU正处于的模态(管态/用户态),一般还会包含比如进位、溢出、是否允许中断的信息等。
只有在管态才能执行特权指令。
到这里我们就讲完了寄存器的组织,在进行控制状态寄存器设计的时候,一个十分重要的因素就是操作系统的支持,设计者对于操作系统有着怎样的理解,那么设计寄存器的时候就会去满足操作系统的需求。还有一个因素就是关于控制的信息放多少在寄存器中,多少在内存中,这是一个关于速度和代价的权衡问题。
12.3 指令周期——间址周期
之前,我们说指令的周期包括了3个:
-
取指周期
-
执行周期
-
中断周期
但是实际情况下,我们如果使用了间接寻址,即数据的地址是存储在内存某个单元中的,我们需要先获得内存中数据的地址,然后再根据该地址到内存中找到我们需要的操作数。于是就引出了另一个指令周期——间址周期,加上该周期后,四个指令周期之间的转换如下图所示。
指令周期转换的主线是取指和执行的交替,加上间址周期后,在取指完成之后,会检查是否需要间址周期(判断是否用了间接寻址),如果需要的话则转入间址周期,反之则直接转到执行周期。
更加细化的指令周期图如下:
先进行取指,然后进行指令的译码,再计算操作数的地址,取操作数,如果有需要则进入间址周期(当然也有可能有多个操作数,需要取多次),然后进行数据的运算,运算完成后进行地址的计算,将结果存入该地址(也有可能进入间址周期,因为可能存入内存地址中的地址),后面就进入了中断周期,判断是否有中断,如果有则进行中断的处理。
间址周期CPU和内存的交互如下图:
如果已经取完指令,那么IR和MBR中保存的都是取到的指令,先检查IR看是否是间接寻址,如果是,则将MBR中的指令地址传给MAR,然后读内存,获得操作数真正的内存地址。
12.4 流水线
在补充了指令周期之后,我们来到重点——流水线(Pipeline)。
首先,什么是流水线? 流水线取自工厂中的流水线作业,想象一下,把一个作业分成多个互不干扰的作业,然后每一个人只负责其中一段,结束之后交给下一个人,流水线技术就是基于这样的思想。将指令的执行分成若干流水段,比如有取指、译码、执行,一个指令在译码的时候,上一个可能在执行,而下一个可能在取指,这样就增加了并行性。
举个例子,我们可以将指令的执行分为这样几个段:
-
取值 Fetch instruction (FI)
-
译码 Decode instruction (DI)
-
计算操作数地址 Calculate operands address (CO)
-
取操作数 Fetch operands (FO)
-
执行 Execute instructions (EI)
-
写回结果 Write operand (WO)
那么流水线在理想情况下会这样工作:
比如在第4个时间段中,第一条指令在取操作数,第二条指令在计算操作数的地址,第三条指令在译码,第四条指令在取值,这样看上去就是4条指令并行执行。
当然,上面这6段流水线是理想情况下的,实际情况会有下面这几个因素使得流水线的效率没有理想的这么高。
-
有的指令可能没有其中的一些段,比如
load
指令并不需要写回结果,那么就没有了WO这一段。 -
如果有冲突和依赖关系,就不会这么顺利地流水,比如存在先写后读的相关,前一条指令对数据进行写,后一条指令读该数据,那么只有在前一条指令WO完成后,后一条指令才能取操作数FO。
-
每一个阶段的长度是不同的,流水段设计的长度是根据最长的一个阶段来的,比如CO是最长的,那么流水段的长度就会是CO的时间。
-
跳转和中断会破坏流水线
跳转破坏流水线的例子如下:
指令3是跳转指令,那么指令3执行完后,需要取指令15,而指令4到指令7的这些流水段都需要被清空,就没有用了。
当然了,虽然存在这些问题,流水线还是被证实能够大大缩短程序执行的时间的。那么如何来衡量流水线的性能呢?
我们引入一个加速比(Speeup)的概念来衡量流水线的性能:
$$
S_k=\frac{没有使用流水线的时间T_{1,n}}{使用了流水线的时间T_{k,n}},其中k表示流水段数量
$$
假设是理想的流水段,定义每一段的时间为:
$$
\tau=max[\tau_i]+d,其中1\le i\le k,d为流水段之间转换的延迟
$$
那么加速比为:
$$
S_k=\frac{T_{1,n}}{T_{k,n}}=\frac{nk\tau}{[k+(n-1)]\tau}=\frac{nk}{k+(n-1)\tau}
$$
上式中当n趋于无穷的时候,加速比可以趋于k,那么就有人想着,那不如直接让流水段的段数无限增大,岂不是就能够提高性能?
实际上并不是的。有这么几点原因:
-
增多流水线段数会让冲突的概率变大
-
控制流水线的逻辑会变得很复杂
-
流水线之间如果要传递数据,那么就需要锁存,锁存的延迟也是一个问题
上面我们提到了“冲突”,那么到底什么是流水线冲突呢?
冲突就是一些使得流水线不能够按理想情况执行的情况,可以分为如下几种:
-
资源冲突:假设内存只有一个端口,每次只允许一个读操作,而取操作数和取指令都要读内存,这时候就会产生资源的冲突。
-
数据冲突:两条指令对同一寄存器或内存单元进行操作,且两条指令有严格的先后顺序。
-
先写后读(RAW)比如你先把1写成2然后读出来,和先读了1再写成2
-
先读后写(WAR)比如你先把1读了然后再写为2,和先写为2再读
-
写写(WAW)比如你先赋值为1,再赋值为2,这能反过来么
-
那么问题来了,如何解决呢?
大致有这么5种方式:
-
多条流水线
-
预取转移目标
-
循环缓冲
-
分支预测
-
分支延迟
具体内容可以自行查阅,这一章就讲这么多啦~