Li Jiaheng's blog
4803 字
24 分钟
处理器原理(2) 流水线原理
WARNING

这是《深入理解计算机系统》(CS)的笔记,彼时还未上CO,OS等计算机系统基础课,存在理解不到位与错误之处.
处理器体系结构的内容都记录地非常简略…

只涉及一些原理性的论述
如果一个周期内只执行一个指令,则这个指令在执行的每一步骤都只会调用一部分硬件,而剩下的部分处于待机状态,造成浪费,性能释放不全。采用流水线设计同时执行多个指令,提高效率。

一、流水线原理#

之前已经将所有指令的执行分为若干步骤(如Y86的 取指 译码 执行 访存 写回;合理的步骤阶段设计十分重要,是流水线的基础),在各个步骤中间加上流水线寄存器,将每个步骤得到的结果和系那个管数据完整地存放在流水线寄存器中,在下一步(时钟周期到上升沿时更新流水线寄存器并且输出,这输出就成为了新阶段的输入)进入下一阶段继续进行。
之前设计阶段时用了许多通用的“变量”表示命名,比如valA valB是ALU的两个输入、它们可能是指令本身指定的常值可能是从寄存器读出来的,valE是ALU的输出,valM是从内存中读出的东西,valP是PC增加器自动计算顺序下一条指令后的结果。这样的命名有助于弄清楚指令执行过程中的所有值是从哪里来的,这对于后续处理数据冒险等十分重要。

PS:  
这些量在前面加个前缀表示所处位置,如对于valE:  
e_valE 表示在执行阶段中,正在电路里面而非存入流水线寄存器的ALU计算结果。它是执行阶段的输出。  
M_valE 表示执行阶段结束后放入了下一个流水线寄存器的值,它是下一阶段(访存)的输入。  
小写表示在线路中,大写表示在对应的流水线寄存器里。  
本阶段的流水线寄存器在本阶段“后面”,里面的值是本阶段的输入。本阶段的输出在下一个阶段的流水线寄存器中。  

另外,值得一提的是,由于取出一个指令后必须立刻确定下一个指令在哪,对于程序计数器的更新必须提前,在取出一个指令时就立刻把代码地址放入程序寄存器。(此时是pred valP,表示预测值,当然大部分情况下它是准确的,如果没有条件跳转等有不定跳转选项的)

二、数据冒险#

1)数据冒险是什么#

很容易想到可能有如下情况:

/* Y86指令 */  
/*1*/ irmovq $3 %rax  
/*2*/ irmovq $10 %rdx  
/*3*/ addq %rax %rdx  

这样,addq指令的两个值都直接受前两个指令的影响。在流水线工作中,当addq执行到译码阶段,需要从寄存器读取值的时候,指令1才在访存阶段,而指令2只在执行阶段,它们都还没到写回阶段,将相应的值放到对应寄存器中,这样addq读到的值是错误的。这就构成了数据冒险。

可能有如下几种数据冒险,需要根据指令集细致地穷举考虑:
和程序员可见状态一一对应

  • 程序寄存器与内存
    和存储的访问与写入有关,很可能这一条指令要读的存储就是前面指令要写的存储,而前面的指令还未进行到将对应值放到对应寄存器的阶段,这样就造成了冒险。
  • 程序计数器
    有时候无法直接确定下一条指令在哪,是不是顺序下一条,比如条件跳转指令,比如ret.,造成程序计数器冒险(也是控制冒险)
  • 条件码寄存器
    条件码只会在执行阶段(有用ALU进行计算的阶段)更新,而它可能在其他阶段要被访问,可能会有冲突。(Y86不会,Y86生成判断控制信号Cnd也在执行阶段,这时候上一个指令的执行阶段一定完成了。)
  • 状态寄存器
    一般让每个阶段都准确跟随相应产生的状态码,能够有效避免问题。

2)数据冒险的解决方案#

1、暂停(stalling)
遇到可能冲突的指令,就把后面的指令暂停,直到所需要的前序操作完成。(称为加入气泡bubble)。截停一个指令,就是让当前指令所在的阶段之前的寄存器值不更新。
需要相应的在控制逻辑上的修改。

2、转发data forward(旁路bypassing)
这个更普遍。基于一个事实:当所需要的正确值计算出来,往往在下一个阶段(周期)开始的时候才会写入正确的寄存器。可以在所需值计算出来的时候就另外引线将这个值引到下一个指令的某阶段,再通过相应的控制逻辑,实现不停止的解决冲突。
比如上面的冲突例子,当 addq 进行到译码阶段需要从寄存器中读值时,第1个 irmovq 在访存阶段,已有所需值valE1,第2个 irmovq 在执行阶段,同一个周期内也能生成所需的valE2。所以建立从执行阶段ALU输出到译码阶段的valA、valB的旁路,以及访存阶段valE到译码阶段valA、valB的旁路,加上相应的控制逻辑,就能无停滞地解决冲突。
通过小心地设计指令实现的阶段,指令要从存储中读值(读的值和更新不及时是大多数冲突的来源)只在很少的(尽量是一个)阶段,这样旁路的目的可以比较容易确定。
注意 能通过单纯的转发策略解决的冒险,一般使在当前指令需要从存储中读值时,前面指令实际上已经把要更新到对应存储的值计算出来了,可以直接把这个值通过加线路、改控制引过去。但对于需要这个值时前序指令尚未进行到把需要更新的值计算 / 读取出来的时候,单纯的转发是不行的,必须 暂停+转发,先等前序指令计算 / 读取出需要更新的值,再转发并继续。

当我们试图转发的时候,必须只能转发流水线寄存器中的值。比如转发ALU的计算结果,只能等它传到流水线寄存器后再转发这个结果,而不能直接在ALU输出线路连一条线到上一个阶段!!这会导致一些时钟频率的问题!
比如,上一个改变a0,下一个add指令用a0,上一个的结果在ALU出来后不能立刻转发,而等它进入下一阶段流水线寄存器的时候才转发,转发到执行阶段ALU对应的输入端口!

/* Y86 */  
mrmovq 0(%rdx) %rax  
addq %ebx %eax  

这个例子里,当 addq 进行到译码阶段,mrmovq 只进行到执行阶段,要在访存阶段才能读取到需要更新入%rax的值,所以至少需要暂停一个周期,等 mrmovq 在访存阶段读到了值,再转发到 addq 的执行阶段。

其它:有时候不同阶段的指令会同时触发对本阶段的转发,这时候接受哪个转发?转发源的阶段越早,优先度越高。考察如下指令:

irmovq $3 %rax  
irmovq $10 %rax  
addq %rax %rax  

在addq执行到译码阶段时,第一个movq执行到访存阶段,有M_valE触发转发;第二个movq执行到执行阶段,有e_valE触发转发,这时候接受阶段更早的movq,这样读到的才是最近更新的值10.

将流水线寄存器锁定(写使能端口置0),可以阻断某个阶段向下一个阶段传播,

3、避免控制冒险
对于更新PC的冒险,主要是条件跳转语句与ret。

(1)条件跳转
为了使流水线进行下去,可以猜测选择一个条件分支放入计数器中,直接当成下一个进行。这就是分支预测。
简单的逻辑分支预测,比如遇到条件跳转,就直接猜测要跳转、或者猜测不跳转,直接顺序下一个。研究表明,前者正确率大概60%,后者40%。还有,当跳转目标的代码地址低于当前地址时,就跳转,否则不跳转,这正确率大概在65%,它基于一个事实,循环语句的跳转大多是这样:

loop:  
	expr...  /* lower addr */  
test:  
	expr...  
	jXX loop;  

而循环一般进行很多次。
人们正在设计成功率更高的分支预测策略。

猜错时要清空后续错误指令进行的阶段。这里很体现阶段设计的精妙
jXX语句在执行阶段就能得到判断是否要跳转的 Cnd 标识(也就是在执行阶段知道正确跳转目标),而后面只进行了两个指令,后一个在译码阶段,后两个在取指阶段,都只是读取阶段,没有对程序员可见状态进行任何更改,即没有产生任何实际上的影响。这时候只要锁住译码阶段向执行阶段的流水线寄存器的更新,同时立刻将PC寄存器中的下一个地址更新为正确地址,然后等正确指令执行到译码步骤,将错误指令挤掉就可以了。非常精妙。

(2)ret指令
这个指令无法进行任何预判,因为从栈中读取的指令地址可能是任意的。遇到ret指令,就把ret后面的指令截留,在下一个周期(ret进入译码阶段后)将D寄存器输入使能端置0。一直到ret进入写回阶段,得到读取的代码地址W_valM,把它接入取指前的Sel_PC逻辑的输入端,让这个W_valM成为新PC,再解锁继续接下来的步骤。
注意 从SEQ+到PIPE的硬件结构中,就没有显示的PC程序计数器寄存器 了,也就是没有单独的地方保存下一条要执行的指令的地址,而是直接作为线路上的值接入内存 的读取地址端。指令保存在内存中,不会改变,而取指执行的是读操作,只要输入读取地址即可,不用等上升沿。所以将ret的W_valM接入指令寄存器的地址输入端口,就能随之取出一条指令。

处理各种冒险,需要细致全面的穷举、找特殊情况等。

三、异常状态#

状态有:正常、地址异常(读写内存时发生),执行了停止指令等特殊指令,非法指令,等。
实际上,异常状态由stat标识,CPU只会根据写回阶段的流水线寄存器中的stat值来确定状态并进行异常处理(这是一条指令执行完毕后产生的最终状态),并决定是否调用异常处理过程,

异常处理需要面对如下可能的“乌龙”:
1)在某个周期内,多个阶段的同时出现一场状态码。此时以阶段最深的指令的异常最优先。在处理异常状态时,只处理写回和访存状态的异常 可能就做到了这一条。
处理 m_stat W_stat。可能是因为访存、写回阶段可能产生地址错误会严重修改程序员可见状态,必须这时候就处理。而前面的,取指、译码不改变程序可见状态,而执行阶段只改变条件码CC,容易重置。(取指阶段可能地址错误,但不改变程序员可见状态,它只读)。

2)当非写回状态的发生异常,比如非法的内存访问地址等,如果等到最后的写回和译码阶段才处理这个异常,则这个错误已经对程序员可见状态产生了影响??或者已经导致程序出问题(比如访问非法地址等)
读地址出错似乎没关系,只要不改变就行
在Y86中,唯一可能出现这种情况的是计算指令,它会在执行阶段修改CC,但是异常不会在执行阶段处理(为什么不提前一个阶段? 可能的原因:涉及反悔,虽然 jXX 在执行阶段生成Cnd,但通过e_Cnd不容易直接判断是否进入预测错误处理过程,因为Cnd的稳定可能在这个周期后半部分,或许要用的是M_Cnd,如果这样的会,要反悔的异常指令可能已经到了执行阶段,而它不应被处理——当然可能没这回事。。。)
异常产生后(注意不是被处理后),异常指令以及其后面的指令不应对程序员可见状态进行改变。即使这个异常指令不是在写回阶段异常的(即这个异常未流到写回阶段,未处理),但这个异常指令后面的指令都暂时“无效”。

3)异常的“反悔”。当遇到条件跳转语句时,jXX指令后面的那些是预测来的,可能出错,如果是错误预测的指令出现了异常,这个异常不应被处理(如前所说,Y86下知道预测错误时,错误指令未对程序员可见状态产生影响),而应该被卡着直到正确指令将错误指令挤掉。
当正确指令中又出现异常时,它同样能“冲掉”错误预测指令的异常。

本书的Y86中,异常处理方法就是简单的终止CPU halt,而实际上探测到异常后会调用异常处理函数。

四、实现中的一些内容#

可以看出,流水线除了正常完成指令本身的工作外,还要处理各种冒险、异常等。
数据转发不需要停滞流水线,可以无论如何都拉一条线从可能转发的各个阶段拉到译码阶段(唯一(伪)数据转发接收阶段)和取指阶段(比较特殊,基本上和转发到译码阶段的不是同一类,这是指令地址的问题而不是取值的问题),这样无论如何其实数据都被转发到了目标阶段,只需要在目标阶段(译码、取指)修改相应的数据选择逻辑即可。数据转发工作被并入了普通流水线操作的一部分

流水线有可能产生如下异常:不得不暂停(stall of bubble),或者发生应该处理的异常,把这些称为流水线的特殊情况,不能把这些特殊情况归入流水线内部行为(就像数据转发那样),而必须要额外的流水线控制逻辑。

补充:暂停(stall)与插入气泡(bubble)的区别。  
  虽然它们都是让某个指令后面的指令停下来,但是有区别。  
  首先,暂停和插入气泡都是对某一流水线寄存器的操作,它们都要截停后续指令,  
所以首先要锁定这个寄存器使之不接受输入,这样后面的指令就上不来,被截停。  
  插入气泡和暂停的效果是针对被操作流水线寄存器的阶段中的内容而言的(注意流水线寄存器与阶段:D-->d)  
当对D流水线暂停,保留D寄存器中原有值,这样阶段d的状态会被保留。  
当对D插入气泡,则将D中的值改为nop指令对应的“空值”,形成气泡。d阶段原有状态被清空。  
*例如*  
jXX 指令到执行阶段发现预测错误,要冲掉后续跟着的两条位于译码和取指阶段的指令,  
则需要在本周期把正确地址发送到pred PC并使之为下一条指令地址,  
然后对E寄存器 插入气泡 处理(下一周期时,e阶段中的jXX指令完好进入M阶段,而E阶段成了气泡nop)。  
这样下一周期中,原本译码阶段的错误指令被原本取指阶段的指令取代,而取指阶段进入了正确指令;  
再下一个周期,错误指令被正确指令挤掉,这时候正确指令到 jXX 中有两个气泡。  
*又比如*  
加载/使用冒险,要把处于译码阶段的指令停一周期,则E寄存器冒泡、D寄存器暂停、F寄存器(取指前的特殊pred PC寄存器)暂停。  

发现特殊控制条件#

[重要!!]
仔细梳理需求指令中的任何组合,发现涉及到暂停、气泡、异常处理的特殊情况(包括指令与指令组合),并分类讨论。

Y86中,不得不暂停 / 气泡的有:
ret加载使用冒险(下一条读寄存器的值被上一条指令从内存中取值更新,当下一条指令译码时上一条指令才执行,未取得需求值,无法数据转发,必须恰当地在各个处理器间冒泡、暂停)、错误预测分支
异常情况:x_stat,其中只有一种可能在处理错误前不该执行的指令改变了程序员可见状态:一条指令异常,到M才处理异常,而后面紧跟一个计算指令,它已经到了执行阶段,改变了条件码,这时候在处理异常的时候也要把条件码重置。

一条条设计对应的每个流水线寄存器的暂停、冒泡情况,尤其小心各种组合。比如预测分支错误 + ret,两者的流水线控制逻辑可能形成冲突。
一条原则:特殊处理冲突时,优先处理阶段更深的指令的特殊处理。 上面的例子中,优先处理预测分支错误,这样ret就被冲掉了。

控制逻辑的实现#

对于每一个流水线寄存器单独考虑其控制逻辑,即设定流水线正常工作(无特殊操作)、暂停、冒泡、异常下重置(某些可见状态如CC)的对应条件。必须全面! 就形成了流水线控制逻辑。

处理器原理(2) 流水线原理
https://namisntimpot.github.io/posts/computersystem/处理器体系结构/2流水线原理/
作者
Li Jiaheng
发布于
2022-08-15
许可协议
CC BY-NC-SA 4.0