深入理解操作系统(12)第四章:处理器体系结构(4)Y86-64的流水线实现(包括:PIPE-处理器/预测下一个PC/分支预测/流水线冒险/暂停,转发避免冒险/PPE硬件结构及实现/CPI)
1. Y86-64的流水线实现
我们开始时以SEQ+作为基础,在各个阶段之间添加流水线寄存器。
1.1 插入流水线寄存器
1.1.1 PIPE-抽象试图
在我们创建一个流水线化的Y86处理器的最初尝试中,我们要在SEQ的各个阶段之间插入流水线寄存器,并对信号重新做了排列,得到PIPE-处理器。
PIPE-处理器,“-”代表这个处理器和最终的处理器设计相比,性能要差
PIPE-的抽象结构如图4.39所示
图4.39
1.1.2 例子说明
下图表明的是下面这段代码序列是怎样通过我们的五阶段流水线的
irmovl $1, %eax #I1
irmovl $2, %ebx #I2
irmovl $3, %ecx #I3
irmovl $4, %edx #I4
halt #I5
图4.40
1.2 对信号进行重新排列和标号
SEQ+一次只处理一条指令,所以诸如vaC、srcA和vaE这样的信号有惟一的值。在流水线化的设计中,对应于正在经过系统的各个指令,有多组这样的值。
在我们采用的命名机制中,通过在信号名字前加上大写的流水线寄存器的名字作为前缀,可以惟一地确定存放在流水线寄存器中的信号。
例如,四个icode值分别命名为 D_icode、E_icode、M_icode和W_icode。
我们还需要引用某些在一个阶段内刚刚计算出来的信号。
PIPE-的硬件结构
图4.41
1.3 预测下一个PC
1.3.1 预测PC的下一个值,来实现每时钟周期都发射一条新指令
我们流水线化的设计的目的就是每时钟周期都发射(Issue)一条新指令
也就是说每个时钟周期都有一条新指令进入执行阶段并最终完成。
要达到这个目的也就意味着吞吐量是每个时钟周期一条指令。
为了达到这个目的,我们必须在取出当前指令之后,马上确定下一条指令的位置。
不幸的是,如果取出的指令是条件分支指令,要到几个周期后,也就是指令通过执行阶段之后,我们才能知道是否要选择分支。类似地如果取出的指令是ret,要到指令通过访存阶段,才能确定返回地址。
除了条件转移指令和ret这些例外,根据取指阶段中计算的信息,我们能够确定一条指令的地址。
对于cal和jmp(无条件转移)来说,下一条指令的地址是指令中的常数字valC,而对于其他指令来说就是valP。
因此,通过预测PC的下一个值,在大多数情况下,我们能达到每个时钟周期发射一条新指令的目的。
1.3.2 分支预测
对大多数指令类型来说,我们的预测是完全可靠的。
对条件转移来说我们既可以预测选择了分支,那么新PC值应为valC,也可以预测没有选择分支,那么新PC值应为valP。
无论在哪种情况中,我们都必须以某种方式来处理我们预测错误的情况,因为此时我们已经取出并部分执行了错误的指令。
我们会在4.59节中再讨论这个问题猜测分支方向并根据猜测开始取指的技术称为分支预测。
在我们的设计中,只使用了简单的策略,那就是总是预测选择了条件分支,因而预测PC的新值为valC
1.3.3 分支预测策略
我们的设计使用总是选择(always taken)分支的预测策略。
研究表明这个策略的成功率大约为60%。反过来,从不选择(never taken,NT)策略的成功率大约为40%。
一种稍微复杂一点的称为反向选择、正向不选择的策略,当分支地址较低时就预测选择分支,而分支地址较高时,就预测不选择分支而选择下一条指令,这种策略的成功率大约为65%。
这种改进是源自这样一个事实,那就是循环是由后向分支结束的,而循环通常会执行多次。前向分支用于条件操作,而选择的可能性较小。
1.3.4 带堆栈的返回地址预测
对大多数程序来说,很容易预测返回值,因为过程调用和返回是成对出现的。大多数函数调用,会返回到调用后的那条指令。高性能处理器中运用了这个属性,在取指单元中放入一个硬件栈,它保存着过程调用指令产生的返回地址。每次执行过程调用指令时,都将其返回地址压入栈中。当取出一个返回指令时,就从栈中弹出最上面的值,作为预测的返回值。
同分支预测一样,也必须提供在预测错误时的恢复机制,因为还是有调用和返回不匹配的时候。
通常,这种预测是很可靠的。这个硬件栈对程序员来说是不可见的。
PIPE的取指阶段,负责预测PC的下一个值,以及为指令的读取选择实际的PC。
1.4 流水线冒险
1.4.1 相关
我们在4.44节中的讨论,将流水线技术引入一个带反馈的系统会导致相邻指令间在发生相关时出现问题。
在完成我们的设计之前,必须解决这个问题。这些相关有两种形式:
1. 数据相关,下一条指令会用到这条指令计算出的结果
2. 控制相关,一条指令要确定下一条指令的位置
1.4.2 冒险:
在执行跳转,调用或返回指令时,这些相关可能会导致流水线产生计算错误,称为冒险。
同相关一样,冒险也可以分为两类:
1. 数据冒险( data hazard):
下一条指令会用到这条指令计算出的结果,未读取到会导致错误
2. 控制冒险( control hazard)。
在本节中,我们关心的是数据冒险。我们会将控制冒险作为整个流水线控制的一部分加以讨论(4.59节)。
1.4.3 无冒险-prog1
图4.42描述的是PPE-处理器处理称为prog1的指令序列的情况。
我们重点关注两条 irmovl指令和addl指令之间的数据相关可能造成的数据冒险。
图4.42
在周期7开始以后,两条irmovl都已经通过写回阶段,所以寄存器文件保存着更新过的%edx和%eax的值。
在此示例中,两条irmovl指令和addl指令之间的数据相关没有造成数据冒险。
原因:
因为三条nop指令在有数据相关的指令之间创些延迟。
1.4.4 有冒险-prog2
irmovl指令和add指令之间有两条nop指令
图4.43
第一个irmovl指令已经通过了写回阶段,因此程序寄存器%edx已经在寄存器文件中更新过了。
在该周期内,第二个irmovl指令处于写回阶段,因此对程序寄存器%eax的写要到周期7开始,时钟上升时,才会发生。
结果,读出%eax的错误值(在此我们假设所有的寄存器的初始值为0),因为对该寄存器的写还未发生。很明显,我们的流水线还不能正确处理这样的冒险。
1.4.5 有冒险-prog3
irmovl指令和add指令之间有一条nop指令
图4.44
现在我们必须检查周期5内流水线的行为,此时addl指令通过解码阶段。不幸的是,对寄存器%edx的写仍处在写回阶段,而对寄存器%eax的写还处在访存阶段。因此,addl指令会得到两个错误的操作数。
1.4.6 列举数据冒险的类型
当一条指令更新,而后面指令会读到的那些程序状态时,就有可能出现冒险。
所谓的程序状态包括:程序寄存器、条件码、存储器和程序计数器。
下面让我们来看看每类状态出现冒险的可能性:
1.程序寄存器:我们已经认识这种冒险了。
出现这种冒险是因为寄存器文件的读写是在不同的阶段进行的,
导致不同指令之间可能出现不希望的相互作用
2.条件码:在执行阶段中,既可能写(整数操作)也可能读(条件转移)条件码。
在条件转移经过这个阶段之前,前面所有的整数操作都已经完成这个阶段了。
所以不会发生冒险。
3.程序计数器:更新和读取程序计数器之间的冲突导致了控制冒险。
当我们的取指阶段逻辑在取下一条指令之前,正确预测了程序计数器的新值时,就不会产生冒险。
预测错误的分支和ret指令需要特殊的处理
4.存储器:对数据存储器的读和写都发生在访存阶段。
在一条读存储器的指令到达这个阶段之前前面所有要写存储器的指令都已经完成这个阶段了。
1.5 用暂停来避免冒险
暂停( stalling)是一种常用的用来避免冒险的技术。
暂停时,处理器会停止流水线中一条或多条指令,直到冒险条件不再满足。
(其实就是等待,当前等待前面得指令执行结束,可以获取正确值为止。)
只要一条指令的源操作数会被流水线后面某个阶段中的指令产生,处理器就会通过将指令阻塞在解码阶段来避免数据冒险。
1.6 用转发(forwarding)来避免冒险
我们PIPE-的设计是在解码阶段从寄存器文件中读入源操作数,但是有可能对这些源寄存器的写要在写回阶段才能进行。
与其暂停直到写完成,不如简单地将要写的值传到流水线寄存器E作为源操作数。
转发:直接将写回阶段的值传到流水线寄存器做源操作数,而不用再写寄存,读寄存器
这种将结果值直接从一个流水线阶段传到较早阶段的技术称为数据转发(data forwarding)。
或简称转发
图4.49
图4.49用prog2周期6的流水线图的详细描述来说明了这一策略。
解码阶段逻辑发现寄存器%eax是操作数valB的源寄存器,而在写端口E上还有一个对%eax的未进行的写。它只要简单地将提供到端口E的数据字(信号W_valE)作为操作数valB的值,就能避免暂停。
这种将结果值直接从一个流水线阶段传到较早阶段的技术称为数据转发(data forwarding,或简称转发)。
它使得prog2的指令能通过流水线而不需要任何暂停。
图4.50
如图4.50描述的那样,
当访存阶段中有对寄存器未进行的写时,也可以使用数据转发,以避免程序prog3中的暂停。
在周期5中,解码阶段逻辑发现,在写回阶段中端口E上有对寄存器%edx未进行的写,以及在访存阶段中有会在端口E上对寄存器%eax未进行的写。它不会暂停直到这些写真正发生,而是用写回阶段中的值(信号w_valE)作为操作数valA,用访存阶段中的值(信号M_vaE)作为操作数vaB。
为了充分利用数据转发技术,我们还可以将新计算出来的值从执行阶段传到解码阶段,以避免程序prog4所需要的暂停
图4.51
如图4.51所示。
在周期4中,解码阶段逻辑发现在访存阶段中有对寄存器%edx未进行的写,而且执行阶段中ALU正在计算的值稍后也会写入寄存器%eax。
它可以将访存阶段中的值(信号M_valE)作为操作数valA,
也可以将ALU的输出(信号e_valE)作为操作数valB注意,使用ALU的输出不会造成任何同步问题。
解码阶段只要在时钟周期结束之前产生信号valA和valB,这样在时钟上升开始下一个周期时,流水线寄存器E就能装载来自解码阶段的值了。而在此之前ALU的输出已经是合法的了。
图4.52
图4.52给出的是PPE的抽象结构,它是PPE-的扩展,能通过转发处理数据冒险。
我们可以看到添加了从五个转发源到解码阶段的额外的反馈路径(用深灰色表示)。这些旁路路径( bypasspath) 反馈到解码阶段中一个标号为“Forward”的块。
这个块会用从寄存器文件中读出的值或转发过来的值作为源操作数valA和vaB。
图4.53
图453给出了PPE硬件结构的一个更为详细的说明。
1.6 加载使用(load/use)数据冒险
有一类数据冒险不能单纯用转发来解决,因为存储器读是在流水线较后面才发生的。
1.6.1 问题:
图4.54
图4.54举例说明了加载/使用冒险(load/use hazard),其中一条指令(位于地址0x018的mrmovl)从存储器中读出寄存器%eax的值,而下一条指令(位于地址0x01e的add)需要该值作为源操作数,图的下部是周期7和8的扩展说明。
add指令在周期7中需要该寄存器的值,但是mrmovl指令直到周期8才产生出这个值。
为了从mrmovl“转发到”addl,转发逻辑不得不将值送回到过去的时间!这显然是不可能的,我们必须找到其他机制来解决这种形式的数据冒险。
1.6.2 解决:暂停+转发
图4.55
如图4.55展示的那样,
我们可以通过将暂停和转发结合起来,避免加载/使用数据冒险。
当mrmovl指令通过执行阶段时,流水线控制逻辑发现解码阶段中的指令(add)需要这个从存储器中读出的结果。它会将解码阶段中的指令暂停一个周期,导致执行阶段中插入一个气泡。如周期8的扩展说明所示,从存储器中读出的值可以从访存阶段转发到解码阶段中的add指令。寄存器%edx的值也可以从写回阶段转发到访存阶段。就像流水线图中,从周期7中标号为“D”的方框到周期8中标号为“E”的方框的箭头表明的那样,插入的气泡代替了正常情况下本来应该继续通过流水线的add指令。
1.6.3 加载互锁
这种用暂停来处理加载/使用冒险的方法称为加载互锁(load interlock)。
加载互锁和转发技术结合起来足以处理所有可能类型的数据冒险。因为只有加载互锁会降低流水线的吞吐量,我们几乎可以实现每个时钟周期发射一条新指令的吞吐量目标。
1.7 PIPE各阶段的实现
现在我们已经创建了PIPE(即我们使用转发技术的流水线化的Y86处理器)的整体结构。
PIPE使用了一组与前面顺序设计相同的硬件单元,
另外增加了一些流水线寄存器、重新配置了的逻辑块以及流水线控制逻辑。
在本节中,我们将浏览各个逻辑块的设计,而将流水线控制逻辑的设计放到下一节中再讲。
许多逻辑块与SEQ和SEQ+中相应部件完全相同,除了我们必须从来自不同流水线寄存器(用大写的流水线寄存器的名字作为前缀)或来自各个阶段计算(用小写的阶段名字的第个字母作为前缀)的信号中选择适当的值。
1.7.1 PC选择取指阶段
这个阶段还必须选择程序计数器的当前值,以及预测下一个PC值。
PC选择逻辑从三个程序计数器源中进行选择。当一条预测错误的分支进入访存阶段时,会从流水线寄存器M(信号M_valA)中读出该指令valP的值(指明下一条指令的地址)。当ret指令进入写回阶段时,会从流水线寄存器W(信号W_valM)中读出返回地址。其他情况会使用存放在流水线寄存器F中(信号F_oredPC)的PC的预测值。
图略
后面再详细添加。
1.7.2 解码和写回阶段
图4.57略给出的是PIPE的解码和写回逻辑的详细说明。
标号为“dsE”、“dsM"“srcA”和“srcB的块非常类似于它们在SEQ的实现中的相应部件。我们观察到,提供给写端口的寄存器ID来自于写回阶段(信号W_dsE和 w_dstM),而不是来自于解码阶段。这是因为我们希望进行写的目的寄存器是由写回阶段中的指令指定的。
略
1.7.3 缓存阶段
略
1.8 流水线控制逻辑
现在我们准备要创建流水线控制逻辑,完成我们的PPE设计了。
这个逻辑必须处理下面三种控制情况,即其他机制(例如数据转发和分支预测)不足以处理的控制情况处理
1. ret:
流水线必须暂停直到ret指令到达写回阶段
2. 加载/使用冒险:
在一条从存储器中读出一个值的指令和一条使用该值的指令之间,流水线必须暂停一个周期。
3. 预测错误的分支:
在分支逻辑发现不应该选择分支之前,分支目标处的几条指令已经进入流水线了。
必须从流水线中去掉这些指令。
我们会浏览每种情况所期望的行为,然后再设计出处理这些情况的控制逻辑。
例子略
1.9 性能分析
1.9.1 问题:
我们可以看到,所有需要流水线控制逻辑进行特殊处理的条件,都会导致我们的流水线不能够实现每个时钟周期发射一条新指令的目标。
1.9.2 解决:插入气泡
我们可以通过确定往流水线中插入气泡的频率,来衡量这种效率的损失,因为插入气泡会导致无用的流水线周期。
一条返回指令会产生三个气泡
一个加载/使用冒险会产生一个
一个预测错误的分支会产生两个
1.9.3 CPI每指令周期数
我们可以通过计算PIPE执行一条指令所需要的平均时钟周期数的估计值,来量化这些处罚对整体性能的影响,
这种衡量方法称为CPI(cycles per instruction,每指令周期数)。
这种衡量值是流水线平均吞吐量的倒数,不过时间单位是时钟周期,而不是微微秒。
1.9 未完成的工作
我们已经创建了PIPE流水线化的微处理器结构,设计了控制逻辑块,并实现了普通流水线流( pipeline flow)不足以处理特殊情况的流水线控制逻辑。
问题:
不过,PPE还是缺乏一些实际微处理器设计中所必需的关键特性。我们会强调其中一些,并讨论要增加这些特性需要些什么。
1.9.1 异常处理
当一个机器级程序遇到出错的情况时
例如,非法指令代码或是指令或数据地址越界会导致程序流中断,
称为异常(exception)。
异常看上去就像过程调用,它调用一个异常处理程序( exceptionhandler),该程序是操作系统的一部分。
执行halt指令也会触发一个异常。异常处理是处理器指令集体系结构的一部分。通常,依赖于异常的类型,异常会导致处理停止。
异常处理包括一些细节问题:
1. 首先,可能同时有多条指令会引起异常
2. 当首先取出一条指令,开始执行时,导致了一个异常,而后来由于分支预测错误,取消了该指令
1.9.2 多周期指令
例子:
Y86指令集中的所有指令都包括一些简单的操作,例如数字加法。这些操作可以在执行阶段个周期内处理完。
在一个更完整的指令集中,
我们还需要实现一些需要更为复杂操作的指令,例如整数乘法和除法,以及浮点运算。
他们需要大约几十个个指令周期
在一个像PIPE这样性能中等的处理器中,这些操作的典型执行时间从浮点加法的3或4个周期到整数除法的32个周期。
解决:
为了实现这些指令,我们既需要额外的硬件来执行这些计算,还需要一种机制来协调这些指令的处理与流水线其他部分之间的关系。
实现多周期指令的一种简单方法就是只是简单地扩展执行阶段逻辑的功能,添加一些整数和浮点算术运算单元。
1.9.3 存储系统的接口
在我们对PPE的描述中,我们假设取指单元和数据存储器都可以在一个时钟周期内读或是写存储器中任意的位置。
我们还忽略了由自我修改(self-modifying)代码造成的可能冒险。在自我修改代码中,一条指令对一个存储区域进行写,而后面的指令又从这个区域中读取。进一步说,我们是以存储器位置的虚拟地址来引用它们的,这就要求在执行实际的读或写操作之前,要将虚拟地址翻译成物理地址。显然,要在一个时钟周期内完成所有这些处理是不现实的。
更糟糕的是,正在访问的存储器的值可能是位于磁盘上的,这会需要上百万个时钟周期才能把数据读入到处理器存储器中。
1.10 当前的微处理器设计
一个五阶段流水线,例如我们已经讲过的PIPE处理器,代表了20世纪80年代中期的处理器设计水平
2. 本章小结
2.1 ISA 指令集体系结构
我们已经看到,指令集体系结构(即ISA)在处理器行为(就指令集合及其编码而言)和如何实现处理器之间提供了一层抽象。
一个处理器支持的指令和指令的字节级编码称为它的ISA(指令集体系结构)。
ISA提供了程序执行的一种顺序说明,也就是一条指令执行完了下一条指令才会开始。
2.2 Y86
基本IA32指令集,并且大大简化其数据类型、地址模式和指令编码,我们定义出了Y86指令集。得到的ISA既有RISC指令集的属性,也有CISC指令集的属性。然后,我们将不同指令组织放到五个阶段中处理,在此,根据被执行的指令的不同,每个阶段中的操作也不相同。
2.3 SEQ
SEQ(取的是“sequentia”处理器的意思)的处理器。
SEQ就是一个能执行这些计算的硬件结构(指令操作6步骤)的抽象表示
从此,我们构造了SEQ处理器,其中每个时钟周期推进一条指令通过每个阶段。
2.4 SEQ+
SEQ+:
使得更新PC阶段在一个周期开始时执行,而不是结束时才执行,这样产生的处理器设计称为SEQ+,
因为它扩展了基本的SEQ处理器。
通过重新排列各个阶段,我们创建了SEQ+设计,其中第一个阶段选择程序计数器的值,它被用来取出当前指令流水线化通过让不同的阶段并行操作,改进了系统的吞吐量性能。
在任意一个给定的时刻,多条指令被处理。在引入这种并行性的过程中,我们必须非常小心,以提供与程序的顺序执行相同的用户可见的、程序级行为。
2.5 PPE-
PPE-:
在SEQ+的各个阶段之间插入流水线寄存器,并对信号重新做了排列,得到PIPE-处理器。
PIPE-处理器,“-”代表这个处理器和最终的处理器设计相比,性能要差
我们通过往SEQ+中添加流水线寄存器,并重新安排周期来创建PPE-流水线,介绍了流水线化。
然后,我们添加了转发逻辑,加速了将结果从一条指令发送到另一条指令,从而提高了流水线的性能。有几种特殊情况需要额外的流水线控制逻辑来暂停或取消一些流水线阶段。
2.6 有关处理器设计的几个重要经验
在本章中,我们学习了有关处理器设计的几个重要经验
1. 管理复杂性是首要问题。
我们想要优化使用硬件资源,在最小的成本下获得最大的性能。
为了实现这个目的,我们创建了一个非常简单而一致的框架,
来处理所有不同的指令类型有了这个框架,我们就能够在处理不同指令类型的逻辑中间共享硬件单元。
2. 我们不需要直接实现ISA
ISA的直接实现意味着一个顺序的设计。
了获得更高的性能,我们想运用硬件能力以同时执行许多操作,这就导致要使用流水线化的设计。
3. 硬件设计人员必须非常谨慎小
芯片被制造出来,就几乎不可能改正任何错误了。
开始就使设计正确是非常重要的。