基础

本文乱序处理器基于tomasulo算法。指令集为mips,参考书籍为《超标量处理器设计》

乱序处理器处理的主要是tomasulo算法中提到的相关,大致可以分为重命名,发射,执行,写回,退休五个阶段。除了这五个阶段外,前端还有取指和译码两个大的阶段。

译码

译码主要解析出这条指令的rs, rt, rd, src1, src2和aluop。rs和rt是两个源操作数,rd为目的操作数, src1和src2是两个操作数的值,aluop是代表这条指令进行的操作。

需要特别注意的一点是mult和div的处理。因为mult和div是存在hilo寄存器中的,并且它写回时需要写回两个操作数,因此可以将它划分成两条指令。两条指令的rd分别为hi和lo。

重命名

基于rob的重命名

首先我们有一个rat(寄存器重命名表),他表示虚拟寄存器(r0-r31)和物理寄存器(真实的物理寄存器堆和rob)的映射关系。

寄存器重命名的过程为:

  • 根据解析出来的rs和rt编号查rat表,与此同时查物理寄存器堆
  • 如果当前值位于物理寄存器中,则直接取出,并且将当前src置为从寄存器中取出来的值,输出的寄存器编号置为0。否则重命名为rat取出来的编号

rat的修改:

  • 当译码出的rd不为0时,将rat中的rd位置置为该指令的rob编号
  • 当指令退休时,如果rat中对应物理位置的编号是当前退休指令的rob编号,则将该项置为0

基于统一prf的重命名

rob的问题在于它为每一条指令都分配一个表项,即使这条指令不需要写入也占用了空间。并且数据存在于rob和物理寄存器堆中,退休时还需要进行额外的数据移动。统一prf的重命名方式解决了上面两个问题。

prf也就是物理寄存器堆,统一prf即对prf进行扩充,在顺序发射中物理寄存器一般是32个,而现在可以扩充到64乃至128个。

现在rat映射不是映射到了rob的序号而是映射到物理寄存器编号。并且为了方便分支预测失败时恢复,还是用了一个额外的rat用来保存退休指令的映射关系。我们认为第一个rat为brat,保存退休指令的rat为rrat

当我们触发分支失败时,由于rrat中保存的是当前所有正确执行命令的映射关系,所以我们只需要将rrat中的数据复制到brat即可

因此现在rat的修改为:

  • 当译码出的rd不为0时,从free_list中取出来一个可用的物理寄存器编号,并将其写入rat的rd位置。即brat[rd] = physical_index
  • 当指令退休时,会给我们origin_rd和rename_rd,我们修改rrat中对应位置为重命名的rd。即rrat[origin_rd] = rename_rd

前面提到了free_list,它保留了所有可用的物理寄存器编号。初始值为所有物理寄存器编号(0除外,因为我们通过0判断是否准备完成).

  • 当rd不为0时,我们会从free_list的head中取出一个rob_index,并让head+1
  • 当指令退休时,如果它的rd不为0,则将该rd放到tail+1,并让tail+1

最后我们需要考虑寄存器重命名过程:

  • 根据解析出来的rs和rt查两个rat表
  • 如果两个表中的值不相同,表示当前最新的结果还未写回,将寄存器编号置为brat中寄存器编号。否则将寄存器编号置为0并且用读出的编号查物理寄存器堆,

寄存器重命名的输出和译码阶段的输出只多出了rob_index,并且rs,rt,rd编号和src1,src2可能改变

发射

发射也就是将重命名的指令暂时保存到发射队列(tomasulo中的保留站) ,直到他们的操作数全部准备完毕后再发送到执行单元中。

发射级的流水线基本可以分为两级。第一级负责存入指令和唤醒指令,第二级负责挑选指令并发射。

设计方式选择

  1. 集中式-分布式:
    集中式和分布式表示发射队列和执行单元的对应关系。现在我们执行时会分为多个执行单元。例如有两个alu,一个乘除法执行单元,一个浮点运算单元等。如果这些运算单元共有一个发射队列,则是集中式。如果每一个执行单元都有一个发射队列,则为分布式

    集中式的优点在于他只有一个发射队列,可以最大程度的利用每一个空间。但是它会使电路设计变得比较复杂。分布式的优点和缺点和集中式恰好相反。

    例如我们分布式设计为8项alu发射,8项浮点发射。如果一直执行浮点指令,那么8项浮点发射队列很快会满,此时我们无法利用空闲的8项alu发射队列。

  2. 数据捕捉-非数据捕捉:

    数据捕捉就是在流水线的发射阶段之前读寄存器,因为这种方法是在发射阶段之前读物理寄存器堆,因此它的读端口个数是发射指令数量 * 2。但是它需要在发射级中保存数据

    非数据捕捉就是两个操作数准备好发射到运算单元时读寄存器,这种方法不用在发射级中保存数据,并且wb阶段前推时不用根据数据缓存中的值。但是这种方法需要为每个发射队列都准备相应数量的端口,而发射队列的端口数一般都远多于指令,因此该方法需要的端口数比较多

    在实际应用中发现即使使用非数据捕捉还是需要保存一些运算指令的立即数信息,即还是需要保存指令的数据,因此本设计偏向于数据捕捉

  3. 压缩和非压缩:
    压缩和非压缩涉及到发射队列的设计。这两种方法直接决定了发射队列的结构。

    压缩指的是发射队列维护队列结构,如果把发射队列中间的某一项移出则需要把后面的所有项都移动,确保中间没有空隙。
    如果只有一次只取一个,那么所有项最多只会移动一格,因此当前项在下一时刻的值为当前值或上一项的值。
    这种方法的问题是:

    1. 浪费硅片面积
    2. 功耗大: 每次都要对很多指令进行移动

    但是它有一个很大的优势即每次取最老的指令。因为最老的指令中可能存在的相关性更多.

    非压缩是每次有空位置就放,取也是随机取,这种方式选择电路比较简单,但是无法取到最老的指令,因此性能难以比较

唤醒

寄存器重命名后的数据将在这一级中存入发射队列。对于压缩队列来说存入直接在对头写入即可,而对于非压缩队列来说可以使用free_list进行管理。

此外还有一个重要的操作是接受唤醒总线的数据并判断当前指令的操作数是否准备。方法为用写回的操作数和当前的操作数进行比较,如果相等则认为当前操作数准备完成,与此同时接受该指令的计算结果。

单周期指令的唤醒

对于单周期指令来说,当它从发射队列中选出之后,便可以将其操作数送到唤醒总线中供所有发射队列判断。假设当前指令在Select阶段从发射队列中选出,那么这一周期它将会发送操作数,下一周期依赖于这一操作数的指令便可以被发射,从而实现了指令的背靠背执行

需要注意在Bypass阶段需要将数据传递给Execute阶段,并且一周期内Bypass的数量是写回总线的宽度。

多周期指令的唤醒

相对于单周期指令唤醒,多周期指令需要考虑的便多了很多。多周期指的是指令需要多个周期指令,例如乘除法指令。

延迟广播: 延迟广播指的是在Select阶段不进行唤醒,在该条指令即将产生数据时再进行唤醒。
例如上图为一个三周期的乘法指令,它将会在bypass的前两个周期进行唤醒,这是因为指令从选出到执行需要经过Select, RF Read两周期再获得执行所需数据。

需要注意的是它不适合发射后读寄存器堆的结构。因为多周期指令往往和单周期指令是共用发射总线的,也就是说唤醒总线和读寄存器总线同时占有。假设mult发射队列优先级大于定点发射队列,那么他会使定点发射队列暂停两次,一次是读寄存器时,一次是提交到唤醒总线时。但是对于发射前读寄存器堆的结构就没有这个问题,因为它发射后不再读寄存器。

这种结构实现起来相对简单,如果是固定优先级,那么只需要将高优先级的唤醒信号传递给低优先级发射队列即可,低优先级发射队列会暂停一周期。

延迟唤醒: 延迟唤醒即所有指令在选择之后都执行提交唤醒总线,并且还提供该条指令的延迟值。例如mult指令的延迟值是2, 一个16周期的除法指令延迟值是15。然后发射队列接受到延迟值之后每周期减一,直到延迟值为0才可以发射。

书中提到的一种实现方式是通过移位寄存器,例如mult指令的延迟值是2,那么将它编码为3’b100。然后每周期右移并且最高位置1,如果最低位为1那么则认为可以发射。

通过移位寄存器实现较为简单,但是如果某一类指令延迟较长便会带来较大的损耗,例如除法指令。

选择

对于压缩队列来说选择队首元素即可。而对于非压缩队列来说则需要一系列比较电路。

首先我们可以根据rob_index来确定哪条指令比较老,但是rob是循环队列,因此不一定是rob_index小这条指令便一定小。

假设我们给rob定一个方向direction,初始为0,tail每翻转一次direction页翻转一次。也就是说在某一时刻tail < head。此时index小的反而更新,但是此时两条指令的direction不同。因此我们可以确定两条规则:

  1. direction相同,rob_index越小越老
  2. direction不同,rob_index越大越老

使用上面两条规则我们就可以判断哪条指令更老,因此可以选择出最老的指令

读寄存器

选择之后rs,rt的数据已经准备好了,在这周期将从prf中获得数据。也就是说有n个发射队列就至少需要2n个读端口。这种方法实现的prf使得发射队列不需要接收写回阶段的数据,简化了发射队列的设计难度,但是增多了prf读端口的数量,如果发射队列太多prf可能成为设计瓶颈。

另外一种方法是在重命名阶段就读取数据,这样假设一次发射n条指令,那么只需要2n个读端口。而发射宽度一般较小,并且可以减少一个周期,但是这种方法使得发射队列的设计复杂度大为增加。

执行

执行单元实际运算指令,包括加减乘除,逻辑指令,分支指令等。大部分指令只需要一个周期即可完成,但是乘除法需要多个周期。一般来讲乘除法会额外设计成一个执行单元,每周期允许流入一条乘法和除法指令,乘除法都设计成多级流水避免阻塞,在完成计算之后送入写回总线中。

写回

因为存在精确异常,我们需要知道当前运行的最后一条指令,因此必须设计一个结构保存指令之间的执行顺序及执行情况,这个结构便是rob。在重定向阶段指令便已存入rob中,在写回阶段将rob中对应的指令标记为完成,最后在退休阶段把最后完成的指令移除rob并且在prf中标记rd,表示当前寄存器正在使用。

此外在退休阶段还将进行分支预测失败及异常的检查,在写回时将同时写入分支预测信息和异常信息,如果触发异常或者分支预测将清空流水线并且从最后一条指令处重新取指。

展望

乱序多发射处理器是现代高性能处理器的设计基础,但是设计方向也经过了数次变化。在21世纪初期,人们致力于增加发射宽度(指令级并行),但是由于大量的硬件消耗和微小的提升导致处理器制造商将开发重心放在了增加处理器核对数量,这样将探索并行的责任从硬件转移到软件和程序员手上。

同时,利用线程级并行也可以隐藏访存缺失带来的性能损失,也就是intel提到的超线程