3.5 Y84-64的流水线实现

发布时间 2023-06-02 11:25:35作者: C~A

我们终于准备好要开始本章的主要任务——设计一个流水线化的Y86-64处理器。首先,对顺序的SEQ处理器做一点小的改动,将PC的计算挪到取指阶段。然后,在各个阶段之间加上流水线寄存器。到这个时候,我们的尝试还不能正确处理各种数据和控制相关。不过,做一些修改,就能实现我们的目标——一个高效的、流水线化的实现Y86-64ISA的处理器。

SEQ+

调整一下SEQ的顺序,使更新PC阶段在一个时钟周期开始时执行,我们称这种修改过的设计为SEQ+。我们移动PC阶段,使得它的逻辑在时钟周期开始时活动,使它计算当前指令的PC值。图给出了SEQ和SEQ+在PC计算上的不同之处。在SEQ中(图4-39a),PC计算发生在时钟周期结束的时候,根据当前时钟周期内计算出的信号值来计算PC寄存器的新值。在SEQ+中(图4-39b),我们创建状态寄存器来保存在一条指令执行过程中计算出来的信号。然后,当一个新的时钟周期开始时,这些信号值通过同样的逻辑来计算当前指令的PC。我们将这些寄存器标号为“pIcode”、“pCnd”等等,来指明在任一给定的周期,它们保存的是前一个周期中产生的控制信号。

 

 

 

 PIPE-

在SEQ+的各个阶段之间插入流水线寄存器,并对信号重新排列,得到PIPE-处理器。

流水线寄存器按如下方式标号:

F保存程序计数器的预测值,稍后讨论。

D位于取指和译码阶段之间。它保存关于最新取出的指令的信息,即将由译码阶段进行处理。

E位于译码和执行阶段之间。它保存关于最新译码的指令和从寄存器文件读出的值的信息,即将由执行阶段进行处理。

M位于执行和访存阶段之间。它保存最新执行的指令的结果,即将由访存阶段进行处理。它还保存关于用于处理条件转移的分支条件和分支目标的信息。

W位于访存阶段和反馈路径之间,反馈路径将计算出来的值提供给寄存器文件写,而当完成ret指令时,它还要向PC选择逻辑提供返回地址。

 

对信号进行重新排列和标号

我们采用的命名机制,通过在信号名前面加上大写的流水线寄存器名字作为前缀,存储在流水线寄存器中的信号可以唯一被标识。在信号名前面加上小写的流水线寄存器名字作为前缀,表示在一个阶段刚计算出来的信号。

SEQ+和PIPE一的译码阶段都产生信号dstE和dstM,它们指明值valE和valM的目的寄存器。在SEQ+中,我们可以将这些信号直接连到寄存器文件写端口的地址输入。在PIPE一中,会在流水线中一直携带这些信号穿过执行和访存阶段,直到写回阶段才送到寄存器文件(如各个阶段的详细描述所示)。我们这样做是为了确保写端口的地址和数据输入是来自同一条指令。否则,会将处于写回阶段的指令的值写入,而寄存器ID却来自于处于译码阶段的指令。作为一条通用原则,我们要保存处于一个流水线阶段中的指令的所有信息。

PIPE一中有一个块在相同表示形式的SEQ+中是没有的,那就是译码阶段中标号为“Select A”的块。我们可以看出,这个块会从来自流水线寄存器D的valP或从寄存器文件A端口中读出的值中选择一个,作为流水线寄存器E的值valA。包括这个块是为了减少要携带给流水线寄存器E和M的状态数量。在所有的指令中,只有cal1在访存阶段需要valP的值。只有跳转指令在执行阶段(当不需要进行跳转时)需要valP的值。而这些指令又都不需要从寄存器文件中读出的值。因此我们合并这两个信号,将它们作为信号valA携带穿过流水线,从而可以减少流水线寄存器的状态数量。这样做就消除了SEQ(图4-23)和SEQ+(图4-40)中标号为“数据”的块,这个块完成的是类似的功能。在硬件设计中,像这样仔细确认信号是如何使用的,然后通过合并信号来减少寄存器状态和线路的数量,是很常见的。

预测下一个PC

如果取出的指令是条件分支指令,要等到几个周期后也就是执行阶段之后,我们才能知道是否要选择分支。为了提高效率,猜测分支方向并根据猜测开始取指的技术称为分支预测。

PIPE-的取指阶段,如图4-41底部所示,负责预测PC的下一个值,以及为取指选择实际的PC。我们可以看到,标号为“Predict PC”的块会从PC增加器计算出的valP和取出的指令中得到的valc中进行选择。这个值存放在流水线寄存器F中,作为程序计数器的预测值。标号为“Select PC”的块类似于SEQ+的PC选择阶段中标号为“PC”的块(图4-40)。它从三个值中选择一个作为指令内存的地址:预测的PC,对于到达流水线寄存器M的不选择分支的指令来说是valP的值(存储在寄存器M_valA中),或是当ret指令到达流水线寄存器W(存储在W_valM中)时返回地址的值。

流水线冒险

当相邻指令间存在相关时会导致出现问题。这些相关有两种形式:1)数据相关,下一条指令会用到这一条指令计算出的结果;2)控制相关,一条指令要确定下一条指令的位置,例如在执行跳转、调用或返回指令时。这些相关可能会导致流水线产生计算错误,称为冒险(hazard)。同相关一样,冒险也可以分为两类:数据冒险(data hazard)和控制冒险(control hazard)。我们首先关心的是数据冒险,然后再考虑控制冒险。

之所以会出现这些冒险,是因为我们的流水线化的处理器是在译码阶段从寄存器文件中读取指令的操作数,而要到三个周期后,指令经过写回阶段时,才会将指令的结果写到寄存器文件中。

避免流水线冒险

用暂停来处理流水线冒险

暂停是避免冒险的一种常用技术,暂停时,处理器会停止流水线中的一条或多条指令,直到冒险条件不再满足。让一条指令停顿在译码阶段,直到产生它的源操作数的指令通过了写回阶段,这样我们的处理器就能避免数据冒险。暂停技术就是让一组指令阻塞在它们所处的阶段,而允许其他指令继续通过流水线。但是会导致流水线暂停几个周期,严重降低整体的吞吐量。

 用转发来避免数据冒险

译码阶段从寄存器文件中读入源操作数,但是对于这些源操作数的写可能要在写回阶段才能进行。直接用旁路电路将提供到端口E的数据字作为操作数valb的值,就能避免暂停。为了充分利用数据转发技术,我们还可以将新计算出来的值从执行阶段传到译码阶段,以避免程序prog4所需要的暂停,如图4-51所示。在周期4中,译码阶段逻辑发现在访存阶段中有对寄存器rdx未进行的写,而且执行阶段中ALU正在计算的值稍后也会写入寄存器rax。它可以将访存阶段中的值(信号M_valE)作为操作数valA,也可以将ALU的输出(信号e _valE)作为操作数va1B。注意,使用ALU的输出不会造成任何时序问题。译码阶段只要在时钟周期结束之前产生信号valA和valB,这样在时钟上升开始下一个周期时,流水线寄存器E就能装载来自译码阶段的值了。而在此之前ALU的输出已经是合法的了。

避免控制冒险

当处理器无法根据取指阶段的当前指令来确定下一条指令的地址时,就会出现控制冒险。控制冒险只会发生在ret指令和跳转指令,而且后一种情况只有在条件跳转方向预测错误时才会造成麻烦。在下一个周期往执行阶段和译码阶段插入气泡,并同时取出跳转指令后面的指令,这样就能取消预测错误的指令。在出现特殊情况时,暂停和往流水线中插入气泡的技术可以动态调整流水线的流程。

异常处理

处理器中很多事情都会导致异常控制流,程序执行的正常流程被破坏掉。异常可以由程序执行从内部产生,也可以由某个外部信号从外部产生。我们的指令集体系结构包括三种不同的内部产生的异常:1)halt指令,2)有非法指令和功能码组合的指令,3)取指或数据读写试图访问一个非法地址。一个更完整的处理器设计应该也能处理外部异常。我们把导致异常的指令称为异常指令。

在一个流水线化的系统中,异常处理包括一些细节问题:

首先可能同时会有多条指令引起异常。基本原则是:由流水线中最深的指令引起的异常优先级最高。比如访存阶段的异常应该比取指阶段的异常优先级高,所以只向操作系统报告这个异常。

第二个细节就是,当取出一条指令,开始执行时导致了异常,后来由于分支预测错误,取消了该指令。

第三个细节问题的产生是因为流水线化的处理器会在不同阶段更新系统状态的不同部分。有可能会出现这种状况,一条指令导致了一个异常,而他后面的指令在异常完成前已经改变了部分状态。

一般地,通过在流水线结构中加入异常处理逻辑,我们既能够从各个异常中做出正确的选择,也能够避免出现由于分支预测错误取出的指令造成的异常。这就是为什么我们会在每个流水线寄存器中包括一个状态码stat。如果一条指令在其处理中于某个阶段产生了一个异常,这个状态字段就被设置成指示异常的种类。异常状态和该指令的其他信息一起沿着流水线传播,直到它到达写回阶段。在此,流水线控制逻辑发现出现了异常,并停止执行。

为了避免异常指令之后的指令更新任何程序员可见的状态,当处于访存或写回阶段中的指令导致异常时,流水线控制逻辑必须禁止更新条件码寄存器或是数据内存。在上面的示例程序中,控制逻辑会发现访存阶段中的pushq导致了异常,因此应该禁止addq指令更新条件码寄存器。