性能建模分享¶
-
硬件与gem5通用测试程序的生成
-
负载程序要求:要求是ELF
-
需要是支持的ISA+ABI组合,RISC-V 只接收 Riscv32/64 + Linux
-
elf内部布局没有特别要求,正常用户态elf,需要有加载段PT_LOAD,e_entry等,可选PT_INTERP(动态链接)
-
-
程序入口:ELF内部的e_entry,一般对应bootloader里面设置的_start
-
程序加载:
-
python层根据isa和sys创建一个se workload,然后交给c++实例化一个对象。ELF相关类负责解析这个对象,从中获取entry,内存布局,把程序搬到内存中。激活线程,初始化页表等...
-
接着初始化栈,初始pc值:通过 Process::getStartPC() 返回值:如果 ELF 有 PT_INTERP,就先跳到解释器入口,即动态链接程序先跑 ld.so;否则跳到主程序入口即_start。
-
-
程序退出:分为两种,exit syscall和m5_exit
-
Exit syscall:走系统调用,根据系统调用号找到对应的处理程序,halt当前线程,如果系统已经没有活动线程则退出模拟
-
m5_exit:执行gem5的特定op,直接退出模拟
-
例子
_start:
# 控制寄存器初始化
li x3, 0x70013
csrs mcor,x3
# enable write allocate
# li x3, 0x4
# csrs 0x7c1,x3
li x3, 0x11ff
csrs mhcr,x3
# performence counter初始化
li x3,0x1
csrs 0x323, x3 # mhpmcnt3 L1 ICache Access
...
# 设置栈指针
la sp, stack_top
# gpr初始化为0,避免仿真出现X
li x1,0
...
# 跳转到main函数,你自己的代码
li a0, 0
call main
exit:
fence
li a7, 93
li a0, 0
ecall
踩过的坑
-
链接错误:如果执意使用m5_exit,链接时需要gem5提前编好的一个.so,比较麻烦,后改用exit syscall解决
-
执行时pagefault:一般是某个段没有分配地址,需要检查ld脚本,什么算是分配一段地址?下面二者的区别
```Plain Text
link script 1¶
ENTRY(_start) SECTIONS { .text : { */bootloader.o(.text) *(.text) *(.rodata) *(.data) *(COMMON) } .bss : { *(.bss) } heap_low = .; . = . + 0x1000000; heap_top = .; . = . + 0x1000000; stack_top = .; }
link script 2¶
ENTRY(_start) SECTIONS { .text : { */bootloader.o(.text) *(.text) *(.rodata) *(.data) *(COMMON) } .bss : { *(.bss) } .heap_low :{ . = . + 0x1000000; } heap_top = .; .stack :{ . = . + 0x1000000; } stack_top = .; } ```
-
gem5执行没问题,硬件仿真时出现X信号:gpr没有初始化,在硬件中会表现为X值,而不是0
gem5执行后有用的信息
-
时间与总量:模拟时间、频率、提交了多少条、ipc(instructions per cycle)
-
指令组成:
-
Load 占比 = numLoadInsts / numInsts
-
Store 占比 = numStoreInsts / numInsts
-
Int 占比 = numIntInsts / numInsts
-
FP 占比 = numFpInsts / numInsts
-
Vector 占比 = numVecInsts / numInsts
-
Branch 占比 = numBranches / numInsts
-
条件分支占比 = committedControl::IsCondControl / numInsts
-
-
分支行为:主要关注btb和预测准确度
-
BTB
-
直接分支
-
间接分支:指的是跳转目标地址需要从寄存器中读取,比如jalr
-
RAS
-
Loop predictor
-
-
cache行为:L1/L2
-
总的access次数
-
总的hit次数
-
总的miss次数
-
-
DRAM行为
处理器前端和后端的概念
-
前端:负责“把活接进来、看懂、分发出去”
-
后端:负责“真正干活、算结果、访问数据、写回结果”
gem5仿真速度?
-
90w insts/s
-
后续可以通过simpoint、kvm等加速?
测试程序的选择?
理想情况下就是根据真实负载,比如别的组出的case,在gem5上面跑,跑完和硬件对比。但是目前gem5还没有接入仿真器,硬件环境也没有就绪。我们只能对单个CPU进行测试,衡量这个CPU建模是不是够准确。
测试程序的底层逻辑是,需要是一个可控的,参数化的行为发生器,通过这种测试程序能够区分开准确的建模和模糊的建模
-
用一小组很明确的计算/访存/控制流模式,持续重复执行足够长时间。
-
每种模式只突出一类主要硬件行为,尽量压低其他干扰因素。
-
所有关键行为都能用少量参数调节,从而扫出一条“响应曲线”,而不是只看一个点。
-
程序行为在每次运行中尽量一致,这样 gem5 和真实硬件的差异才更可能来自建模,而不是来自程序随机性或 OS 噪声。
性能建模:如何最大程度对齐到微架构,从手册里面获取各种需要的参数信息,总结为一个表。但是手册不全,一般只会给出大概的信息,然后建模需要一些很具体的参数,比如btb有多少个entry,物理寄存器有多少个,需要从RTL中获取。如果没有RTL,就需要通过测试程序,进行CPU逆向,试出来这些参数了
在进行性能建模时,需要关注这些问题
-
性能差异较大时,如何找出怀疑的地方,性能差异出现在gem5的哪些指标
-
不同指令的延时测量,计算指令延时很好测量,但是到了O3CPU时,load/store指令,retire时通常不代表真的完成了load和store
-
如果模拟器架构和待建模的cpu架构有差异,是修改模拟器架构还是修改模拟器参数?
- 迭代到后面,模拟器的规模也非常大,修改架构可能不比修改rtl简单,如何选择
gem5与硬件的校准体现在两个地方
-
gem5与硬件都有的架构:参数高度保持一致
-
gem5与硬件不共有的架构:先评估是否由于此处的误差,从而影响了整体程序的误差。如果是,能否在不修改gem5架构的情况下逼近
粗糙的对齐方法:
简单对比gem5与硬件的统计数据,一个个猜可能出现误差的地方,比如发现icache的访问次数不对,就要怀疑这一整条链路上,prefetcher、icache配置(way,大小,tag和data latency,lsq等),一个个改参数看是否能缩小差异
此过程需要先分析理论值、再看硬件和gem5
906例子:程序特征是lw多,sw少
- icache访问次数对不上,gem5差不多是906的⅓,首先怀疑每个周期取指令的宽度是否一样,是否有合并icache访问次数这一说法。后来发现是gem5每次默认取一个cacheline,而不是一条指令,将
fetch1LineSnapWidth从0(这个值为0时代表使用cacheline size)改到了8,这样就能实现每次取两条指令,不过这种地缺点是没有模拟如果这两条指令是跨cacheline的,则只会取第一条指令,第二条下次再取,但是误差很小可以不考虑
修改之后icache次数从 413526 -> 1303136,十分接近硬件的1305959
-
dcache访问次数对不上:906是gem5的½。首先还是怀疑访问粒度不一样,但是check了一下发现都是正确的,然后查看preftch,cache配置等都没有发现问题,只能怀疑是不是硬件有合并load或者store访存的机制,又或者是统计口径就不同
-
硬件有合并load/store机制?
-
有load-store forwarding(后续的load请求命中store buffer,直接返回数据,不会加一次cache访问请求),但是gem5这种情况会增加一次cache访问请求
-
有store同target合并,gem5没有,这也是误差之一
-
-
以上次数都能对上之后,发现时间差得还是比较多,于是写了脚本,记录gem5中每条指令执行时间,与硬件每条指令执行时间进行对比,发现大部分误差来源于lw
- dcache访问时间对不上
首先分析各个功能单元指令数量占比
然后分析时间瓶颈,分析每个pc的执行次数和执行时间,主要是在cache,从0.0048降到0.0026,一共有三次主要修改

-
将dcache的data latency和tag latency从2改为1,tag和data并行访问,修改之后从0.0048->0.0038
-
对icache的data latency和tag latency进行同样的修改,修改之后从0.0038->0.0033
-
允许outstanding的memory请求,即允许上一个lw还没有完全返回,下一个lw也发到lsq上,修改之后从0.0033->0.0026
发现有很多连续的lw,906一般一个cycle就处理了一条load,而gem5通常是需要3~4个cycle。
920例子;
-
l1 icache访问次数:硬件值 2988328,gem5 1744278
-
l1 dcache load miss:硬件值 79207,gem5 114743
-
gem5比硬件高了45%,gem5 miss太多,可能原因
-
排查下load access是否对齐,是的
-
prefetch对齐?是否是因为硬件的prefetch提前将数据load进来,导致miss减少?
-
推测:硬件对load miss有合并机制,write a case verify it?
gem5也有合并机制,可能是gem5的load miss合并不够,有一个奇怪的点,miss次数更多,但是耗时反而更少,why?
-
miss合并机制在gem5中表现为mshr,如何逼近到硬件的mshr? 当同一个 cache block 已经有一个未完成 miss 时,后续对同一 block 的请求会命中已有 MSHR,而不是再新开一个 miss
-
mshr指标是什么:
-
通过把mshr不变,tgts_per_mshr变小,既达到了减小miss次数,也达到了时间延长,why?
-
-
dram未对齐
硬件没有建模,只是外挂了一个vip,不存在真实的DRAM
-
但是内部DDR4有合法性检查,不能真的把延时设为0,那么把这个延时设置得很小,ps级别,基本可忽略
- 为什么仅仅修改延时,也会导致访存次数这些的改变
-
尝试更改ddr4为一个简单memory,但是这会导致访存次数这些也有很大的改变,why?
- ddr内部有bank,以及对burst的拆分
-
这里的CPU逆向其实和测试程序的选取关系很大,首先介绍下CPU架构逆向的定义
-
已知微架构采用某种设计,但是不知道其具体的设计参数
-
不确定微架构采用的设计,给出一些可能的设计,逐个排除和确认
我们建模时遇到的问题主要还是第一种,遇到这种问题时,需要选用或者编写合适的benchmark
需要思考的问题
-
针对什么微架构部件
-
针对该部件的什么设计参数
-
反映出现差别的指标是什么
-
程序在什么情况下会导致瓶颈出现,程序的参数如何对应到设计参数
-
如何校准指标?
比如上面的 L1 DCache 容量的测试上,这几个要素的回答是:
-
针对什么微架构部件:L1 DCache
-
针对该部件的什么设计参数:L1 DCache 的容量
-
反映出现瓶颈的指标是什么:时间,周期数,缓存缺失次数
-
如何构造程序来导致瓶颈出现:在内存中开辟数组,然后不断地扫描访问
-
程序在什么情况下会导致瓶颈出现:数组大小超过 L1 DCache 容量
-
程序的参数如何对应到设计参数上:数组的大小对应到 L1 DCache 的容量
逆向的意义不仅仅在于逆向,还在于对齐微架构参数,以及暴露单一因素是否是造成性能差异的原因。当一个benchmark跑上去时,很可能某个地方跑的时间多了,但是因为另一个未对齐地方执行时间更少,从而掩盖了这部分差异,如果需要针对任何负载程序都做到非常精确,这种一个个小部件的对齐是很重要的。
举个例子:https://zhuanlan.zhihu.com/p/720136752
假如不知道CPU每个周期取多少条指令,怎么测量呢。我们是不是应该设计一个程序,我们需要找到一种方式精确控制取回的指令的数量,并让其不足以供给后端的指令执行从而产生瓶颈。
cache的平均访存时间,其实很难测量