CPU-EXE阶段的设计

  1. 概述
  2. Bypass Network
    1. 整体架构与流水线
    2. Bypass冲突问题
  3. load/store指令加速
    1. Memory Disambiguation
      1. load指令部分乱序
      2. load指令完全乱序
    2. Non-block Cache

概述

在现代的CPU中, 为了获得更大的并行度, 一般都会同时使用多个FU.

  • FU的个数决定了每个周期可以并行执行的指令个数, 也就是issue width.

在EXE阶段还有一个很重要的东西叫做bypass network, 它负责将FU的运算结果送到所有需要它的地方, 例如:

  • FU的输入端.
  • 物理寄存器

当指令并行度提升时, bypass network会变得越来越复杂, 这也是一个trade off.

如果采用Non-Data-Capture的结构, 那么物理寄存器的读端口的个数, 就等于Issue Width乘2.

Bypass Network

整体架构与流水线

当FU计算出结果时, 这个结果需要立马被bypass到所有FU的输入端(其实本质上就是把FU算出来的结果直接交给Issue出来的指令).

此时, FU的输入端就可能来源于多个地方:

  • 可能来源于物理寄存器.
  • 可能来源于所有FU bypass过来的结果.

因此, FU的输入端就会有一个大的多路选择器, 指令从Issue完之后, 先会读取物理寄存器, 然后再经历一个叫做Source Drive的流水线阶段之后, 再把结果送入FU的输入端.

  • Source Drive阶段会对物理寄存器和输入和FU bypass的输入进行选择.

  • Source Drive阶段的引入是为了提高CPU的频率.

同样, 计算结果从FU的输出端到FU的输入端也会经过一个很长的阶段, 可以在中间插入一个叫做Result Drive的流水线, 来提高频率.

加入Source Drive和Result Drive阶段之后, 对于一条指令来说:

  • 一条指令可以从Source Drive阶段获得FU bypass的结果, 也可以从EXE阶段获得FU bypass的结果, 在这两个阶段都会进行操作数的选择(从物理寄存器中读, 还是从FU bypass中读).

    • 选择的方法如下:
      • FU bypass时也会把目的寄存器的编号进行广播.
      • 当前指令会把这个广播的寄存器编号和自己的源寄存器编号进行比较, 如果相同就用FU bypass的结果, 如果不同就用物理寄存器的结果.
  • FU bypass的结果既可以从Result Drive阶段获得, 也可以从Write Back阶段获得.

Bypass冲突问题

假设一个FU可以处理多种类型的指令:

  • 处理A指令, 需要3个周期.
  • 处理B指令, 需要1个周期.

这种FU可能会出现一种情况, 在某一个周期, A指令和B指令的结果同时被计算出来了, 此时A指令和B指令都需要借助这个FU的bypass network, 这就产生了冲突.

load/store指令加速

Memory Disambiguation

load指令部分乱序

对于load/store指令来说, 它们之间也存在RAW, WAW以及WAR的依赖.

但是load/store指令的地址, 只有在EXE阶段才能够计算出来, 因此依赖关系很难检测.

在现代的CPU中, 一般采用如下的处理方法:

  • store需要顺序执行.
  • 两个store指令之间的load指令可以乱序执行.
    • 采用这种方法, 可以排除WAW和WAR依赖.
    • 同时, 也可以检测RAW依赖.

具体的工作过程如下:

  • 准备一个Store Buffer, 用来缓存已经被Select, 但是还没有被Commit的store指令的目标地址.
  • 当store指令计算出目标地址后, 这个目标地址就会被写入Store Buffer.
  • 当store指令被Select之后, 他后面的所有load指令都可以被Select, 当load指令计算出目标地址时, store指令肯定能计算出目标地址, 并且将目标地址写入Store Buffer中.
  • 此时, load指令会将自己的目标地址和Store Buffer中的地址进行比较, 如果相等, 那么load指令会直接从前面的store指令那里取出数据.

一个问题?

假设现在有两条store指令, 中间夹着若干load指令, 那么会有这样一种情况:

  • 第二条store指令会比之前的load指令先选中.

此时, 第二条store指令就会先在Store Buffer中写入目标地址, 那么之前的load指令在读取Store Buffer时, 就会不知道使用哪个地址了.

因此, 需要一种机制告诉load指令我应该读取Store Buffer中的哪个目标地址, 可以给每一个load/store指令附一个编号, 这个编号可以来自于:

  • 指令在ROB中的地址.
  • Decode阶段分配的编号.

但是这会增加CPU的复杂度, 因此, 可以考虑加一个闲置, 就是只有等到中间的load指令都被Select之后, 才允许第二条store指令参与Select.

load指令完全乱序

load指令完全乱序的做法如下:

  • store指令还是in-order issue.
  • load指令是out-of-order issue.
    • 让load指令提前执行, 以便能够wakeup更多指令, 提高指令并行度.

但是如果这条load指令与前面的store指令具有RAW的依赖, 那么这个load指令, 以及与它具有相关性的所有指令都要从流水线上被抹掉, 然后重新参与Select.

此时, 可以采用一种load/store相关性预测的方法, 对于每一条load指令, 都需要预测这个load指令是否和前面的store指令具有RAW:

  • 如果没有, 那么它可以先于store指令参与Select.
  • 如果有, 它就必须等到store指令被Select后才能参与Select, 并且load的结果可以尝试从Store Buffer中获取.

Non-block Cache

Non-block Cache主要是针对D-Cache.

对于load/store指令来说, 两种指令都可能引起D-Cache的Miss:

  • 对于load指令, 如果出现了D-Cache Miss, 那么:
    • 首先需要从下级存储器中拿到数据.
    • 然后在D-Cache中, 按照某种算法写入数据.
    • 如果被写入的Cache Line是dirty, 那么还需要将这个Cache Line的数据先同步到内存.
  • 对于store指令, 如果出现了D-Cache Miss, 那么:
    • 对于Write Back+Write Allocate类型的D-Cache, 首先需要从物理内存中找到数据.
    • 然后将这个数据和store的数据进行合并.
    • 然后, 从D-Cache中按照某种算法找到一个Cache Line, 写入.
    • 如果这个Cache Line原来是dirty, 需要同步到物理内存.

不管怎样, 当D-Cache发生缺失时, D-Cache就处于阻塞状态, 此时CPU就不能够执行后续的load/store指令了.