ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

gem5缓存预取仿真全指南:从访存路径到aarch64跑SPEC2006

gem5缓存预取仿真全指南:从访存路径到aarch64跑SPEC2006 研究缓存预取逃不开gem5这句话我在不同场合说过很多次。无论你是想复现一篇ISCA/HPCA上的prefetcher论文还是想在毕设里加一种自己的预取策略gem5几乎都是第一站。但现在网上关于gem5 prefetcher的资料非常分裂老教程还在讲getNextPacket()新版本接口已经改了两轮有人用经典内存系统有人用Ruby两边配置完全不通用。这篇文章想把这块短板补上以gem5 21.2为基准从访存路径、配置入口、自定义实现到aarch64完整系统下跑SPEC2006把一条可以落地的实验链路完整串起来。适合正在做体系结构方向的研究生也适合想快速验证缓存预取思想、但被模拟器折腾到头疼的工程师。1. 访存延迟这座墙prefetcher能拆多少1.1 缺失一次到底要赔多少cycle刚接触模拟器的人往往低估缓存缺失的代价总觉得“少了一次访问无非多等会儿”。但真实处理器流水线的账不是这么算的。拿一个典型O3核心举例L1命中延迟大约4个cycleL2命中延迟大约20到30个cycle而访问内存的延迟通常在200到400个cycle。一次L2缺失意味着CPU要在这条load/store上停顿几十上百拍而现代应用内存访问占比并不低missing多到一定程度流水线根本填不满。沿这条路径往下推就会理解prefetcher的目标不是“提升缓存命中率”这么宽泛而是把miss延迟从访存关键路径上挪走。它提前把数据搬到离核心更近的缓存里等真正要用的时候load指令看到的是hit流水线不用停。这个思路在单核、多核都成立区别只是复杂度多核还牵扯缓存一致性、存储层次和带宽分配这就是后面要讲Ruby路径的动机。看数字最直观。假设某benchmark的L1 miss率是10%而L2 miss率是20%以1000次访存为例配置L1 miss次数L2 miss次数粗略访存代价无预取基线1002020×300 6000 cycle预取覆盖一半L2 miss1001010×30010×25 3250 cycle这还没算流水线重叠度、MSHR资源和带宽占用但数量级已经说明问题了只要预取器选得准省下的延迟非常可观。这也是为什么学术界每年还有大量prefetcher论文模拟器里的prefetcher实验平台始终是刚需。1.2 预取的本质用带宽换延迟关键是accuracy和timeliness预取不是“多拿几个块”那么简单。它本质上是用额外消耗的访存带宽提前去换取未来的hit。如果预取的数据最终没用上那它不仅浪费了带宽还挤占了有用的缓存行可能把本来该留下的数据替换出去反而增加miss——这就是俗称的prefetch pollution。评估一个预取器是否有效业界一般看三个量accuracy预取块中有多少被真正访问了、coverage被预取覆盖掉的miss占总miss的多少、以及timeliness预取到达的时间是不是刚好在访问发生之前。三者是互相制约的比如狂发预取可以把coverage做高但accuracy必然下降同时把MSHR和带宽打满最终端到端性能不一定变好。gem5的好处是这些矛盾都能在模拟环境中被定量地看到每个prefetcher的命中、发出数量、被使用情况都有统计项可查。gem5里prefetcher的触发时机也值得先说透分为on-miss只有访问缺失才触发、on-access无论命中还是缺失都触发、on-fill只有新数据块填入缓存时才触发。默认情况下gem5很多预取器是on-access的但是否可配置取决于具体实现。而这些行为直接决定了后面的实验设计——你不能拿一个on-fill的prefetcher去和on-miss的prefetcher直接比命中率。2. gem5里挂prefetcher的两条工程路径经典缓存与Ruby第一次在gem5里找prefetcher的人经常被两个完全不同的目录搞晕src/mem/cache/prefetch/和src/mem/ruby/prefetch/。这俩不是重复代码而是gem5两套内存系统的产物。2.1 经典路径Cache对象里加一行配置经典内存系统Classic Memory System里L1、L2缓存各自是一个Cache对象prefetcher挂在这个对象上。最省事的接入方式是命令行参数./build/ARM/gem5.opt configs/example/se.py \ --cpu-typeO3CPU --caches --l2cache \ --l1d_prefetcherTaggedPrefetcher \ --l2_prefetcherStridePrefetcher对应到Python配置本质就是给缓存对象赋值from m5.objects import StridePrefetcher system.cpu.dcache.prefetcher StridePrefetcher() system.l2.prefetcher StridePrefetcher()经典路径的预取逻辑在src/mem/cache/prefetch/下每个prefetcher由一对cc/hh文件加一个SimObject Python文件组成。这个路径最大的优点是好调试prefetcher被直接注入到缓存访问流程里行为直观统计项也集中在缓存对象里想看某个地址有没有被预取打个debug就能追到。2.2 Ruby路径控制器内基于消息的预取Ruby路径则完全不同。Ruby把存储系统建模成网络中的若干个控制器Controller每个控制器内部通过有限状态机响应不同的请求消息。prefetcher被挂到控制器上比如L1Cache_Controller、L2Cache_ControllerL1Cache_Controller( ... prefetcher stride_prefetcher, )Ruby的prefetcher基类在src/mem/ruby/prefetch/Prefetcher.hh核心接口也和使用方式与经典路径的BasePrefetcher不一样它是在控制器处理某个请求时调用observeMiss或observeHit然后返回一个预取地址列表由控制器决定把这些地址包装成什么消息发到网络上。两者最大的差异在于Ruby的预取是显式占用网络资源的因为预取请求也会像正常请求一样在互连网络里流动、占据消息缓冲区整个系统的时序和带宽模型更真实。经典路径的预取请求相对“局部”主要影响缓存内部和存储层次网络建模没有Ruby精细。2.3 怎么选看你的研究问题卡在哪一层选哪条路径本质上取决于论文/项目要论证的点。维度经典路径Ruby路径预取器实现难度较低接口清晰较高要理解控制器状态机内存时序精度中高多核一致性建模较弱强网络/DRAM建模简单详细适合研究问题预取算法本身预取与一致性/网络/DRAM的交互如果只是研究“某种stride/PCS/签名预取在单核上的效果”经典路径足够而且跑得快很多。如果研究的是多核场景、一致性协议对预取的影响、或者在DRAM控制器里的预取那Ruby才是合适的实验室。我自己的习惯是先经典路径快速验证算法收益确定有价值后再往Ruby搬。直接上手Ruby写预取器光是在SLICC/控制器里排查一个时序问题可能就耗掉一周。3. 手写一个Stride2Prefetcher从接口到编译跑通网上大量旧教程教你继承BasePrefetcher然后重写getNextPacket()但gem5 21.x之后这套接口已经改掉了。现在核心回调是calculatePrefetch它的输入是一个PrefetchInfo对象输出是一票带优先级的候选地址。新版还提供notifyHit、notifyMiss、notifyFill等事件钩子让预取器能感知缓存访问状态。3.1 新版BasePrefetcher的核心回调PrefetchInfo里包含了触发预取的访存信息常用的有getAddr()请求的物理地址、isSecure()、以及PC信息。calculatePrefetch接收它然后往std::vectorAddrPriority addresses里塞候选地址。AddrPriority是个地址优先级的二元组优先级数字越小通常代表越优先发射。以Stride2Prefetcher为例它假设访存地址按固定步长跳动于是把一个块的地址加上stride*blockSize、2*stride*blockSize作为预取目标。3.2 完整代码与SimObject注册在src/mem/cache/prefetch/下新建stride2.hh#ifndef __MEM_CACHE_PREFETCH_STRIDE2_HH__ #define __MEM_CACHE_PREFETCH_STRIDE2_HH__ #include mem/cache/prefetch/base.hh #include params/Stride2Prefetcher.hh namespace gem5 { class Stride2Prefetcher : public BasePrefetcher { protected: int stride; int degree; public: Stride2Prefetcher(const Stride2PrefetcherParams p); void calculatePrefetch(const PrefetchInfo pfi, std::vectorAddrPriority addresses) override; }; } // namespace gem5 #endif对应stride2.cc#include mem/cache/prefetch/stride2.hh #include base/logging.hh #include params/Stride2Prefetcher.hh namespace gem5 { Stride2Prefetcher::Stride2Prefetcher(const Stride2PrefetcherParams p) : BasePrefetcher(p), stride(p.stride), degree(p.degree) { } void Stride2Prefetcher::calculatePrefetch(const PrefetchInfo pfi, std::vectorAddrPriority addresses) { Addr blk pfi.getAddr() / blkSize; for (int d 1; d degree; d) { Addr target (blk d * stride) * blkSize; addresses.push_back(AddrPriority(target, d)); } } } // namespace gem5还需要SimObject定义Stride2Prefetcher.pyfrom m5.SimObject import SimObject from m5.params import * from BasePrefetcher import BasePrefetcher class Stride2Prefetcher(BasePrefetcher): type Stride2Prefetcher cxx_header mem/cache/prefetch/stride2.hh stride Param.Int(1, Prefetch stride in cache blocks) degree Param.Int(2, Number of prefetch addresses per trigger)最后在SConscript里把新文件注册进去SimObject(Stride2Prefetcher.py) Source(stride2.cc)3.3 编译与快速验证编译整个ARM版本scons build/ARM/gem5.opt -j8第一次编译建议手动指定-j$(nproc)但注意内存别太小8GB内存开20并发很容易OOM。编译通过后先用SE模式快速验证逻辑./build/ARM/gem5.opt configs/example/se.py \ --cmd/bin/ls --caches --l2cache \ --l1d_prefetcherStride2Prefetcher \ --l2_prefetcherStride2Prefetcher跑完看stats.txt用grep -i prefetch m5out/stats.txt搜索预取统计。正常情况下你能看到预取器发出了多少个预取请求、有多少最终被访问。如果预取数量为0先检查代码里blkSize是否设置正确再看缓存是否使能了L2——很多“不生效”其实是忘加--caches或--l2cache。4. 在aarch64下拿SPEC2006做预取对照实验SE模式可以验证预取器的基本行为但既然热词里都提到了aarch64下跑SPEC2006说明大家真正关心的是完整系统下的真实效果。这一步确实更繁琐我把完整链路拆开讲。4.1 交叉编译SPEC2006与构建aarch64根文件系统完整系统FS需要三样东西aarch64内核、带用户态工具的磁盘镜像、以及交叉编译好的benchmark。交叉编译工具链直接装sudo apt install gcc-aarch64-linux-gnu内核从kernel.org拉源码后make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- defconfig make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- Image -j8rootfs建议用busybox拼一个最小的然后dd一块空镜像、mkfs.ext4、mount后拷入busybox和benchmark。SPEC2006的交叉编译是很多人卡壳的地方先source shrc设置SPEC环境再写一个gem5-aarch64.cfg配置target、CC、CXX例如target aarch64-linux-gnu CC aarch64-linux-gnu-gcc CXX aarch64-linux-gnu-g然后用runcpu --configgem5-aarch64 --actionbuild 400.perlbench这类命令逐个构建。构建好的二进制和输入数据同样拷入镜像。这一步没有任何捷径SPEC2006的规则比较老有些benchmark需要手动处理库路径和-static选项我当时在456.hmmer上就折腾过一整天。4.2 启动FS并挂载自定义prefetcherfs.py同样支持--l1d_prefetcher和--l2_prefetcher./build/ARM/gem5.opt configs/example/fs.py \ --kernelvmlinux \ --disk-imagespec-aarch64.ext4 \ --cpu-typeO3CPU \ --num-cpus1 \ --caches --l2cache \ --l1d_prefetcherStride2Prefetcher \ --l2_prefetcherStride2Prefetcher启动后用m5term连上gem5默认的端口通常是3456在aarch64的shell里挂载磁盘、运行benchmark。这里有个关键动作在benchmark主体开始前用m5 resetstats结束后用m5 dumpstats这样才能得到干净的ROI统计否则你统计的是整个启动过程和benchmark混在一起的数据。4.3 实验矩阵设计跑哪些benchmark、控制哪些变量预取实验必须做对照否则单跑一个配置没法说明问题。推荐的最小矩阵配置编号说明预取器设置A基线无预取不设任何预取器B单级L1D预取--l1d_prefetcherStride2PrefetcherCL1DL2双级预取--l1d_prefetcherStride2Prefetcher --l2_prefetcherStride2Prefetcherbenchmark建议从整数集中挑4到6个有代表性的比如400.perlbench、401.bzip2、429.mcf、456.hmmer。不要一上来跑全量gem5 FS下O3CPU模拟SPEC2006是非常慢的跑完一个ref输入可能要数小时甚至更久。工程做法是先跑test或train输入验证流程最后再对选定benchmark跑长数据或者用--maxinsts500000000限制指令数配合ROI统计也能得到有参考价值的结论。4.4 从stats.txt提取关键指标跑完一轮后m5out/stats.txt里重点看这几项simSeconds端到端模拟时间最直接反映性能。system.cpu.dcache.overallHits::total与overallMisses::totalL1D整体命中/缺失。system.l2.overallMisses::totalL2 miss的变化。system.cpu.dcache.prefetcher.*该预取器自身统计。对比A和B两组数据时先看simSeconds是否下降再看L2 miss是否下降。如果miss降了但simSeconds没降说明预取太激进或者timeliness太差预取到的数据没有被及时使用资源白费了。5. 结果怎么看才靠谱统计量与踩坑复盘5.1 不同版本统计项名称差异gem5不同版本对prefetcher统计项的命名差异极大。老版本有demandMisses、prefetchedMisses这类直观字段新版则倾向于把预取块计入overallMisses的某种分类。最稳妥的检查办法是跑完一次实验后在stats.txt里搜所有带prefetch、pf、overall的字段先弄清楚当前版本有哪些统计再做脚本分析不要照搬网上老教程里的字段名。我自己就吃过亏照着旧版写了一个解析脚本在新版统计里直接抓不到数最后发现字段改名叫prefetcher.numHits了规则还不完全一样。5.2 模拟速度太慢的工程应对aarch64 FS本来就很慢x86 host上模拟aarch64还没有KVM加速只能纯指令翻译跑。几个实用手段缩短ROI用m5 resetstats/m5 dumpstats只统计核心区域跳过初始化。限制指令数--maxinsts在到达指定指令数后自动结束模拟。先atomic再timing用Atomic CPU快速验证整个镜像和benchmark跑得通再用TimingCPU/O3CPU作正式实验。经典路径优先如果不需要Ruby的精确网络模型就留在经典路径下Ruby比经典路径通常慢不少。还有一点调试prefetcher逻辑时用gem5.opt加--debug-flagsCachePrefetch可以输出详细预取日志但正式实验一定不要开debug输出日志会拖慢几十倍。5.3 不要被prefetch hit数带偏警惕bandwidth与pollution最后说一个很多实验报告容易翻车的地方只看prefetch hit数字高就下结论说预取器好。实际上预取hit高只能说明预测方向是对的它不能告诉你为了拿到这些hit预取器占了多宽的带宽、挤掉了多少有用的缓存行。两个预取器对比时A的accuracy比B高但coverage低综合下来可能B的端到端性能反而更好而C如果同时发很多预取可能upper到访存带宽上限simSeconds比基线更差。所以在频谱的另一端一定记得看两个间接指标L2整体miss率是否下降以及程序最终CPI/simSeconds是否真的改善。只有端到端时间变好预取才是真的有价值。提示做一个30分钟能跑完的401.bzip2小输入实验把baseline和你自己的prefetcher各跑一遍分别记录simSeconds、L2 miss、prefetch hit三个数。你会发现三者之间的关系经常不是线性的这正是prefetcher研究最有趣也最容易出文章的地方。我在实际项目中还有一个习惯先写一个最小可跑的prefetcher跑通全流程再往里面加状态维护和调参。这比一上来就实现论文里的复杂算法要稳妥得多。gem5里prefetcher的调参空间很大stride、degree、触发时机、是否双级预取协同每一个参数都值得单独做一轮对照实验。当你把这条链路跑熟之后后面换一个prefetcher实现基本就是一天内的事。
返回列表