4. 性能评估基础与基准测试
执行单元是处理器的“心脏”。ALU、乘法器、除法器这些部件,决定了你的CPU能不能在每一个时钟周期都干出活来。我见过不少同学,RTL写得很漂亮,一上FPGA跑分就拉胯,问题十有八九出在执行单元的延迟没处理好。
这一章,咱们就聊聊怎么把E203的执行单元调顺溜。说白了,就是让流水线不因为等计算结果而空转。
4.1 执行单元的延迟模型
先看一张图,理解一下E203执行单元在流水线中的位置。
E203的执行阶段(EX)是单周期设计的,但乘法器和除法器不是。这就带来了一个矛盾:流水线希望每个阶段都只花1个周期,可乘法器要3个周期,除法器更久。怎么办?
核心思路:让乘法器和除法器“自成一派”,在EX阶段内部做多周期处理,同时通过数据前推(Forwarding)把结果提前送给后续指令,避免流水线停顿。
4.2 ALU的延迟隐藏
ALU在E203里是1周期完成的,按理说没什么延迟问题。但我在项目中遇到过一种情况:ALU指令后面紧跟着一条使用其结果的指令,如果写回阶段(WB)和读寄存器阶段(ID)没对齐,就会多等一个周期。
E203的做法很直接——结果写回和结果前推同时进行。ALU计算完,结果直接送到EX阶段的输入多路选择器,同时写入寄存器堆。这样,下一条指令在ID阶段就能拿到值,不用等WB完成。
实战技巧:检查你的ALU结果通路。如果发现ALU输出先去了WB寄存器,再从WB寄存器绕回EX,那就多了一个周期的气泡。E203的ALU结果是从EX直接前推到EX的,没有中间寄存器。
4.3 乘法器的3周期延迟处理
乘法器在E203里用了3个周期。为什么是3?因为E203的乘法器是逐位 Booth 编码 + Wallace 树,3周期刚好平衡面积和速度。
延迟隐藏的策略是这样的:
- 第1周期:乘法器开始计算,流水线继续向前走,不等待。
- 第2周期:乘法器还在算,此时如果有其他不依赖乘法结果的指令,可以正常执行。
- 第3周期:乘法器出结果,结果通过前推通路直接送到EX阶段的输入。
你想想看,如果乘法器结果要等写回寄存器堆才能用,那后续指令至少等3个周期。有了前推,只需要等1个周期(如果乘法结果被下一条指令使用)。
| 场景 | 无前推(停顿周期) | 有前推(停顿周期) |
|---|---|---|
| MUL rd, rs1, rs2 → ADD rd, rs1, rs2 | 3 | 1 |
| MUL rd, rs1, rs2 → 无关指令 | 0 | 0 |
| MUL rd, rs1, rs2 → LW rd, 0(rs1) | 3 | 2 |
注意:乘法器结果前推有一个陷阱——如果乘法结果被用于地址计算(比如基址寻址),前推路径可能来不及。E203的做法是:地址计算在ID阶段就开始了,所以乘法结果不能用于地址前推。我曾经在这个坑里折腾了两天,最后发现是地址生成逻辑没等乘法结果。
4.4 除法器的多周期挑战
除法器是最难搞的。E203用的是恢复余数除法,周期数等于被除数的位宽(32位除法需要32个周期)。
延迟隐藏的思路变了——不再试图隐藏,而是让流水线“绕行”。
具体做法:
- 当检测到除法指令时,EX阶段进入“忙等待”状态。
- 除法器开始逐位计算,每个周期产生1位商。
- 流水线中后续的指令如果依赖除法结果,必须等待。
- 如果不依赖,可以继续执行(E203支持乱序完成吗?不支持,但可以插入气泡让后续无关指令通过)。
嗯,这里要注意:E203是顺序发射、顺序完成的。除法器占用EX阶段时,后续指令不能进入EX。但ID阶段可以继续译码,只要不发射到EX就行。
优化点:我曾经在项目里把除法器改成非恢复余数算法,周期数从32降到16。代价是面积增加了约15%。对于实时性要求高的场景,这个trade-off是值得的。
4.5 数据前推的实现细节
数据前推是延迟隐藏的“杀手锏”。E203的前推逻辑集中在EX阶段:
// 伪代码:E203 数据前推选择逻辑
// 来自 ALU 结果
if (ex_alu_valid && ex_alu_rd == id_rs1_addr) then
forward_rs1 = ex_alu_result
// 来自乘法器结果
else if (ex_mul_valid && ex_mul_rd == id_rs1_addr) then
forward_rs1 = ex_mul_result
// 来自除法器结果
else if (ex_div_valid && ex_div_rd == id_rs1_addr) then
forward_rs1 = ex_div_result
// 来自写回阶段
else if (wb_valid && wb_rd == id_rs1_addr) then
forward_rs1 = wb_result
else
forward_rs1 = regfile[rs1_addr]
这段逻辑看起来简单,但优先级很重要。我习惯把EX阶段的结果放在最高优先级,因为它是“最新鲜”的。写回阶段的结果虽然也有效,但可能已经被EX阶段的结果覆盖了。
调试技巧:如果你发现数据前推没生效,先检查比较器。E203的寄存器地址比较是5位宽(x0-x31),但x0是硬连线0,比较时要排除。我曾经漏掉了x0的排除逻辑,导致所有读x0的指令都前推了错误的值。
4.6 性能调优实战
咱们看一个具体的调优案例。假设有一段代码:
mul t0, a0, a1 # t0 = a0 * a1, 3周期
add t1, t0, a2 # t1 = t0 + a2, 依赖t0
div t2, t1, a3 # t2 = t1 / a3, 依赖t1, 32周期
sw t2, 0(sp) # 写回内存, 依赖t2
没有前推的情况下,这段代码需要:3(mul)+ 1(add等待)+ 32(div)+ 1(sw等待)= 37个周期。
有了前推:3(mul,add在第4周期拿到结果)+ 32(div,sw在第36周期拿到结果)= 35个周期。
只省了2个周期?对,因为除法器是瓶颈。真正的大头在除法器。如果能把除法器从32周期优化到16周期,总周期数就能降到19。
调优原则:先优化最慢的部件。在E203里,除法器是执行单元的“长尾”。ALU和乘法器的延迟隐藏做得再好,也抵不过除法器拖后腿。我建议你在做性能分析时,先看除法指令的占比,如果超过5%,就该考虑优化除法器了。
4.7 避坑指南
最后,分享几个我踩过的坑:
- 前推路径的时序:数据前推增加了组合逻辑的深度。E203的EX阶段本来时序就紧,加上前推多路选择器,容易导致setup违例。我习惯在关键路径上插入一级流水寄存器,代价是多一个周期的延迟,但时序能收敛。
- 乘法器与除法器的互斥:E203的乘法器和除法器共享部分硬件资源。如果同时发射乘法和除法指令,会出现资源冲突。E203的做法是:检测到冲突时,后一条指令 stall 一个周期。
- 写后读(RAW)冒险:前推能解决大部分RAW冒险,但有一种情况例外——加载指令(LW)后面紧跟使用加载结果的指令。加载结果来自MEM阶段,前推到EX阶段需要跨两个阶段,E203在这里插入了一个气泡。
嗯,执行单元的优化,说白了就是跟延迟做斗争。ALU快,就让它直接前推;乘法器慢,就让它边算边推;除法器更慢,就让它独占流水线。理解了这些,你就能把E203的执行单元调得服服帖帖。