处理器的流水线式实现

我们熟知的编程语言C、java、脚本语言等,都是按照顺序执行的,前面提及过的文章中,机器代码也是按照顺序执行的,但是从机器硬件的实现上,近代处理器执行指令的顺序并不是我们所看到的顺序执行,然而,却能保证指令顺序执行的结果。
前提知识:
1.机器指令是二进制格式的代码,以字节为单位的一个或多个的字节序列。
2.指令集体系结构(ISA):一个处理器支持的指令和指令的字节编码。不同的处理拥有不同的指令集体系结构(Intel IA32与x86-64)。因此,一个程序被编译在一个机器上运行,就不能在另外的机器上运行!
处理器的流水线式实现
图一,指令代码表示

指令的顺序实现
通常,处理一条指令包括很多操作。将它们组织成某个特殊的阶段序列,即时指令动作差异很大,但所有的指令都遵循统一的序列。所有的指令动作都可以囊括为以下五个阶段:
取值:取值阶段从内存读取指令字节,地址为程序计数器(PC)的值。从指令中抽取出指令指示符字节的两个四位部分,称为icode(指令代码)和ifun(指令功能)。它可能取出一个寄存器指示符字节,指明一个或两个寄存器操作数指示符rA和rB。
译码:译码阶段从寄存器文件读入最多两个操作数,得到值valA和/和valB。通常,它读入指令rA和rB字段指明的寄存器,不过有些指令是读寄存器%rsp的。
执行:在执行阶段,算术/逻辑单元(ALU)要么执行指令指明的操作,计算内存引用的有效地址,要么增加或减少栈指针。得到的值我们成为valE。在此,也可能设置条件码。对一条条件传送指令来说,这个阶段会检验条件码和传送条件(由ifun给出),如果条件成立,则更新目标寄存器。同样,对一条跳转指令来说,这个阶段会决定是不是应该选择分支。
访存:访存阶段可以将数据写入内存,或者从内存读出数据。读出的值为valM。
写回:写回阶段最多可以写到两个结果到寄存器文件。
更新PC:将PC设置成下一条指令的地址。

任何一条简单的指令都可以按照上述的步骤完成一个周期,(下图表明寄存器文件与内存数据的读写是由时钟周期控制的,图中上凸的形状表明,此时寄存器文件处于不能修改的状态。)
处理器的流水线式实现

上述的简单指令的顺序实现,通过将执行每条不同指令所需的步骤组织成一个统一的流程,就可以用很少量的各种硬件单元以及一个时钟来控制计算的顺序,从而实现整个处理器。不过这样一来,控制逻辑就必须要在这些单元之间路由信号,并根据指令类型和分支条件产生适当的控制信号。
但是,这要求每个阶段必须等待上一个阶段的完成,下一个指令的寄存器状态需要等待上一个指令的寄存器值的更新,这就不能充分利用硬件单元的所有时间,从另外一个角度来看也不符合实际的利用二进制流控制硬件的实现,二进制流是高低电压的连续表示。

流水线的实现
处理器的流水线式实现
a).指令的顺序实现

处理器的流水线式实现
 b). 流水线实现

从顺序实现到流水线实现,其实就是把顺序实现的过程里面拆分出更小的阶段。图b中可看出,指令I1完成A阶段进入B阶段时,I2同时开始A阶段。那么组合逻辑A硬件就能够得到充分的利用。
影响流水线的效率的因素有以下两个:
1、组合逻辑不一致的划分,也即组合逻辑A的延迟时间与组合逻辑B的延迟时间大小悬殊
处理器的流水线式实现
 
2. 流水线过深,也即组合逻辑之间的保存状态的寄存器的数量太多,组合逻辑被划分成过分的小部分,影响了实际需要执行的内容。
处理器的流水线式实现

 流水线的数据相关
但是,我们很容易会产生以下的疑问:如果I2的A阶段依赖I1中C阶段的结果(下一条指令的寄存器状态依赖上一条指令的计算结果),那么怎么确保信息对称?
因此需要带反馈的流水线实现!表达每对相邻指令之间的数据相关。
对比上述的流水线实现,带反馈的流水线系统需要在指令的每一个阶段都加入流水线寄存器,保存该阶段的状态信息。另外,需要将更新PC操作,放到取指阶段,具体方法是:创建新的寄存器保存一条指令执行过程中计算出来的信号,然后在新的时钟周期开始时(新的指令执行时),利用上一条指令保存的信号通过同样的逻辑计算当前指令的PC值。
流水线寄存器的负责如下的信息保存,当我们执行一条新的指令时,这些流水线寄存器分别有F、D、E、M、W,它们的功能如下:
F:保存程序计算器的下一条指令的地址的预测值,也即下一条PC值;
D:位于取指与译码阶段之间。它保存关于最新取出的指令和从寄存器文件读出的值的信息,即将由执行阶段进行处理。
E:位于译码和执行阶段之间。它保存关于最新译码的指令和从寄存器文件读出的值的信息,即将由执行阶段进行处理。
M:位于执行和访存阶段之间。它保存最新执行的指令的结果,即将由访存阶段进行处理。它还保存关于用于处理条件转移的分支条件和分支目标的信息。
W:位于访存阶段和反馈路径之间,反馈路径将计算出来的值提供给寄存器文件写,而当完成ret指令时,它还要向PC选择逻辑提供返回地址。

在这里,我们如果要实现流水线,那么我们在执行当前指令的同时必须要预测下一条PC的值,对于普通情况,下一条PC的值可以简单地从二进制流中得到。但是对于条件分支指令,我们必须执行完结果后,才能知道下一条应该执行的指令的地址,这种情况,会采取之前的文章中提及的分支预测操作。
流水线冒险
上述需要预测下一条指令PC值的解决方法,这里将要讨论控制相关的策略,如何解决数据相关的问题。控制相关指,一条指令要确定下一条指令的位置,例如在执行跳转、条用或返回指令时。这些相关可能会导致流水线产生计算错误,称为冒险。同相关一样,冒险分为两类:数据冒险和控制冒险。
处理器的流水线式实现
图中是典型的数据相关的例子,即将要执行的指令需要前面计算后的值,但是流水线的过程,让前面的指令仍未计算好后面需要的值
 解决这种数据相关的问题,就需要数据冒险,可以使用如下的方法:
1.用暂停来避免数据冒险
暂停是避免冒险的一种常用技术,暂停时,处理器会停止流水线中一条或多条指令,知道冒险条件不再满足。让一条指令停顿在译码阶段,直到产生它的源操作数的指令通过了写回阶段。(如上图,当把addq指令阻塞在译码阶段时,我们还必须将紧跟其后的halt指令阻塞在取指阶段。通过将程序计数器保持不变就能做到这一点,这样一来,会不断地对halt指令进行取指,知道暂停结束)
2.用转发来避免数据冒险
与其暂停直到写完成,不如简单地将要写的值传到流水线寄存器E、M、W作为源操作数。如下图
处理器的流水线式实现
a)
处理器的流水线式实现
b)
处理器的流水线式实现
c)
上述三图表示利用流水线寄存器的值充分利用了转发技术,实现了数据冒险。图c)表示了ALU逻辑处理单元是不存在时序问题的。e_valE的值是在取指阶段就完成了。并把值存放在流水线寄存器中。
3.加载/使用数据冒险
有一类数据冒险不能单纯用转发来解决,因为内存读在流水线发生的比较晚。如下图所示:
处理器的流水线式实现
我们可以将暂停和转发结合起来,避免加载/使用数据冒险。上图所示,只要周期7延迟一个周期到周期八,就可以利用转发技术,因此。我们可以将译码阶段中的指令暂停一个周期,导致执行阶段中插入一个气泡。
处理器的流水线式实现

避免控制冒险
上述的加载/使用数据冒险很好的解决数据关联时,怎么实现流水线的假设。当处理器无法根据处于取指阶段的当前指令来确定下一条指令的地址时,就会出现控制冒险。
TODO:编写下面的内容 339...