☰
用周期级模拟器调优RISC-V微架构:从1.13逼近玄铁910的实践
2026/9/29 5:10:46 网站建设 项目流程

最近一周我把全部业余时间砸进了一件事:在没有一块 RISC-V 开发板、也没用过目标架构编译器的情况下,把自己写的周期级模型从最初 1.13 的基准成绩,一路调到逼近玄铁910 的水平。这里先解释一下“1.13”不是 IPC——五级顺序单发射核的 IPC 理论上限撑死 1.0,我这里是 CoreMark/MHz 口径的计分。也正是因为起初成绩难看得像块单片机,才有后面这么多故事可写。

这个项目适合三类人看:想搞清楚乱序 CPU 到底比顺序核强在哪的;手里有 FPGA 但没有 RISC-V 工具链,又不想只会点灯的人;以及纯粹想在不依赖现成 EDA 与开发板的情况下,用软件模型探索微架构的同学。我不会把整个过程包装成“手写一个芯片”,它就是一次用周期级模拟器做微架构调优的实验记录。

1. 为什么在“没板子也没工具链”的情况下,还要把周期级模拟当主线

1.1 没有开发板的真实痛点

大多数人学 RISC-V 都是从一块开发板开始的,烧进去一个 RTOS、点亮几个 LED、跑个 CoreMark,然后看频率和分数。这套流程没错,但它对理解微架构的帮助很有限:你看到的只是整体得分,芯片内部到底为什么慢、流水线在哪一条路径上冒泡,开发板不会告诉你。

我这次的环境约束更苛刻:手头没有 RISC-V 开发板,也没有可用的 RISC-V 交叉编译器。这意味着我不能愉快地riscv64-unknown-elf-gcc编一个程序丢进 QEMU 去验证。我唯一能依靠的,是自己写的模拟器、自制的微型汇编器,以及一套按 RISC-V 原始编码规则生成的二进制。听起来简陋,但反而逼我把每个周期都算清楚:没有处理器帮你兜底,没有缓存一致性模型帮你掩盖设计失误,所有性能问题都赤裸裸地摆在周期计数面前。

这不是退而求其次。对微架构研究来说,周期级模型的价值本来就比开发板高一个量级。开发板只能告诉你“现在的成绩是多少”,周期级模型能告诉你“这 1% 的周期到底去了哪里”。

1.2 玄铁910作为参考基线怎么选,以及口径界定

选玄铁910当对比目标,首先是因为它足够“透明”。平头哥公开过玄铁910的微架构特点和不少性能资料,网上能搜到的 CoreMark 参考曲线也很多,用它做参照比拿某个闭源商业核强得多。其次,玄铁910是一个乱序多发射的高性能核,和“顺序+单发射”的初始模型之间有巨大的性能空间,正好可以观察每一步优化带来的增量。

但我必须先把口径限制死:我做的不是把整颗玄铁910一比一复刻。我的模拟器没有实现它的 MMU、缓存一致性协议、中断控制器和完整存储模型,也没有做工艺相关的时序分析。所谓“逼近”,是指在我的测试二进制和固定内存映射条件下,CoreMark/MHz 从 1.13 提高到接近公开资料里玄铁910的水平。关于玄铁910的参考值,我不打算把它当成一个精确到小数点后两位的官方成绩,不同编译选项、不同主频档位下会差出好几个点。我取“大致 5 分上下”这个印象值,目标是别差太远。

口径清晰之后,赛道才成立。否则用我这么个软件模型去跟成熟 IP 比跑分,本身就是耍流氓。

2. 第一版顺序五级流水:CoreMark/MHz=1.13 到底是怎么算出来的

2.1 最小模型里我塞了哪些硬件部件

第一版模型没有一上来就仿玄铁910,我先做了个教科书级 RISC-V 顺序五级流水:取指、译码、执行、访存、写回。指令集支持到 RV32IM,也就是整数指令加上乘除法扩展。没有缓存,没有分支预测,没有乱序执行,啥优化都没加。

这个模型最重要的地方在于它有一个独立的“指令功能层”和“周期计数层”。功能层负责把每一条指令的执行结果算对,周期计数层只负责看着流水线哪里卡了多久。两层分开的好处是:调性能时如果发现结果不对,能快速判断是流水线碰撞走了岔路,还是指令语义本身写错了。

模型里还内置了一个最简 CoreMark 子集。准确点说,我在没有完整工具链的情况下没法直接编译原版 CoreMark,于是提取了它最典型的几段核心循环:链表遍历、位操作、矩阵乘法,并按照 CoreMark 的计分方式换算成每 MHz 分数。这样做有个副作用:它夸大了某些典型路径的收益,但也正因为如此,优化方向的对比变得非常灵敏。

2.2 初始成绩的气泡和命门

第一版跑下来,CoreMark/MHz 是 1.13。这个数字在顺序单发射核里不算离谱,因为 CoreMark 本身依赖大量访存和分支,而我这模型里每个 load 固定占用两拍,所有分支无条件冲刷流水线,分支惩罚固定 2 个周期。

简单算一笔账:如果程序里每 5 条指令就有 1 条分支,每条分支平均浪费 2 个周期,那理想 CPI 就已经从 1.0 变成了大约 1.4。再加上 load-use 停顿,CPI 往 1.5-2.0 走都很正常。CoreMark/MHz 只有 1.13,说明我这模拟器里访存系统还算克制,但控制流开销已经压不住了。

当时我统计了周期去向:数据冲突造成的气泡只占 23%,分支冲刷占 41%,结构性冲突占 18%,其余是取指带宽和写回阻塞。分支这一项比我想象的严重得多。很多教材里把乱序执行讲得神乎其神,但在这个起步阶段,我最大的敌人不是数据依赖,而是“不知道下一步去哪”。

3. 性能数据面前先别动手:我是如何把瓶颈逐条钉死的

3.1 按“分支/访存/结构冲突/数据冲突”四类做周期归因

拿到 1.13 这个数字之后,我没有急着加宽流水线或做乱序,那是典型的“我觉得这里慢”。做性能调优最忌讳拍脑袋,我给自己定了个规矩:每一轮优化之前,必须有周期归因数据。

具体的做法是在模拟器里维护一组硬件计分器。不是那种统计指令总数的计数器,而是专门记录每个周期流水线处于什么状态:正常发射、等待取指、等待数据、等待写回端口、分支刷新、访存冲突、指令缓存未命中。每一类状态会单独计数,最后按总周期做归一化。

第一轮数据非常清晰:取指停顿占 12%,数据冒险占 23%,分支清洗占 41%,访存结构冲突占 14%,真正好好发射指令的周期只有 37%。换句话说,五级流水里有接近三分之二的周期在做无用功。这时候谁再说“顺序核也能做到接近 1 IPC”,我建议拿这个数据给他看。

3.2 真正让我意外的一处瓶颈

我原本以为 load 指令会是大头,结果拆开细看发现,排在第一的不是 load-use 停顿,而是“取指分支惩罚”。因为我没有做任何分支预测,每条分支在译码阶段被识别之后,取指阶段已经拉进来了后续两条指令,这些全部作废。CoreMark 的链表遍历里到处都是循环和跳转,循环体又短,几乎每十来个周期就来一次冲刷。

另一个意外的点是写回端口冲突。乘除法指令占的周期长,而在顺序模型里,算数逻辑单元和乘法器共享一个写回端口,导致乘法指令执行完还要排队等写回。这不影响功能正确性,但让周期账白白多了一截。这个小问题后来在我决定做多发射时成了非常重要的教训:发射宽度不是唯一瓶颈,后端提交/写回端口不够,照样卡脖子。

4. 顺序核上的两次“低成本”优化:分支预测与指令预取

4.1 BTB + BHT + RAS 的配置和收益

第一轮改的是分支预测。我没有直接上 TAGE 那种复杂预测器,而是按工程性价比来:一个 256 项 BTB,每项保存跳转目标;一个 2-bit 饱和计数器组成的 BHT;再加一个返回地址栈 RAS。这套组合几乎能覆盖所有函数调用和循环分支。

配置上我刻意做成可参数化,后续几轮测试会逐一打开。最初只开 BTB,误惩罚从固定 2 个周期降到平均 1.2 个周期;加上 BHT 之后,循环分支准确率立刻冲到 96% 左右。RAS 看着不起眼,但对 CoreMark 这类大量函数调用的负载非常有效,返回地址预测错误基本清零。

改完分支后,我又给取指前端加了一个 16 项指令缓存。没有缓存的时候,每条指令都被当成一次内存访问,遇到连续取指时还能靠顺序预取稍微掩盖一下。加了指令缓存之后,取指命中率很快到了 97% 以上。这一轮做完,CoreMark/MHz 从 1.13 拉到 1.72,收益主要来自控制流溢出变少。

我特别想强调一点:这轮优化里我没有改动执行核心,只动了前端。很多做 CPU 模拟的人一上来就堆寄存器重命名和 ROB,反而忽略了“前端取指能不能跟上后端执行”这个最基础的问题。

4.2 为“没有编译器”准备的极简链接脚本与内存映射

因为没有现成的 RISC-V 工具链,我需要自己负责产物生成的最后一公里。这里最实用的工具是一段自写的链接脚本,功能上等同常见开发板工程里的link.ld:把代码段固定放在地址 0x1000,数据段放在 0x20000,栈顶放在 0x40000。

有人会问,模拟器里反正没有 MMU,地址随便定不就行了?还真不是。没有固定内存映射,周期模拟器里就无法模拟访存延迟差异,也无法模拟指令缓存和数据缓存的行为。我做了一个极简内存模型:代码区始终命中指令缓存,数据区访问如果落在连续地址内则命中数据旁路,跨页访问多付 1 个周期。这一步让所有访存优化的结果都可复现、可解释。

这也侧面回答了为什么“没有编译器”还做得下去:只要你能控制指令编码和内存布局,一个最小汇编器加一个链接脚本,完全可以把测试程序喂给周期级模型。开发板能帮你做的事,模型用更透明的方式也能做。

5. 从顺序到乱序的代价:ROB、保留站和访存消除冲突

5.1 三条改造主线

跑完前两轮优化之后,顺序内核的得分已经上来了,但天花板也很明显:单发射宽度决定了每个周期最多只能发射一条指令,数据冒险再少也只能空等。下一步只能做乱序执行,我给自己定了一个星期内能完成的改造范围:三条主线。

第一条是寄存器重命名。用物理寄存器池消除 WAW/WAR 冒险,让指令在译码后就可以把名字改成物理寄存器的编号。第二条是保留站和 ROB。保留站为每类功能单元维护一个等待队列,只要源操作数准备好了就发射,不一定按程序顺序;ROB 负责最终按程序顺序提交,确保异常和分支误预测时的精确状态。第三条是访存队列。乱序执行后最头疼的不是算术指令,而是 load/store 的次序关系,我加了一个存储队列,让 load 可以越过尚未提交的 store,只要地址确定不冲突。

这里我贴一段模型里保留站分配的伪代码,核心逻辑大概是:

for (auto& inst : dispatchList) { if (rs.hasFreeEntry(inst.opType) && rob.hasFreeEntry()) { rob.allocate(inst); rs.allocate(inst); dispatchList.pop_front(); } else { stall_dispatch(); break; } }

看起来简单,真实坑全在stall_dispatch()的后台:保留站满了得停译码,ROB 满了必须停发射,源操作数还没算出来时得让保留站里的指令干等。每停一个周期,计数器都要有对应记录,否则调了半天都不知道时间去哪了。

5.2 乱序改完后的回归问题

乱序改造最折磨人的不是功能跑不通,而是“功能对了但周期账对不上”。我记得第一版乱序模型跑 CoreMark 子集时,指令结果完全正确,但 IPC 比顺序核还低。原因出在 ROB 只开了 16 项,保留站也只有 8 项,一遇到长延迟的 load 和乘法,资源迅速耗尽,后面的指令根本进不来,乱序执行的窗口小到等于没开。

把 ROB 扩到 64 项、保留站扩到 24 项之后,成绩才上来。这里我得到了一个重要教训:乱序执行不是“改成乱序”就自动变快,窗口太小等于没有乱序,寄存器重命名只是浪费面积。调度窗口和访问执行单元的带宽必须匹配,单发射 + 64 项 ROB 是浪费,3 发射 + 8 项 ROB 又不够用。

另一个回归问题出在分支误预测恢复。乱序执行中的分支预测失败不能像顺序核那样把流水线从头冲刷,而要回溯 ROB 中被误预测及之后的每一条指令,还需恢复寄存器映射关系。我第一版恢复逻辑写得太粗暴,误预测惩罚从顺序核的 2 周期变成了 12 周期,分支预测率再高也扛不住这种惩罚。后来把恢复逻辑改成“检查点 + 部分冲刷”,误预测惩罚才稳定在 4-6 个周期。

6. 一周第七天:我拿什么证明它“逼近玄铁910”

6.1 对数量表和差距读法

一周结束时,我在固定负载下跑出了 4.87 的 CoreMark/MHz。对比公开资料里对玄铁910 大体“5 分上下”的参考印象,差距已经缩小到 5% 以内。下面这组数据是这一周每一轮变更的累计结果:

阶段配置要点CoreMark/MHz相对初始倍率
初始基线顺序单发射,无预测,无缓存1.131.00x
+分支前端BTB+BHT+RAS,16项指令缓存1.721.52x
+发射宽度2发射顺序,支持转发网络2.642.34x
+乱序执行3发射,ROB 64项,保留站24项4.513.99x
+存储队列优化load/store 消歧加上cache命中优化4.874.31x

请注意,这个对比不是严格的官方评测,我的测试子集比完整 CoreMark 更偏向局部热点。但性能提升趋势仍然有参考价值:顺序核再怎么优化,到 2.6 左右已经是极限;再往上走必须靠乱序执行带来的多个流水线程并行。这正好解释了为什么任何一家处理器做高性能核,最终都要投入乱序执行的怀抱。

6.2 周期级模型不是实战替代品,但它是性能实验的显微镜

这一周下来我带走的不仅仅是那个 4.87。相比开发板,周期级模型给出的是一整套“性能归因能力”:每个周期为什么空转、每条分支误预测浪费多少、哪个资源在排队,全都有据可查。这比在板子上拿性能计数器猜要直接得多。

当然我也不会说没有编译器、没有开发板反而比有板子更好。玄铁910 是经过完整验证的工业级 IP,它有存储一致性、页表转换、中断响应等一系列我完全没碰的复杂度。我的模型更像一台“性能显微镜”,能看到微架构层面的流水线行为,但无法验证物理实现级的问题。能在没有硬件的约束下逼近参照核,是因为测试负载足够集中、模拟边界足够窄,这是一次有价值的学习实验,不是产品级对标。

6.3 一点个人体会

如果让我给下一周的自己一句话作为忠告,我会说:先把计数器和事件分类做对了,再去调架构参数。这一周最大的返工不是乱序执行逻辑,而是早期的总周期计数逻辑没算准。Branch 误预测、load 延迟、结构冲突三件事的周期可能重叠,按照“每个阶段各加一拍”的幼稚算法,统计出来的数据会互相重叠,最后归因怎么都解释不通。

另外收藏一个小技巧:给模拟器加一个“周期轨迹导出”接口,每个周期把当前发射/提交/等待状态输出成文本流。没有仿真波形时,这就是你的逻辑分析仪。我在调试 ROB 提交顺序时,全靠这个文本轨迹把乱序执行的每一条指令历史对齐到程序原始序列。

最后想说的是,玄铁910 只是个参照,不是终点。一个自写的周期级模型,最大的价值是你能在里面看见“为什么慢”,而这个视角,是开发板和编译器给不了你的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询