高性能处理器设计与快速迭代方法
主要讲一下现代高性能处理器设计与性能迭代方法
简单综合一下几个月的实习学习结果
现代高性能处理器设计思路
超标量乱序执行
高性能的基石,提高ILP
现代处理器后端一般结构比较固定
使用重命名算法消除指令假依赖,使用保留站乱序发射指令
deocde->rename->dispatch->issue->execute->writeback->commit(retire)
处理器后端基本技术
- 寄存器重命名
- 推测快速唤醒及replay机制
现代处理器还会额外使用基于寄存器重命名的mov消除技术,进一步提高ILP
推测唤醒预测器
一条指令从被唤醒到写回一般需要以下几个周期
wakeup->select->issue->readData->execute->writeback
如果我们需要指令背靠背执行
则需要在第一条指令写回时,就已经在执行第二条指令了
所以我们需要对第二条指令进行推测唤醒
对于单周期执行的指令来说
只需要在第一条指令select时,就对其他指令进行唤醒即可
但是对于某些多周期指令(访存指令)来说,这个唤醒的时机并不确定
当然,我们可以简单的认为访存指令一定会命中L1Cache,再进行唤醒
考虑到实际情况,L1Cache的命中率较高,因此可以采取此方法
当然,也可以提前预测访存指令能否命中L1Cache,再决定唤醒其它指令的时机
强大的分支预测
现代超标量乱序处理器流水长度基本在10-15之间,分支惩罚严重
因此需要分支预测器减少惩罚
现代高性能处理器往往有着非常复杂的分支预测器
目前主流的是混合式预测器,即多个预测器共同作用
香山处理器前端使用到了多个预测器
按照速度可以分类为:
- 能一个周期内得到结果的快速预测器,但是准确率差
- 准确率更高,但是需要多个周期才能得到结果的复杂预测器
这里主要挑复杂预测器来讲一下
TAGE预测器及其变体
TAGE预测器的核心思想是不同的程序段可能会有不同长度的分支历史相关性
使用一个准确性计数器来决定某一个分支使用较长的全局分支历史进行预测还是使用较短的分支历史
比如有的程序段比较看重全局分支历史,有的程序段则比较看重局部分支历史
本质上是不同的分支历史长度对某一条分支指令的预测准确率
TAGE可以通过训练来权衡一条分支指令使用哪种分支历史长度的预测结果
TAGE还有不同变体,但本质上原理相似
相关论文
A case for (partially) tagged geometric history length branch prediction
A 64-Kbytes ITTAGE indirect branch predictor∗
解偶的取指前端
解偶的取指前端通常将取值与分支预测分离
因为分支预测速度实际上比取指快,如果等到取指取出指令再预测,可能会导致很多bubble
解偶的分支预测器可以提前取指进行预测,预测更未来的指令流
以供应足够的指令
(论文名我忘了,关键词:decoupled frontend,stream fetch engine)
乱序访存
访存指令是限制现代处理器IPC的最大瓶颈
根本原因是存储器速度与高性能处理器速度的不均衡
因此现代处理器提高性能的方法之一就是提高MLP(memory level parallel)
由于访存不能像寄存器重命名那样消除指令假依赖
因此对于乱序访存,我们需要保证访存指令的依赖正确
一般情况下我们需要做访存依赖预测
如果一条访存指令被预测为无依赖,则直接执行
如果被预测为有依赖,则需要等待所依赖的访存指令执行完成才能执行当前的访存指令
storeset访存依赖预测器
相关论文
Memory Dependence Prediction using Store Sets
快速性能迭代
核心原理
当我们想知道某一项技术在我们的处理器上是否能带来性能提升时该怎么做?
很简单
将这项技术添加到处理器rtl中
使用rtl仿真器运行benchmark得到性能结果
将其与原始性能数据进行对比,我们就能知道这项技术是否有用
但最大的问题是
rtl代码修改麻烦,debug缓慢,往往需要几个月的时间
同时rtl仿真器运行极其缓慢(运行完整的spec06可能需要几年时间)
假如该技术没有任何作用,则意味着我们浪费了大量时间做无用功
假如我们能找到一种方法,能够快速(一个星期)得到修改后的性能结果进行比对
我们就能节省大量的无用功
因此除了rtl仿真器之外,我们需要一种速度更快的模拟器
gem5模拟器
当我们使用hdl写好处理器后
需要进行仿真或者综合后在fpga上运行得到结果
以便测量处理器水平,方便进一步改进
但是目前rtl仿真器仿真速度慢,fpga综合慢,价格高
因此往往会使用速度更快的性能或时序模拟器来进行模拟运行
下面是几种处理器模型
模拟/仿真器种类 | 模拟精度 | 模拟速度 | 用途 | 举例 | 使用便捷性 |
---|---|---|---|---|---|
isa功能模拟器 | 无 | 极快 | 处理器行为验证 | nemu/qemu | 高(只用于行为验证) |
通用性能模拟器 | 较高 | 快 | 处理器性能验证 | gem5 | 中(需要进行性能对齐) |
时序模拟器 | 高 | 慢 | 处理器性能验证 | (一般是根据处理器模型进行搭建) | (需要自行搭建) |
rtl仿真器 | 极高 | 极慢 | 处理器rtl验证 | iverilog/verlator | 高(自动生成rtl仿真模型) |
实际上,性能测试往往可以与处理器架构分离
即模拟器使用a种架构,而处理器则采用b中架构
因此不可避免的会存在一定的性能测量误差(一般在10%左右)
但是仍能够反映处理器当前的性能
这点我们则可以使用gem5通用性能模拟器
在使用之前我们需要将gem5与待测试的处理器rtl进行性能对齐,以便得到更高精度的性能对比结果
但即使是gem5,运行完整的spec06仍然需要花上不少时间(一周左右)
有没有更快的方法,能够让性能测算时间缩短到一天之内?
checkpoint采样
实际上一个程序往往有不同的特征点
比如,在某一段时间内,分支指令比较多,在另一段时间内,访存指令比较多
当然实际可能不会按这样划分
但是把一个程序分成不同的子程序段的核心思想在于:
处理器运行一个程序的完整性能反映了处理器对该程序的特征的处理速度
比如(只是举例,实际checkpoint采样会使用特殊算法):
我们将一段程序均匀的分为100份,然后从中均匀挑选20份程序段(checkpoint采样)
我们再运行这20份checkpoints,得到20份局部性能数据后,进行加权和,就能够得到与完整程序的性能结果相似的性能数据
并且精度可达90%以上,由于实际上并没有运行完整的程序,因此性能测算时间可进一步减少
warmup 预热
当我们得到checkpoint后,准备运行时
还需要做的一份十分重要的工作是预热,预热是什么下面会讲,我们先来看看为什么需要预热
运行完整程序与运行checkpoint的最大区别在于:
运行完整程序时,处理器中的分支预测器状态,cache等组件是一直都在更新的
在到达采样点时,分支预测器和cache等组件内部是处于更新好的状态
而直接运行checkpoint时,处理器中的状态一开始是空的,分支预测器和cache等组件都是处于初始状态
因此在运行checkpoint时,我们需要提前运行一小段程序来将处理器组件的状态变为与运行完整程序到达该采样点时的状态
这样我们才能得到更准确的性能数据
一般流程则是:
warmup->开启性能计数器->运行checkpoint
实际上warmup所用的程序可以直接使用checkpoint,但不开启性能计数器
warmup 功能预热
这是warmup的拓展,实际处理器在warmup的时候,可以只预热比较重要的组件
比如:分支预测器,访存依赖预测器,L1,L2Cache等
而这些组建实际上都位于处理器前端和访存模块,与后端执行组件无关
因此在warmup时,我们可以将处理器后端执行组件切换为简单模型,比如功能模型
以此来提高预热速度
在正式运行checkpoint时再将后端功能模型切换为完整模型,以此来提高精度
快速性能迭代总结
使用gem5性能模拟器
使用checkpoint加快程序运行速度
使用warmup功能预热加快预热速度,同时几乎不影响性能测算精度