zoukankan      html  css  js  c++  java
  • 流水线中的精确异常

    流水与精确异常

    我们为什么需要精确异常?

    1. 冯诺依曼结构ISA的语义
    2. 帮助软件调试
    3. 使得异常恢复和进程重启更容易
    4. 可以在软件中加入trap

    多周期与流水线

    因为并不是所有指令的执行时间都是一样长的,能不能在流水线中实现多周期的思想,即用多个不同的功能单元花费不同数量的时钟周期。而在这些功能单元中指令的执行可以流水也可以不流水,这样就必须保证指令序以及确定精确异常,因为在某个阶段发现异常时,其之后的指令已经开始执行了,那么就需要将微体系结构的状态还原到异常发生之前的那个状态。

    上图展示了我们之前的思路,在传统的单(指令执行)周期流水线中,当FMUL在EX阶段时,ADD不能处于EX阶段,进一步,ADD和FMUL的EX阶段的时钟周期必须一致,但是实际上ADD的EX阶段并不需要那么长的时间,这样就导致指令处理时间较长。那么如果进行多周期执行,当FMUL和ADD不存在相关时,ADD可以使用EX阶段不同的功能单元,从而不需要等待FMUL指令EX阶段的完成。

    看起来不错,但是如果第一个FMUL在WB阶段出现异常了呢?这样除了需要处理异常外,我们必须消除后续已执行的指令所带来的对体系结构状态的影响,否则指令的序就出现了问题,这个系统就不再是一个冯诺依曼结构的系统。

    异常与中断

    必须指出的是,在不同的语境中,异常与中断之间会发生混用。从以下几个方面区分:

    起因:

    • 异常:系统内部产生,作用于运行的线程。
    • 中断:系统外部产生,例如外部的IO操作,作用于运行的线程。也可以理解为外部产生的异常。

    处理的时机:

    • 异常:一旦检测出立即处理,否则很可能会导致线程运算错误。
    • 中断:方便的时候,并不需要马上响应(除非中断的优先级非常高,蓝屏等)。

    处理的上下文:

    • 异常:进程参与处理。
    • 中断:系统参与处理。

    异常的精确与非精确:

    • 精确异常:这类异常可能不会导致进程终止,所以为了保证指令序,需要记录并返回未发生异常之前的体系结构状态。
    • 非精确异常:这类异常会导致进程终止,所以并不需要返回到之前的体系结构状态。

    精确异常

    当准备处理异常或中断时,体系结构状态必须是一致的,所以这就需要:

    1. 所有之前的指令必须回收(回收:指令执行完毕并更新体系结构状态)。
    2. 之后的指令一律不得回收,即不可以更新体系结构状态。

    一点个人的理解是,精确异常属于为了指令吞吐(流水)做出的改进所带来的额外的问题,并不属于进程本身产生的异常。所以应该尽可能地减少其影响。

    流水中的精确异常

    思路零


    使每个操作花相同的时间。

    这样虽然保证了精确异常,但这也是我们想要避免的情况。

    重排序缓冲

    思路:乱序执行指令,产生体系结构状态可见的结果之前进行重排序。

    基本实现:主要借助于ROB(ReOrder Buffer)来实现。在指令译码阶段在ROB中预留一个entry,在指令执行完成(产生结果但不更新体系结构状态)时,根据指令entry将结果写入ROB中相应的位置,当指令成为ROB中最旧的指令,已经产生结果且不出现异常时,将结果输出到寄存器堆或内存,完成体系结构状态的更新。可见ROB可以通过队列数据结构实现。必须指出的是,具体到实现阶段是十分复杂的。

    下面是一个ROB entry的表项:

    • V:有效位,表示结果是否准备好
    • DestRegID:写回寄存器ID
    • DestRegVal:写回寄存器的值
    • StoreAddr:内存的修改地址
    • StoreData:内存的修改值
    • PC:指令寄存器,可以标识指令的新旧,也可以通过PC得到指令,进而得到指令类型等信息
    • Valid bits:表示指令结果是否是有效的
    • Exc:是否出现异常

    应用了重排序之后的指令执行过程如下图所示:

    其中R表示写入ROB中。

    但是现在就出现了这样一个问题,就是如何处理数据相关?因为指令产生的结果并没有写回寄存器,而是存在ROB中,那么如果这时另一条指令需要读这个写回寄存器的值时就会出现问题,因为从程序语义上来说,要读的值在ROB中,当前寄存器堆中的值是一个无效值,因为从顺序上来说他已经被覆盖了,这就会出现错误。

    那么如何解决这类问题?可以考虑数据转发,而数据转发涉及到很多的细节问题,因为需要访问ROB来获取尚未写回的结果,两种典型的方式一是同时访问ROB和寄存器堆,这需要用内容寻址寄存器来实现ROB;二是间接寻址,这种方式需要在寄存器堆中存储ROB entry的tag。这也就解释了为什么读后写和写后写不是真正的相关,因为使用ROB相当于对寄存器进行了重命名,因为不同的值存在ROB中不同的entry里,相当于扩充了寄存器堆的容量。

    那么关于寄存器重命名,上述办法并不是唯一的方式,还有比如从体系结构寄存器(汇编代码中的寄存器)ID转换为物理寄存器(微体系结构中真实存在的硬件寄存器)ID等等。

    ROB的tradeoff:

    • 好处:简单直接;ROB的重命名可以消除虚假相关。
    • 坏处:需要访问ROB来获取尚未写回的结果,增加了复杂性。

    历史缓冲(History Buffer)

    思路:指令执行完后直接更新寄存器堆,但是当出现异常时撤销那些更新。

    基本实现:当指令译码时,预留一个HB条目,当指令执行完毕时,将目标地址中的旧值留在HB中,当指令是HB中最旧的一条且未发生异常时,丢弃该HB条目,当指令是HB最旧的一条并且出现异常,那么将HB中的旧值依次写回体系结构状态。

    好处:寄存器堆中存在最新的值,而HB的访问不在关键路径上。

    坏处:需要读目的寄存器的旧值;在异常时需要回滚HB,增加了异常的处理时延。

    未来寄存器堆(FF)+ROB

    思路:维护两个寄存器堆(投机的和体系结构的),指令执行完毕后立即更新投机寄存器堆;使用ROB维护一个体系结构的寄存器堆,投机的寄存器堆可以保证指令访问最新的数据。

    投机寄存器堆也叫前端寄存器堆,体系结构的寄存器堆也叫后端寄存器堆。

    好处:不用从ROB中读取值

    坏处:使用了多个寄存器堆,出现异常时,需要从堆向堆中复制数据,这样会带来更大的异常处理时延。

    检查点

    分支预测错误相当于一个“异常”,那么在恢复时并不需要分支指令成为最旧的指令。 因为分支预测错误相比于真正的异常来说是很常见的,所以我们希望分支预测错误的处理速度要尽可能快,思路是当分支取指时对前端寄存器状态设立检查点,保存现场,并且对比分支指令旧的指令产生的状态保持更新。这样可以使分支后正确的下一条指令能够在分支预测错误被解决后立即执行。

    在分支译码时,复制未来寄存器堆并关联到分支,当指令产生寄存器值更新(FF)的时候,所有晚于该指令的未来寄存器堆的检查点更新状态;当分支预测错误被检测出来,当错误被解决时,恢复被错误预测的分支的未来寄存器堆ckpt(checkpoint),清空所有晚于分支的指令,释放所有晚于分支的检查点。

    上面的句子不像人话,那么我们画图拆解一下:

    好处:响应快速。

    坏处:保存ckpt带来了大量的额外开销。

    存储器的精确异常

    我们之前所说的都是关于寄存器的,那么怎么处理存储器的异常呢?这也是一个十分复杂的问题,这里我们只讨论简单的思路,如果想要撤销store,我们可以在ROB中添加字段记录store的地址,当store指令是最旧的指令时才写回存储器,但是这也带来了转发的困难,因为这时load指令也需要读ROB。

    那么我们如果把ROB中那些store指令的entry单独拿出来,就组成了一个非常重要的部件,叫store buffer。我们将store指令按序放到store buffer中,当store指令译码时,在store buffer中分配一个条目,当store指令地址和数据都可用时,将地址和数据写到store buffer的entry中,当store指令是最旧的指令时,更新存储器地址(cache)和存储的数据。

    我们为什么需要store buffer?因为如果使用ROB且最旧的指令是store指令时,如果当前的store指令无法提交结果,或者提交时间很长(比如store的数据不在cache中,这时就需要访存),这时候全部流水线就会停下来等,我们想尽可能减少这种停顿的情况,所以使用store buffer作为二级缓冲,一条store指令写到store buffer中就算是完成,而如果出现store buffer无法提交的情况,流水线中不存在相关的指令还是可以继续进行。

    当然,上述想法都是较粗略的,涉及存储器的精确异常一定还存在着很多更细致的问题,作者水平有限,拾人牙慧,权当作抛砖引玉。

  • 相关阅读:
    Channel 9视频整理【6】
    Channel 9视频整理【5】
    Channel 9视频整理【4】
    Channel 9视频整理【3】
    SQL_Server_2008完全学习之第二章管理工具
    SQL_Server_2008完全学习之第一章入门知识
    【转】大话模拟退火
    Unity使用DLL库
    常用纹理和纹理压缩格式
    Gamma空间和线性空间
  • 原文地址:https://www.cnblogs.com/LuoboLiam/p/13411709.html
Copyright © 2011-2022 走看看