超标量处理器设计——第十章_提交


参考《超标量处理器》姚永斌著

超标量处理器设计——第十章_提交

10.1 概述

  • 一条指令到达流水线提交阶段不一定代表该指令一定是正确的.
  • 由于分支预测失败和异常的存在, 处于完成状态的指令很可能还会从流水线中抹除
  • 只有在这条指令之前进入流水线的所有指令都退休了, 且该指令也处于完成状态, 它才能退休离开流水线
  • 提交阶段的一个重要任务就是处理精确异常, 所谓精确异常, 就是异常之前的指令都已经完成, 其后的指令都不应该改变处理器状态.
  • 一个N-way超标量处理器, 每周期最少可取N条指令送人流水线, 所以每周期至少也要将N条指令退休, 才能保证流水线不堵塞

10.2 重排序缓存

10.2.1 一般结构

  • Reorder Buffer, ROB, 本质上是一个FIFO

  • ROB的容量决定了流水线中最多可同时执行的指令个数

  • ROB表项的内容包括:

    1. Complete : 表示指令是否执行完毕
    2. Areg: 指令在原始程序中指定的目的寄存器, 以逻辑寄存器形式给出
    3. Preg: 指令的Areg经过寄存器重命名后对应的物理寄存器编号
    4. OPre: 指令的Areg被重命名为Preg之前, 对应的旧的Preg, 当指令发生异常进行状态恢复时会用到
    5. PC: 指令对应的PC
    6. Exception: 如果发生异常, 会将类型写到这里, 当指令退休时会处理他
    7. Type: 指令类型, 退休时根据不同指令有不同的动作
  • 分发(Dispatch)阶段, 指令会按照进入流水线的顺序写入ROB, 同时ROB中的complete位置0

  • 指令在ROB中的编号会跟随指令流动

  • 当指令变为ROB中最旧的指令, 可以用header pointer来表示, 如果他的complete状态位=1, 则这条指令可以退休了. 如果其exception部分=0, 则他可以顺利离开流水线, 否则需要启动异常处理程序

  • 下面是ROB的工作原理展示:

    • 指令i2,i3,i4可以同时退休

10.2.2 端口需求

  • 如果要实现上述三条指令同时退休, 需要对ROB中最旧的指令开始, 连续4个complete信号进行判断, 如果某个为0, 那么后面的所有指令都不允许在本周期退休.

  • 可见,对于4-way超标量处理器来说, ROB至少要支持4个读端口, 但流水线的其他阶段对ROB也有端口需求, 例如采用ROB进行寄存器重命名的方法中:

    1. 寄存器重命名阶段需要从ROB读取四条指令, 需要ROB的8个读端口(假设指令都有2个源操作数)
    2. 分发阶段, 需要向ROB中写入4条指令, 需要4个写端口
    3. 写回阶段, 需要写入至少(issue width >= machine width)4条指令结果, 需要4个写端口
    4. 加上退休的读端口, 总共需要12个读端口和8个写端口! 这会导致多端口FIFO在面积和延迟上很难优化

10.3 管理处理器的状态

  • Architecture State : 指令集定义的状态
  • Speculative State: 内部状态, 例如物理寄存器, 重排序换成, 发射队列, Store Buffer等不见的值, 他们总是超前于Architecture State, 甚至可能有不正确的,这些状态不应该被外部看到, 而是单纯为超标量的并行化提供服务的.

10.3.1 使用ROB管理指令集定义的状态

  • Retire Register File (RRF)方法 : 逻辑寄存器真实存在, 当指令从ROB中退休, 就会将结果搬到逻辑寄存器中.

  • 本质上是将ROB作为物理寄存器, 一般都会使用数据捕捉结构进行发射.

    • 指令退休时会将结果搬到ARF中, 所以一个寄存器在生命周期可以存在两个地方, 一个是ROB, 一个是ARF
    • 如果用数据捕捉结构, payload RAM就会捕捉FU计算出来放到旁路网络上的寄存器值, 所有用到该寄存器的后续指令都可以用payload RAM中的值,而不需要关心此时该寄存器是在ROB还是ARF中
    • 当指令进行重命名时, 会访问RAT, 如果发现源寄存器已经被计算出来, 那么就可以直接从ROB或ARF中取值, 然后写到payload RAM中. 如果没被计算出来, 那就将该寄存器在ROB中的地址写到payload RAM中, 等待从旁路网络捕获该值:
  • 如果使用非数据捕捉方式进行发射?

  • 此时没有payload RAM, 指令被仲裁器选中后, 直接从需要的位置读取源操作数, 该位置信息会被记录在IQ中

  • 在重命名阶段, 通过读取RAT可以知道源操作数的位置, 之后将该位置信息随指令一起写入IQ

  • 当指令真正被唤醒时, 其操作数可能还在ROB中, 等他真正被仲裁电路选中时, 操作数可能已经搬到通用寄存器中了, 这个位置变动信息需要通知IQ. 所以需要IQ支持额外的写端口.

  • 所以用ROB管理ARF的方式与非数据捕捉式相性不合

10.3.2 使用物理寄存器管理指令集定义的状态

  • 只维护一个统一的物理寄存器堆(PRF), 指令集中定义的逻辑寄存器都混在这个寄存器堆中
  • 当指令被寄存器重命名后, 他的目的寄存器就和一个物理寄存器产生了对应关系, 但这个关系不被外部可见
  • 当指令的结果被计算出来时, 这条指令的状态变为complete; 等到指令退休时, 该指令的结果仍然会占据这个物理寄存器, 但是其状态会被标记为Architecture state
  • 后续指令也写到同一个目的寄存器, 且他也退休时, 才能将这个物理寄存器进行释放
  • 优点:
    1. 指令退休时, 不需要将指令结果进行搬移. 源操作数被确定在哪里之后, 就不会变化了, 便于低功耗实现.
    2. 在基于ROB的管理方式中, 需要从ROB中开辟空间存放指令结果. 但有很多指令实际没有目的寄存器, 造成ROB空间浪费. 而基于统一PRF的方法只对存在目的寄存器的指令分配空间, 所以占用的空间更小
    3. 基于ROB的管理方式需要大量读写端口, 严重拖累处理器速度. 采用统一PRF的方法则可以通过对PRF采用Cluster结构来减少过多的端口
  • 缺点:
    1. 寄存器重命名过程较为复杂, 需要一个额外的表格来存放哪些物理寄存器是空闲的, 且RAT的建立和释放都会比较复杂
    2. 当处理器外部需要访问ARF时, 由于他们混在PRF中, 所以无法直接进行分辨, 需要额外的表格表示哪些物理寄存器是Architecture state

10.4 特殊情况处理

10.4.1 分支预测失败的处理

  • 分支预测失败后, 处理器恢复的过程分为两个任务:

    1. 前端状态恢复(front-end recovery)
    2. 后端状态回复(back-end recovery)
  • 两者的分界点是寄存器重命名.

  • 恢复前端: 只需要将寄存器重命名之前的指令抹掉, 将GHR,BHR等进行恢复

  • 恢复后端: 将内部组件(IQ, StoreBuffer, ROB等)错误的指令都抹掉, 将RAT进行恢复, 还要释放错误指令占据的物理寄存器和ROB空间

  • 在基于ROB进行重命名的架构中进行状态恢复:

    • ARF真实存在, 每个寄存器在生命周期存在两个地方(ROB和ARF中)

    • RAT需要将这两个位置进行标记:

    • 只有一条指令在从ROB中退休的时候, 发现自身在RAT中也是最新的映射关系, 才能将RAT中对应的内容改为ARF状态. 如果某个逻辑寄存器被多条指令使用, 即使旧的指令退休, 也不能将RAT中该逻辑寄存器设置为ARF状态, 也就是说新的指令仍然使用ROB中的值

      • 可以在指令退休的时候用其目的寄存器来读RAT, 读取对应的ROB pointer, 如果发现和当前退休指令在ROB中占据的地址一样, 就表示这条退休指令就是最新的映射关系.
    • 分支预测失败恢复方案:

      • 这种架构中, 流水线中发生预测失败(执行阶段), 在分支指令之前的指令仍能执行, 所以需要停止取指, 让流水线继续执行, 这一步骤称为流水线抽干(frain out). 等分支指令之前进入的指令都退休后, 将流水线中的指令都抹除, 再将RAT所有内容标记为ARF状态.
  • 基于扩展的ARF进行寄存器重命名的方式:

    • 与基于ROB类似, 不赘述
  • 基于统一的PRF进行重命名架构中进行状态恢复:

    • 流水线中存在两个RAT, 一个在寄存器重命名阶段使用, 其状态是推测的, 称为前端RAT , 或Speculative RAT; 另一个是在提交阶段使用的, 退休的指令会更新该表,所以他永远是正确的, 称为后端RAT,Architecture RAT
    • 可以用后端RAT进行流水线状态恢复, 过程与上述类似, 也是进行流水线抽干, 在将剩余指令抹除, 不同之处是需要将后端RAT全部复制到前端RAT中:
  • Recovery at Retire: 实际上上述恢复方法都是一样的, 都是在退休的时候进行状态恢复. 这种方法如果遇到分支指令之前存在D-Cache缺失的情况,则会导致过打的惩罚时间.

  • 快速恢复: 使用Checkpoint可以快速进行状态恢复. 这种方法会在分支指令改变处理器状态前, 将处理器状态保存起来, 后面发现预测失败的分支时, 可以使用这条分支指令的编号将流水线中错误的指令都抹掉, 之后使用Checkpint资源恢复状态. 缺点是checkpoint消耗的资源较大.

10.4.2 异常的处理

  • 由于处理器的乱序特性, 时间点上先发生的异常未必在程序中是靠前的:

  • 例如上图中,第二条指令在decode阶段发现了异常, 而第一条在Ex阶段发现异常, 看起来是第二条指令的异常先发生

  • 为了按照正常的顺序处理异常, 在处理异常时需要保证该异常之前的所有指令都已经被处理完成了.

  • 最简单的办法就是在指令退休时, 如果在ROB中发现其有异常, 则必须处理完才能退休.

  • 为了支持精确的异常, 当发现退休的指令存在异常时, 在跳转到异常处理程序之前, 需要将产生异常指令后面的所有指令都抹除, 并将他们对处理器状态的修改恢复:

  • 一条指令从ROB退休时发现其触发过异常, 那么ROB中所有指令都不允许退休而改变处理器状态. 可以将整个流水线指令都抹除并恢复状态. 与Recovery at Retire是类似的.

  • 基于WALK进行状态恢复: 一条指令被重命名后会把该指令对应的旧映射关系也写入ROB, 这样当指令退休时, 如果发现异常, 可以从ROB中最新写入的指令开始,逐个将指令对应的旧映射关系写到RAT中, 就能对RAT进行恢复, 这种方法就是前面提到的[[D-图书馆/DDD-读书笔记/超标量处理器设计/超标量处理器设计——第七章_寄存器重命名#^59c0b8 | WALK]]:

  • 使用[[D-图书馆/DDD-读书笔记/超标量处理器设计/超标量处理器设计——第七章_寄存器重命名#7.2.3 使用统一的PRF进行寄存器重命名 | 统一的PRF]] 进行寄存器重命名时, 与RAT相关的还有两个表格, 一个是存储空闲物理寄存器的[[D-图书馆/DDD-读书笔记/超标量处理器设计/超标量处理器设计——第七章_寄存器重命名#^0880b7| Free Register Pool]], 另一个是用来存储每个物理寄存器的值是否被计算出来, 称为Busy Table. 这两个表格都需要进行恢复.

    • 对于Free Register Pool来说, 每次进行重命名时, 都会从表格中读取一个空闲物理寄存器编号, 和指令的目的寄存器产生映射关系, 之后读指针指向下一个地址. 所以对其恢复只需要恢复他的读指针即可.
    • 对于Busy Table, 因为指令在写回阶段就可以将结果写到物理寄存器, 所以当一条指令到达ROB顶部发现存在异常, 之前的很多指令已经把结果写到了Busy Table. 对其恢复很简单, 在从ROB中读取指令进行状态恢复时, 根据这条指令的目的寄存器将busy table对应表项置为无效即可.
  • 使用统一的PRF进行重命名的架构, 比较适合使用WALK的方法

  • 使用ROB进行重命名的架构, 比较适合用Recovery at Retire. 因为用Architecture ROB虽然也能对Busy table和free list进行恢复, 但是需要逐个恢复,速度较慢.

10.4.3 中断的处理

  • 异常是处理器内部执行指令产生的, 因此异常是跟某条指令同步的
  • 中断是处理器外部产生的, 所以是异步的
  • 对中断的处理有两种:
    1. 立即处理. 将此时流水线的所有指令都抹除, 用上一节的方法对处理器状态进行恢复, 并将流水线中最旧指令的PC保存下来(还有一些状态寄存器), 之后跳到中断处理程序. 处理完成后, 会返回保存的PC处
      • 优点是响应迅速, 缺点是会有一些指令做无用功, 浪费效率
    2. 延迟处理. 流水线停止取指, 但是等到所有指令退休之后才处理中断.
      • 有额外的问题:
        1. 如果在流水线中这些指令发生了D-Cache缺失, 则需要很长时间才能响应中断
        2. 如果发现一条分支预测失败指令, 那么要先处理分支失败, 将处理器状态进行恢复, 也会导致很长的中断响应时间
        3. 如果发现一条指令发生异常, 是先处理异常还是先处理中断? 一般来说先处理中断,因为很多异常处理很费时, 例如TLB缺失或PageFault

10.4.4 Store指令的处理

  • store指令在退休时, 才能将其数据写入D-Cache, 在此之前, 即使store计算完成, 也会将其存在Store Buffer中.

  • 所有的load指令不仅要访问D-Cache, 也需要访问Store Buffer, 如果在store buffer中发现有store指令携带的地址与他相同, 且在他之前进入流水线, 则这条store指令携带的数据将直接给load

  • 为了不让store阻塞其他指令,一种方法是: 在Store Buffer中可以增加一个状态位:

    1. 当store指令在分发阶段时, 会按照程序顺序占据Store buffer的空间,标记为un-complete状态
    2. 当store指令已经计算完地址和数据, 但是还没成为最旧的指令时, 标记为complete
    3. 当store指令成为最旧指令并退休时, 标记为retire
    • 硬件自动将store buffer中处于retire的store指令结果写到D-Cache
  • 由于退休的指令仍然会占据store buffer,只有当其写完D-Cache后才能离开, 所以store buffer 可用容量变小. 一种办法是: 可以将退休的store指令存储在另一个地方: Write Back Buffer:

    • 指令一旦退休, 就将其放到Write back buffer, 这样write back buffer就负责将数据放到D-Cache, 解放了SB的空间

    • 一旦Write back buffer满了, 就不能再退休store指令

    • 在store进入write back buffer 时, 还要检查是否有同地址的指令, 需要将其置为无效

10.4.5 指令离开流水线的限制

  • 对于4-way超标量处理器, 一次可以退休4条指令, 这对很多部件提出了多端口需求:
    1. D-Cache或Write Back Buffer需要支持4个写端口
    2. 每周期要将4条分支指令信息写入分支预测器, 需要BTB, PHT等都支持4个写端口, 同时还需要能够将checkpoint 资源在每周期释放四个
    3. 如果实现了store/load相关性预测, 则每周期也有四条load信息要写回到相关预测器, 需要4个写端口
  • 好在这些情况出现的几率都很小, 可以对这些特殊情况做限制, 例如
    1. 每周期最多只能有一条分支指令,如果有多条, 那么第二条及其后的指令都不能在本周期退休
    2. 或者在提交阶段对分支,store, load指令的个数都进行限制:
      • mask信号可以将多余的指令进行退休屏蔽

      • 异常也可能有多条指令发生, 但是因为要进入异常处理程序, 所以每次只能处理一条异常, 也可以使用该电路选出第一条异常, 屏蔽其他有异常又要退休的指令

完结,撒花~

posted @ 2022-12-21 11:47  love小酒窝  阅读(277)  评论(0编辑  收藏  举报