Skip to content

CPU设计

CPU设计

2021/7/22

此文档是我们小组的处理器实际采用的架构

分支预测(新)

部件设计

所有的部件都根据当前的PC值来索引对应的信息,也就是在取指第一阶段利用部件的信息。

BIT

  • 信息为一位:当前PC的下一个PC是否是RAS栈顶的地址(是否是函数返回)
  • 该器件的更新只在解码阶段:(1)是延迟槽(BB,即Branch Buffer有效,且当前pc与BB中存放的分支指令pc连续),若此时BB中存放的分支类型是函数返回,则把当前延迟槽所在的指令组作为索引更新BIT,并把指令组第二条指令置为无效(2)不是延迟槽(BB无效),此时根据预解码传来的信息是否是函数返回来更新BIT

RAS

若RAS更新后,后面的流水线异常了,或是分支预测失败了导致本次更新无效该怎么办?

(1)跟PHT和BTB一样在(延迟)执行阶段更新,确保其正确性

(2)检查点机制

  • 目前采用方法(1)

  • 带有计数器的栈,保存的是函数调用的地址

  • 或该器件的更新只在执行阶段:若是函数调用就压栈,若是函数返回就出栈。

PHT

  • 信息为两位饱和计数器,指出当前PC是会顺序取下一个PC(当饱和计数器位0或1是),还是会取BTB中的值(当饱和计数器的值为2或3)
  • 该器件的更新在执行阶段或延迟执行阶段:(1)分支指令的PC值8字节对齐,以分支指令所在指令组为索引来更新(2)分支指令的PC值4字节对齐,以分支指令+4(延迟槽)所在指令组为索引来更新

BTB

  • 信息为32位地址:当前PC的下一个PC值,只会当PHT预测跳转的时候才会使用
  • 该器件的更新在执行阶段或延迟执行阶段:(1)分支指令的PC值8字节对齐,以分支指令所在指令组为索引来更新(2)分支指令的PC值4字节对齐,以分支指令+4(延迟槽)所在指令组为索引更新

BB

  • 保存分支指令的实际的信息:有效位、分支指令的类型(是否是函数跳转和函数返回)、分支指令所在地址、地址有效位、分支指令跳转地址
  • 当指令组第二条为分支指令时保存,把有效位置为1,若下一条指令不是紧挨着的延迟槽,就把其bubble掉,并在延迟槽的地方取指,直到延迟槽的到来;等到延迟槽来了之后,在当前周期利用保存的信息,并在下个周期把有效位置0

正确性判断

  • 取指第二阶段:若分支指令在指令组第二条,则一定应该顺序取;若分支指令在指令组第一条,且为函数调用或返回,则一定不应该顺序取,若是函数返回,则一定应该取RAS栈顶的值。其它情况暂时无法判断

  • 解码阶段:(1)是延迟槽(BB有效,且当前pc与BB中的分支指令pc连续),按照BB中存放的信息来判断

当前指令组预测情况 分支指令的实际情况 行动
顺序取 顺序取 /
不顺序取 不顺序取 指令组第二条指令置为无效
顺序取 不顺序取 指令组第二条指令置为无效,且要重新取指、之前的指令置为无效
不顺序取 顺序取 重新取指,之前的指令置为无效
/ 暂时无法判断 /

(2)不是延迟槽(BB无效):若指令组不包含分支指令则应该顺序取;若指令组第二条为分支指令,则应该顺序取;若指令组第一条为分支指令,直接判断是否跳转和跳转地址,无法判断的放到后面。

  • 执行(延迟执行)阶段:直接判断是否跳转和跳转的地址


数据旁路

目前先不考虑延迟执行

当指令进入FU的时候将信息写入计分板

  • **先考虑**两条紧挨着的计算指令有执行相关应该怎么处理,前一条指令计算出来直接旁路还是等一个周期再旁路呢?计算出来再旁路可能延迟会高⚡
  • 计分板:一共31行,每一行包含的是对应寄存器的状态(一共32个寄存器,但是不包括0号寄存器,所以是31行),因为每周期最多发射两条指令,所以需要四个读端口(对应每条指令的两个源寄存器)和两个写端口(对应每条指令的一个目的寄存器)
  • 计分板中一行包含的信息:pending(寄存器有效位,即寄存器是否是FU中某些的指令的目的寄存器)、相关指令所在行、相关指令所在列(每个周期自增,除非有stall信号)
  • 在读操作数的时候,需要用当前指令的源寄存器索引计分板,若pending位为1,则需要进行转发,若判断当前回合无法转发,则需要stall住,直到能从对应的行列处将数据转发过来,下表为不同类型的指令(对应不同行数)转发的最早阶段:
指令类型 转发的最早阶段 备注
ALU 1 一般计算指令只需周期
BRU / 分支指令不转发其它指令
AGU 3 访存数据只能在第三个阶段获得
HILO 1
MLT 3 乘法为三周期流水线
DIV 1 暂停流水线直到算完
COP / 异常指令


延迟执行(目前先不用)

  • 可以延迟执行的指令:在解码阶段之后不会出现异常的指令,比如部分计算
可能出现异常的指令 出现异常的原因
  • 什么时候需要延迟执行:当在读操作数的时候,指令操作数没有完全准备好,且此时指令允许延迟执行,需要在写入计分板的时候带上额外一位(允许)


指令数据传输

  • ICache传来的指令组要8字节对齐,也就是包含两条指令
  • 接收到指令时,需要根据PC值来判断第一条指令是否有效,如果PC值只是4字节对齐而没有8字节对齐(如bfc00004),那么此时指令组第一条指令应该判定为无效
  • 当分支指令为8字节指令组的前四个字节的话(一条指令4个字节),此时延迟槽正好为后四个字节,不会出现问题,所以该指令组的分支预测正常进行
  • 当分支指令为指令组后四个字节的话,需要在下个周期把延迟槽取出来,也就是说该分支指令没有预测的必要,始终是顺序取,等到后面的延迟槽来了才应该真正开始分支预测
  • 综上,我们发现,分支预测应该是在延迟槽取来的那个周期进行,而不是分支指令本身取来的那个周期!!!
  • 但是延迟槽本身没有跳转的地址这样的信息,需要上个周期的分支指令的信息,因此需要在decode阶段放置一个BB(Branch Buffer)来存储上个周期的分支指令的信息,在当前周期处理延迟槽的同时利用这个信息进行分支预测的正确性判断


CPU架构

CPU架构缩略图

与Cache的交互只在取指和访存两个阶段。一下每个阶段都对应**时钟一个周期**

  1. 取指(1):将计算好的PC发给ICache
  2. 取指(2):获取ICache中的指令组和其它信息
  3. 译码:对指令进行译码,并对分支指令的地址进行计算
  4. 发射:往队列尾部插入指令组,在头部根据控制逻辑发射一条或两条指令
  5. 数据获取:通过寄存器堆和数据旁路获取数据
  6. 执行:在FU中计算数据
  7. 访存(1):在FU中向DCache发送访存请求,延迟取数据
  8. 访存(2):在FU获取DCache中的数据或其它信息,延迟计算
  9. 退休:将数据写回到寄存器堆


取指

第一个阶段发送第二个阶段接收的原因:让CPU流水线阶段与ICache的流水线阶段**匹配**,这样可以提高吞吐量,如果在同一个阶段发送并接收的话,CPU流水线就必须暂停,降低了效率。

  • 取指分为两个阶段,第一个阶段向ICache发送地址,在第二个阶段接受指令,因为指令传来需要一定时间,所以只对传来的指令进行简单的处理和判断。
  • 第一个阶段发送的数据:指令地址PC、地址有效位valid
  • 以ICache返回的 addr_ok 作为是否接受到的标志,若未接收到,需继续发送数据,也就是把当前周期给stall(暂停)住,这个是用流水线控制逻辑实现的。
  • 根据当前的PC值,预测下一个PC值,并把是否是跳转指令、预测的分支指令类型、是否预测跳转以及跳转的地址传给后续的阶段来判断分支预测的正确性,以更新分支预测模块。
  • 第二个阶段传来的数据:指令组中的指令(两条)、指令组的有效位 data_ok、指令是否是分支指令is_branch(两位、需要ICache预解码)
  • 同时当第二个阶段没有传来data_ok的话,且第二个阶段是有指令在等待请求返回的,也就是流水线寄存器中的valid为1(表明该阶段是有效的,而非nop之类的无效指令),此时需要stall住第二阶段来等待数据返回,由于第二阶段stall住了,所以如果第一阶段也是有效的地址请求即valid为1,此时也要跟着stall住。
  • 返回的指令组8字节对齐,包含两条指令,若PC请求仅为4字节对齐而不是8字节对齐,此时指令组第一条指令为无效指令,因为PC应该是从第二条指令开始取。

译码

  • 判断指令的类型,以及源寄存器和目的寄存器的下标,放入到发射队列中
  • 若分支指令取来了,但是延迟槽没有取来,则把分支指令的信息传给BB,等到延迟槽来的时候,把BB保存的分支指令的信息传给延迟槽。

发射

  • 两个写端口、两个读端口的FIFO的双发循环队列,用head和tail下标的方式模拟循环队列。
  • 存放的数据:指令的地址、指令的各类操作数、是否为访存指令、是否为特权指令、是否为分支指令、是否预测跳转、预测跳转的地址
  • 以下不能同时发射两条指令的情况:
  • 两条连续的乘除法指令:只有一个乘法器和除法器
  • 访存指令:只有一个访存单元,而且DCache只有一个读写端口,没法同时处理两条访存指令
  • CP0TLB等特权指令
  • 两条指令有数据相关:注意0号寄存器的假相关性(0号寄存器一直为零)
  • 分支指令和延迟槽必须一起发射,当第二条为分支指令或延迟槽还未取到发射队列的时候。

数据获取

  • 用两条指令的源寄存器下标获取源寄存器里的值,同时进行旁路(转发)从而获得所有的操作数,因为可能会有延迟读操作,所以当前周期读到的可能无效,同时对于不可避免的指令相关需要暂停流水线。
  • 寄存器堆有四个读端口、两个写端口

执行和访存阶段

后续提到的FU囊括了读操作数阶段后的所有阶段,一共三个阶段,对于访存指令来说,,总共三个阶段,对于一般的计算指令来说,,对于乘法指令来说,下图是FU的缩略图,一共有三行,而实际一共有8行,每一行均有三个阶段。它们分别是

  1. ALU1:计算单元1号,处理一般的计算指令,有一个执行阶段(计算)和两个延迟执行阶段(延迟读操作数和延迟计算)
  2. ALU2:计算单元2号,同上
  3. BRU:处理分支指令,有一个执行阶段(计算地址)和两个延迟执行阶段
  4. AGU:处理访存指令,有一个执行阶段(计算地址)和两个访存阶段(发送和接收数据)
  5. HILO:处理hilo寄存器和普通寄存器的交互,在提交写段写回寄存器
  6. MLT:处理乘法指令,乘法指令需要三个周期才能算出结果,可以流水化
  7. DIV:处理除法指令,除法指令需要n个周期才能得到结果,会暂停等到结果算出。
  8. COP:处理异常指令,在访存阶段统一处理所有的异常

FU架构缩略图

执行

  • 获得可能的转发数据,生成指令的结果,计算访存地址
  • 对于需要延迟执行的指令,这个周期可以不管。
  • 对于分支指令,这里可以最终判断是否跳转以及跳转的地址,更新分支预测模块。
  • 多周期指令会暂停流水线

访存

若访存后紧跟需要访存数据的指令,即访存相关,则需要暂停流水线很多个周期,为了提高效率,处理包括访存相关在内的各种指令相关,减少流水线暂停的周期,进行部分指令的延迟执行。如果指令在译码阶段后不会产生异常(例如 AND,XOR,ADDU 等),我们可以将这类指令的操作数读取放到到访存的第一阶段执行,执行放到访存的第二阶段。这样访存相关的暂停最短只要一个周期。我们还可以将分支指令也如此推迟。但是由于分支有可能预测失败,我们需要能够撤销其后的指令,若在访存第二阶段处理分支指令时发现分支预测失败,需要把这个信息传给DCache,防止改变DCache。

当然,如果指令不能延迟执行,那么仍然需要暂停到操作数均可用为止。此时的指令相关不仅要考虑访存指令,还要考虑正在延迟执行的指令。

在执行阶段计算结果。如果指令之间存在访存相关,并且指令是分支或者不会在执行阶段出现异常,那么其可以在流水线访存第一阶段读取操作数,在后一个阶段计算结果。

  • 访存有两个阶段对应于FU的第二和第三阶段,与取指的两个阶段类似
  • 访存第一阶段会发送的数据:地址有效位、是否为写请求、四位字节写使能、读写的地址、写入的数据
  • 访存第一阶段以 Dache 返回的 addr_ok 作为成功接收的标志
  • 第一阶段还会处理异常,并进行部分指令的延迟取数据
  • 访存第二阶段接受的信号:有效位 data_ok 和数据,其中data_ok作为成功完成当前访存的信号
  • 第二阶段还会进行部分指令的延迟计算

退休

  • 将数据写回到寄存器堆中


分支预测架构(旧)

主流的处理器都是基于两位的饱和计数器来实现的,采用!

经测试,PHT为2KB时,即PC值中用来寻址PHT的位数为13位,此时分支预测准确率达到了93%,采用!

MIPS 74kf 处理器:不进行目标地址的预测,而是在取指阶段根据ICache预解码的分支指令的信息直接计算目标地址,因为MIPS程序的绝大部分分支指令是直接跳转指令,可以很快算出目标地址。

MIPS处理器中用JALBAL等指令作为CALL指令,用jr ra作为Return指令

预测

  • 部件有BIT、PHT、BTB、RAS,下述的PC索引位**仅为示例**,具体实现请参数化(方便挤牙膏
  • BIT:在取指第一阶段根据当前的PC[13:2]索引,内容有两位(是否是分支指令、是否是函数返回,即jr ra
  • PHT:在取指第一阶段根据当前的PC[13:4]索引,内容为两位饱和计数器(00:强不跳转,01:弱不跳转,10:弱跳转,11:强跳转)
  • BTB:在取指第一阶段根据PC[13:4]索引,内容为32位的跳转的地址
  • 同一条分支指令它的PHT和BTB是一一对应的。
  • RAS:在取指第一阶段获取栈顶数据,根据BIT中的一位信息(是否是函数返回)来使用,但是暂时不更新栈(因为有可能BIT中的信息是错的),在解码阶段(能够确保正确的情况下)根据指令的类型(call:jal bal、return:jr ra)把地址进行压栈或出栈,栈中每一项带有计数器(当压栈时,把栈顶和需要压栈的地址比较,相同就让计数器加一,栈指针不动,出栈时计数器减一,减到零才真正让栈指针往下一格)

正确性判断和更新

分支预测可能出现的问题:

编号 具体表述
1 未能正确判断是否是分支指令
2 是否跳转预测错误
3 跳转地址预测错误
4 未能正确判断是否是函数返回
  • 取指第二阶段根据ICache传来的预解码的是否是分支指令的信息is_branch能够判断编号为1的错误
  • 解码阶段根据分支指令类型能够判断编号2、4(部分分支指令一定跳转),根据简单加法器算出的地址能够判断编号3,根据RAS的地址能够判断编号3,同时更新RAS。
  • 执行阶段根据得到的结果能够完全判断编号2和3
  • 在预测错误时需要立刻重新取指,而上述部件的更新或者写入在执行阶段统一处理(除了RAS在解码阶段更新)


权衡与选择

  1. store指令需要返回data_ok吗?目前dcache对标icache,所以是需要返回data_ok
  2. BIT(Branch Instruction Table)部件的选择:看过各种资料,分支预测这块的架构中好像都没有BIT这个部件,也就是直接判断一条指令是否是分支指令的部件。我们知道,分支预测是按照当前PC去预测下一个PC值,而且因为有大小的限制,不可能用PC的所有的位来索引,一定会有冲突,即不同的PC索引到同一个地方,而如果分支指令对应的PC和非分支指令对应的PC索引到同一个地方产生冲突的话,即每次非分支指令都会用到分支指令的预测结果,如果没有办法把它们区分开来,就会造成非分支指令,但是却预测跳转的错误。一般架构中好像是用BTB中tag比对后出现的miss(非分支指令一定会miss,所以只能顺序取,所以不会出现跳转的错误)来被动区分非分支指令和分支指令,这里我们打算主动区分,因为每个PC值只需要存放一位是否是分支指令的信息,所以可以存放很多。同时还可以存放分支指令类型的信息。
  3. 分支与非分支指令的区分?主动:BIT,需要的空间多。被动:在BTB中利用tag比对会出现的miss,时间花费多
  4. 取指时分支指令与延迟槽怎么处理?(1)ICache给的指令组中分支和延迟槽绑定,问题:分支和延迟槽不在同一个cache line(2)译码阶段若发现没有取到分支指令后的延迟槽,把分支的信息放在一个缓存中,等到下一周期取来了延迟槽再进行分支预测和正确性判断。目前我们采取的是方法(2)
  5. 发射阶段若第二条是分支指令则不发射的原因:当分支预测失败后需要清空之前的指令,如果延迟槽慢分支指令一个周期的话,则延迟槽需要执行,而和延迟槽同周期发射的另一条指令则不需要执行,但他们是在同一个周期发射的,处理起来比较麻烦。
  6. 针对分支指令(就是拿分支指令的PC来)进行分支预测:(1)因为一个指令组最多只有一条分支指令,所以不会出现需要多次预测的情况,可以直接用指令组对应的PC值(8字节对齐)来预测(2)虽然有可能不执行分支指令,而是直接执行分支指令后一条指令(即延迟槽):比如说有一条分支指令跳转到延迟槽,这样用分支预测就不正确了,一个解决方法是存放指令组中分支指令的具体位置,但是可能性比较小,所以不进行特殊处理。