处理器取指
每条指令在存储器空间中所处的地址称为它的指令PC,取址指处理器核将指令(按照其指令PC值对应的存储器地址)从存储器中读取出来的过程
取址的目标如下:
- 快速取址
- 连续取址
可能面对的问题如下:
- 指令的编码宽度不相等,导致PC地址与地址边界无法对齐
- 分支跳转指令执行后,可能导致跳转到另一个不连续的PC处,取址时需要从新的PC值对应的存储器地址读出指令
- 处理器会按顺序执行非分支跳转指令,需要按顺序从存储器中读取指令
传统RISC架构处理器的解决方案
- 连续不断地从存储器中顺序读取出非分支跳转指令,即使是地址不对齐的32位指令,也应该能每个周期读出一条完整指令
- 能够快速判断在分支跳转指令中是否跳转,如果需要跳转,则从新的PC地址处快速取出指令,力求每个周期读出一条完整指令
下面以传统RISC架构处理器介绍取址步骤
对于取指速度的优化
优化取指速度需要保证存储器的读延迟越小越好,但常见的存储器会存在不同程度的延迟
为了让处理器核能以最快速度取址,常使用以下两种方法取址
ITCM(Instruction Tightly Coupled Memory)指令紧耦合寄存器
配置一段较小容量的存储器(通常为几十KB,使用SRAM),用于存储指令
物理上需要离核心很近且专属于处理器核来取地很小的访问延迟(一般为一个时钟周期)
优点:1. 实现非常简单;2. 能保证实时性
缺点:1. 使用地址区间寻址,无法像缓存一样映射无限大的存储器空间;2. 容量受限
I-Cache(Instruction Cache)指令缓存
利用软件程序的时间局限性和空间局部性,将外部指令存储器空间动态映射到容量有限的指令缓存中,将访问指令存储器的平均延迟降低到最小
优点:1. 延迟确定,可以用于实时性要求较高的应用场景;2. 结构、实现复杂
缺点:1. 缓存容量有限,访问缓存不确定性较大,可能造成缓存不命中;2. 无法保证处理器反应速度的实时性
对于非对齐指令取指的优化
处理器取指的一个目标是连续不断,争取每个时钟周期都能取出一条指令,源源不断地为后续执行提供指令流,不出现空闲的时钟周期
一般上述两种优化取指速度的方法都会使用SRAM,他的读端口宽度固定,n位的SRAM在一个时钟周期只能读出一个n位的数据,但如果一条n位的指令被存储于地址不对齐的位置,则意味着需要分2个时钟周期才能读出一条指令,一般使用以下方法来处理非对齐指令
普通指令
使用剩余缓存保存上次取指后没有用完的比特位,供下次使用
例如:从ITCM中取出一个32位的指令字,但只用到了它的低16位,则
- 只需要使用此次取出的32位中低16位指令和之前取出32位中高16位指令组成一个32位指令,再进行执行
- 指令长度本身就是16位,将其暂存在剩余缓存中,等待下一个周期取出下一个32位指令字后再拼接出完整指令执行
分支跳转指令
如果分支跳转指令的目标地址与32位地址边界不对齐,且需要取出一个32位的指令字,则剩余缓存的解决方案失效!
这种情况下常使用多体化的SRAM进行指令存储
常见的形式为奇偶交替:使用两块32位宽的SRAM交错地进行存储,将两个32位指令字分别存储在两块不同的SRAM中,这样就可以在一个时钟周期内访问两块SRAM并取出两个连续的32位关键字,然后拼接形成真正的32位指令
分支指令处理
分支指令类型
无条件跳转/分支指令
无需判断条件就一定会发生跳转的指令
还存在以下分类
无条件直接跳转/分支
使用立即数计算得到跳转地址的指令
RISC-V中的JAL指令就是无条件直接跳转指令,该指令使用编码在指令字中的20位立即数作为偏移量,将其乘2后与当前指令所在地址相加就得到了最终的跳转目标地址
无条件间接跳转/分支
使用寄存器索引的操作数计算得到跳转地址的指令
RISC-V中的JALR指令就是无条件间接跳转指令,该指令使用编码在指令字中的12位立即数作为偏移量,与基地址寄存器(其中索引的操作数)相加得到最终的跳转目标地址
带条件跳转/分支指令
判断条件决定是否跳转的指令
存在以下分类
带条件直接跳转/分支
使用立即数计算得到跳转地址的指令
带条件间接跳转/分支
用寄存器索引的操作数计算得到跳转地址的指令
上面两个类型和无条件跳转/分支指令类似,但是都多出判断条件的这一部分。RISC-V架构中没有带条件间接跳转指令
理论上指令只有在执行阶段完成后才能解析出最终的跳转结果,如果在取指期间暂停,直到执行阶段完成才继续取指,会浪费大量时钟周期,造成流水线断流。所以处理器会采用分支预测技术,会预测跳转的方向和跳转的目标地址
分支预测
- 静态分支预测
不依赖任何执行过的指令信息和历史信息,凭借当前分支指令本身的信息进行预测
最简分支预测:总是预测分支指令不会发生跳转,如果执行阶段发现需要跳转,则冲刷流水线重新取指,会造成两个时钟周期的流水线延迟
分支延迟槽:每一条分支指令后面紧跟的一条或若干条指令不受分支跳转的影响,不管分支是否跳转,后面的指令都一定会被执行。分支延迟槽中的指令永远被执行而不用被丢弃重取,它不会受到冲刷流水线的影响
BTFN预测(Back Taken,Forward Not Taken):对向后跳转预测为跳,向前跳转预测为不跳,比较常见
- 动态分支预测
依赖已经执行过的历史信息和分支指令本身的信息综合进行方向预测
一比特饱和计数器:最简单的动态预测器,每次分支指令执行后就会使用此计数器记录上次的方向,采用下一次分支指令永远采用上次记录的方向作为本次的预测
两比特饱和计数器:最常见的动态预测器,采用FSM的方式进行预测。
当前状态=强不需要跳转 或 弱不需要跳转,则预测该指令方向为 不需要跳转
当前状态=弱需要跳转 或 强需要跳转,则预测该指令方向为 需要跳转
如果预测出错,则反向更改当前状态:从 强需要跳转 要出错连续2次才能变为变为 弱不需要跳转,因此具有一定的切换缓冲,其在复杂程序流中预测精度一般比简单的一比特饱和计数器更高
但是使用该方案可能会导致别名重合:使用多个两比特饱和计数器负责不同分支指令的预测,会导致大量空间占用,所以只能采用有限个计数器组成计数器表格,但表项数目有限但指令众多,所以很多不同的分支会不可避免地指向相同的表项
解决这个问题一般采用动态分支预测算法:采用不同的表格组织方式(控制表格大小)和索引方式(控制别名重合问题)来提高预测精准率,常见算法如下:
- 一级预测器
将有限个两比特饱和计数器组织成一维表格,称为预测器表格。直接使用PC值的一部分进行索引
“一级预测器”指的是其索引仅仅采用指令本身的PC值
优点:简单易行
缺点:索引机制过于简单导致预测精度不高
- 两级预测器
又称为相关预测器
对于每条分支,将有限个两比特饱和计数器组织成PHT(Pattern History Table),使用该分支跳转的历史作为PHT的索引
只需要n个bit就能索引2^n^个表项
分支历史又可以分为局部历史(每个分支指令自己的跳转历史)和全局历史(所有分支指令的跳转历史)
局部分支预测器采用分立的局部历史缓存,每个缓存有自己对应的PHT,对于每条分支指令,会先索引其对应的局部历史缓存,再使用局部历史缓存中的历史值所引导对应的PHT
全局分支预测器使用所有分支指令共享的全局历史缓存。这个解决方案节省资源但只有在PHT容量非常大时才能体现出其优势,且PHT容量越大,优势越明显
常见的全局预测算法有:
- Gshare算法:将分支指令PC值的一部分和共享的全局历史缓存进行异或,使用运算的结果作为PHT的索引
- Gselect算法:将分支指令PC值的一部分和共享的全局历史缓存进行拼接,使用运算的结果作为PHT的索引
- 预测地址
分支目标地址需要在执行阶段计算后才能得到分支的目标地址,这些任务无法在一个周期内完成,在连续取下一条指令前,甚至连译码判断当前指令是否属于分支指令都无法及时地在一个周期内完成,因此为了连续不断地取指,需要预测分支的目标地址,常见技术如下
- BTB(Branch Target Buffer分支目标缓存):使用容量有限的缓存保存最近执行过的分支指令的PC值及它们的跳转目标地址。对于后续需要取指的每条PC值,将其与BTB中存储的各个PC值进行比较,如果出现匹配则预测这是一条分支指令,使用其对应存储的跳转目标地址作为预测的跳转地址
优点:最简单快捷
缺点:1. BTB容量与时序、面积难以平衡;2. 对于间接跳转/分支指令的预测效果并不理想
- RAS(Return Address Stack返回堆栈地址):使用容量有限的硬件堆栈(FIFO)来存储函数调用的返回地址
间接分支/跳转指令多用于函数调用/返回,这两者成对出现,因此可以在函数调用时PC+=4或2,将其顺序执行的下一条指令的PC值压入RAS堆栈,等到函数返回时将其弹出,只要程序正常执行,RAS就能提供较高的预测准确率。不过由于RAS深度有限,出现多次函数嵌套则可能堆栈溢出,影响准确率
优点:正常情况下准确率高
缺点:出现函数嵌套时难以处理
- Indirect BTB(间接BTB):专门为间接分支/跳转指令设计的BTB,它通过高级的索引方法进行匹配,结合BTB和动态两级预测器的技术
优点:预测成功率很高
缺点:硬件开销非常大
- 其它扩展技术
RISC-V架构对取指硬件的简化
规整的指令编码格式
RISC-V指令集编码十分规整,可以快速译码得到指令类型及其使用的操作数寄存器索引或立即数
指令长度指示码放在低位
RISC-V提供可选的压缩指令子集C,如果支持此子集就会有32位和16位指令混合交织在一起的情形
所有RISC-V指令编码的最低几位专门用于编码表示指令的长度,将指令长度指示码放在指令的最低位,方便取指逻辑在顺序取指的过程中以最快速度译码出指令的长度,化简硬件设计。取指逻辑在仅取到16位指令字时就可以进行译码判断当前指令长度而无需等待另外一半16位指令字的取指
此外,由于16位的压缩指令子集是可选的,假设处理器不支持此压缩指令子集而仅支持32位指令,甚至可以将指令字的低2位忽略不存储(因为其肯定固定为11),从而节省I-Cache的开销
换句话说,RISC-V的变长指令集为译码提供方便
简单的分支跳转指令
RISC-V架构中存在2条无条件跳转指令JAL和JALR;存在6条带条件分支指令BEQ、BNE、BLT、BLTU、BGE、BGEU,这些指令和普通运算指令一样,直接使用两个整数操作数,然后对其进行比较,如果比较的条件满足时则会跳转。
这些指令使用12位有符号数作为偏移量,有如下计算公式:
$偏移量*2+当前指令所在地址=目标地址$
16位的压缩指令子集中指令能够一一对应32位的标准指令
没有分支延迟槽
RISC-V砍掉了分支延迟槽,节省了这一器件的面积
提供明确的静态分支预测依据和RAS依据
RISC-V架构中明确规定编译器生成的代码应该尽量优化,使向后跳转的分支指令比向前跳转的分支指令有更大概率进行跳转,因此硬件层面可以更好地和软件匹配,最大化提高静态预测的准确率
并且规定
如果使用JAL指令且目标寄存器索引值rd=x1或rd=x5,则属于需要进行RAS压栈;如果使用JALR指令,则按照使用的寄存器值(rs1和rd)的不同,明确规定相应的RAS压栈/出栈行为,软件编译器必须按照此原则生成汇编代码
蜂鸟E200处理器的取指实现
E200系列处理器核的取指子系统由ITCM、BIU和核心内部取指令单元IFU完成
IFU设计思路
功能如下:
- 对取回的地址进行简单译码(Mini-Decode)
- 简单的分支预测(Simple-BPU)
- 生成取指的PC
- 根据PC地址访问ITCM或BIU
为了进行快速、连续不断的取址,做了以下优化:
假定绝大多数取指发生在ITCM中,主要使用ITCM进行指令的存储以满足实时性的要求
ITCM使用单周期访问的SRAM
对于从外部存储器中读取指令的特殊情况,IFU可以通过BIU使用系统存储接口访问外部存储器
这种情况下无法做到单周期访问,但这种情况很少,所以不做优化
要求软件应当利用绝大多数取指发生在ITCM中的假定进行设计
IFU直接将取回的指令在同一个周期内进行部分译码,如果显示当前指令为分支跳转指令,则IFU直接在同一个周期内进行分支预测
这个优化涉及Mini-Decode和Simple-BPU两个模块,会在后面详细介绍
由于同一个周期内完成ITCM内取址、部分译码、分支预测、生成下一条待取指令的PC等操作,处理器主频会受到一定影响
采用最简单的静态预测机制
对向后跳转的条件分支指令预测为真的跳转,向前的指令则不跳转
Mini-Decode模块
源码保存在/rtl/e203/core/e203_ifu_minidec.v文件
此处的译码不会完整译出指令的所以信息,只需要译出IFU所需的部分指令信息,包括此指令是属于普通指令还是分支跳转指令、分支跳转指令的类型和细节
模块内部例化调用一个完整的decode模块,但是将其不相关的输入信号接零、输出信号悬空,从而让综合工具能将完整模块中的无关逻辑优化掉,这就是Mini-Decode。这样可以避免同时维护两份Decode模块导致出错
源码如下:
1 | module e203_ifu_minidec( |
Simple-BPU机制
用于进行简单的分支预测,源码位于/rtl/e203/core/e203_ifu_litebpu.v
总体上分成三个部分:
- JAL:直接跳
- Bxx:后跳前不跳
- JALR:使用rs1索引的操作数作为基地址,根据操作数的不同再分成三个小部分
- x0:直接使用x0+偏移地址立即跳转
- x1:不需占用寄存器读端口,直接获取寄存器中的值,进行RAW相关性判断
- xn:需占用寄存器读端口,先判断读端口空闲,再进行RAW相关性判断
所有的PC共享同一个加法器,在这一阶段生成加法器的两个操作数,不管前面的分支如何,总是使用立即数表示的偏移量作为一个操作数;使用三种情况下从regfile或其本身PC读出的操作值作为另一个操作数
源码如下:
1 | module e203_ifu_litebpu( |
PC生成
PC生成逻辑模块用于产生下一个待取指令的PC
该模块源代码存放在/rtl/e203/core/e203_ifu_ifetch.v文件
蜂鸟e200将PC生成分为了四种情况
复位后第一次取指
默认使用CPU-TOP顶层输入信号
pc_rtvec
指示的值作为第一次取指的PC值通过在SoC顶层集成时将此信号赋不同值来控制PC的复位默认值
顺序取指
根据当前指令是16位还是32位来判断自增值
16位:PC=PC+2
32位:PC=PC+4
分支指令取指
使用Simple-BPU预测跳转的目标地址
来自EXU的流水线冲刷
使用EXU送来的新PC值
1 | // 控制PC自增 |
访问ITCM和BIU
蜂鸟E203支持16位压缩指令子集,在32位与16位指令交错的情况下,IFU使用位于/rtl/e203/core/e203_ifu_ift2icb.v的非对齐访问逻辑模块来进行处理
蜂鸟E200采用剩余缓存技术来处理非对齐指令取指
- IFU固定取指32位
- 如果访问ITCM,由于ITCM是由SRAM构成的,上次访问后的输出值将一直保存,这一过程称为Hold-up,利用这一特点省略一个64位寄存器的开销:ITCM的SRAM(64位)输出为一个与64位地址区间对齐的数据,这里称为Lane,由于CPU取指位宽32位,会连续两次或多次访问同一个Lane,这里第二次访问利用Hold-up特点,直接读取其保持不变的输出来避免重复打开SRAM
- 如果顺序取指时一个32位指令非对齐地跨越了64位边界,则会将SRAM当前输出的最高16位存入16位宽的剩余缓存,然后开始正常的拼接取指(参考之前所说使用剩余缓存保存上次取指后没有用完的比特位供下次使用)。因此可以只用一个周期的ITCM访问来取回32位指令
- 对于分支跳转指令或流水下冲刷情况下的取指,需要连续发起两次ITCM读操作,这种情况下的性能损失无可避免
1 | // 处理非对称取指情况使用FSM控制 |
蜂鸟e203的取指单元IFU、指令紧耦合寄存器ITCM、总线接口单元BIU分开实现,IFU使用ICB协议,相关协议接口存放在/rtl/e203/core/e203_ifu_ift2icb.v
基本上,IFU有两个ICB接口,一个64位的用于访问ITCM,一个32位的用于访问BIU
CPU会根据IFU访问的地址区间进行判断是要用ITCM-ICB还是BIU-ICB进行访问
比较判断的代码片段如下
1 | // 使用比较逻辑判断高位基地址与ITCM基地址是否相等,如果相等则进行访问 |
ITCM的特殊点
E200采用数据宽度64位的单口SRAM组成,其大小和基地址可以通过config.v
中的宏定义参数配置
64位的SRAM在物理大小上比32位的SRAM面积更紧凑,且同一时钟频率下可减少动态功耗