zoukankan      html  css  js  c++  java
  • 计算机组成与设计(十)—— 流水线的冒险

    冒险

    流水线技术之所以能提高性能 究其本质是利用了时间上的并行性,那它让原本应该先后执行的指令在时间上一定程度的并行起来,然而这也会带来一些冲突和矛盾,进而可能引发错误。

    冒险(Hazard:在流水线中我们希望当前每个时钟周期都有一条指令进入流水线可以执行。但在某些情况下,下一条指令无法按照预期开始执行,这种情况就被称为冒险

    冒险分为三种:

    • 结构冒险:如果一条指令需要的硬件部件还在为之前的指令工作,而无法为这条指令提供服务,那就导致了结构冒险。(这里结构是指硬件当中的某个部件)
    • 数据冒险:如果一条指令需要某数据而该数据正在被之前的指令操作,那这条指令就无法执行,就导致了数据冒险
    • 控制冒险:如果现在要执行哪条指令,是由之前指令的运行结果决定,而现在那条之前指令的结果还没产生,就导致了控制冒险。

    结构冒险

    示例一:如果指令和数据放在同一个存储器中,则不能同时读存储器

    解决方案一:我们有一个方便又简便的方法,即流水线停顿(stall),产生空泡(bubble)。

    虽然流水线停顿能用来解决各种冒险,但它的效率低下,应尽量避免。

    解决方案二:在存储器中设置单独的指令高速缓存和数据高速缓存。(要强调的在计算机中主存储器也就是内存是统一存放指令和数据的,这也是冯诺依曼结构的要求,只是在CPU当中 的一级高速缓存会采用指令和数据分别存放的方式)

    示例二:如果读寄存器和写寄存器同时发生,如何处理?

    解决方案:前半个周期写,后半个周期读,并且设置独立的读写端口。

    相对来说寄存器堆的读写速度比较快, 我们假设读或者写寄存器的延迟为100ps,而其他部件比如说ALU的延迟就就比较大,视为200ps, 那么我们就可以在前半个时钟周期用于完成寄存器堆的写,后半个时钟周期用来完成读操作,并且在寄存器堆上设置独立的读写口。这样就可以在一个时钟周期内同时完成了读和写的操作。
     
    要设计一个新的处理器,结构冒险仍然是我们优先要考虑并解决的问题。但结构冒险在设计处理器时就考虑并解决好了,我们在使用时就不必考虑。
     

    数据冒险

    示例一:一条指令需要使用之前指令的结果,但是结果还没有写回。

    软件解决方案:插入nop指令

    但这种方法有个很大的问题,首先,插入nop指令的个数与流水线的结构相关,例如在5级流水线上正确运行的程序,在8级流水线上就不能正确运行。其次,我们希望对软件屏蔽硬件尽可能多的细节。

    那么既然两条nop指令就能解决的问题,我们可以尝试在硬件上完成相同的工作。

    解决方案一:流水线停顿,增加气泡

     解决方案二:数据前递(Forwarding

    t0在EX阶段就被计算出,所以可将它送到下一条指令ALU的输入,而不需要添加气泡。

    在电路上的实现如下:在过600ps后,t0的值会被保存到EX/MEM这个流水线寄存器中,与此同时,加法指令正在执行,它需要将t0的值传到ALU的输入,显然它直接从t0寄存器读的值不是最新的,最新的在访存阶段的连线上,我们从硬件连线上把这个信号引回来,作为ALU的输入端。是否使用前递的信号,我们需要根据是否出现数据冒险,来控制一个二路选择器。

    当然加法指令,也有可能是第二个源操作数使用t0的值,所以前递信号也要连接到第二个输入端,同样这里也要添加二路选择器。

    这样的方式就被成为前递。它还有个名称叫作旁路。那从根本上来说,前递和旁路指的都是这件事情。只不过是观察和描述的角度不同而已。前递是从指令执行顺序的角度来描述的,而旁路则是从电路的结构角度来描述。 本来前一条指令应该将运行的结果写入到寄存器堆,然后再交给后一条指令使用,而我们现在搭建来一条新堆通路,相当于绕过了寄存器堆,直接进行了数据堆传递,所以从硬件时限的角度来看,这是一个旁路。那这就是前递和旁路的关系。 

     
    那我们进一步来看,其实不仅仅在这个点可以建立旁路,我们在下一个流水级也可以建立旁路。
     
    示例二:从MEM/WB阶段前递到ALU的情况

    所以,再添加一条旁路

     示例三:访存指令出现数据冒险

    这个单纯的前递也无法解决(前递的箭头方向正向下或者右下)

    解决方案:流水线停顿+数据前递

    控制冒险

    示例:尚未确定是否发生分支,如何进行下一次取指

    解决方案一:流水线停顿,添加气泡

    前面说过添加气泡效率很低,并不是一种较好的方法。我们可以从一下两方面考虑:

    一、假设分支不发生

    例如,假设经过beq指令分支不发生,最坏情况是其实分支总是发生,所以执行两条错误的lwsw指令,又执行两条正确的指令,这样导致50%的性能浪费。

     

     这也是因为转移指令本身和流水线的模式是冲突的,因为转移指令会改变指令的流向, 而流水线则希望能够依次地取回指令,将流水线填满。那如果这种情况是非常罕见的,也许我们还可以容忍,但实际上转移指令是非常常用的指令。

    二、缩短分支延迟

    转移指令的分类:

    • 直接转移:j targetbeq rs,rt,imm两种
    • 间接转移:jr t0

    无条件直接跳转(j target

    这种情况跳转是确定发生的,且跳转地址在取指阶段就能得到,所以流水线不停顿。

    这条指令的编码当中,带有一个26位的立即数,这个数就是要转移的目标地址的主体部分, 但是我们的目标地址应该是32位的,所以还差6位,在差的6位当中,低两位我们用0补上,因为目标地址肯定是四字节对齐的,地址的低两位肯定是0,然后还缺4位,我们通过当前的PC寄存器计算而得。先将PC寄存器的内容加4,得到的这个32位数,取其高4位,和26位地址以及最低的两位的0连接起来,构成了一个32位的数,这就是转移的目标地址。这些工作和取指可在一个时钟周期内完成。

    无条件间接跳转(jr rs

    在译码阶段得出跳转地址,流水线需停顿一个周期。

    条件跳转(beq rs,rt,imm16

    不做优化,需要根据EX阶段的结果,判断是否跳转,需要等待2个周期。

     而实际上,比较两个数是否相等是十分简单的,只需在译码阶段对寄存器的两个输出进行比较,这样流水线停顿周期缩减为1个周期

    三、 延迟转移技术

    就是调整指令的顺序,将一定会执行的指令放在分支指令后面,这样流水线不停顿。注意,不能改变这段代码原来的意义。

    例如:可以将xor指令放到beq后面,经过xor指令后,beq不用等待正好可以执行,但是不能将addi或subi放到beq后面,因为beq指令需要这两个。

    参考资料:https://www.coursera.org/learn/jisuanji-zucheng/lecture/fWOzV/606-kong-zhi-mou-xian-de-chu-li

  • 相关阅读:
    Go 语言简介(下)— 特性
    Array.length vs Array.prototype.length
    【转】javascript Object使用Array的方法
    【转】大话程序猿眼里的高并发架构
    【转】The magic behind array length property
    【转】Build Your own Simplified AngularJS in 200 Lines of JavaScript
    【转】在 2016 年做 PHP 开发是一种什么样的体验?(一)
    【转】大话程序猿眼里的高并发
    php通过token验证表单重复提交
    windows 杀进程软件
  • 原文地址:https://www.cnblogs.com/lfri/p/10053598.html
Copyright © 2011-2022 走看看