流水线建模:让吞吐、冒险与气泡从结构里长出来
最后更新:2026-09-16
本篇是 CPU 微架构建模系列的 A3。A2 指令集架构建模 结尾把一个统计式 CPI 公式挂在 ISS 的 trace 上:
CPI = 1 + 分支率×误预测率×惩罚 + load率×miss率×惩罚。它能给出数量级,但惩罚是拍进去的常数。本篇回答下一个问题:这些 stall 气泡能不能不拍,而是从流水线的资源与依赖结构里逐拍算出来? 真机机制(流水线基础、超标量、数据冒险)请配合阅读,本篇只讲怎么给它们建参数化模型。
一、先分清 throughput 与 latency:模型记的是两件账
初学者最容易把两个量混为一谈,而建模时它们恰好是两张不同的表:
- 延迟 latency:一条指令从进入流水线到结果可用经过多少拍。
lw命中 L1 也要 4~5 拍才有数据,但这 4 拍未必产生气泡。 - 吞吐 throughput:单位时间能提交多少条指令(IPC)。只要后续指令不依赖那条
lw,它们可以钻进流水线把槽位填满,吞吐不受单条延迟影响。
经典例子是独立 load:一条 load 延迟 5,但每拍都能发一条新的,稳态吞吐仍是 1 条/拍;只有当紧跟的指令要用这条 load 的结果时,才出现 load-use 气泡。模型必须同时维护"某资源此刻是否占用"和"某寄存器的值第几拍可用"两张状态,才能区分这两种情形。
| 概念 | 真机含义 | 模型里的数据结构 |
|---|---|---|
| 流水线段 | IF/ID/EX/MEM/WB | 阶段占用计数或段级队列表 |
| 结构冒险 | 两条指令抢同一功能部件/端口 | 资源空闲时间表(如 load 端口 freeAt) |
| 数据冒险 RAW | 后指令用前指令尚未写出的结果 | 架构寄存器就绪时间表 ready[r]=cycle |
| 控制冒险 | 分支方向/目标未确定就取了错指令 | 分支解析拍 + 误预测冲刷惩罚 |
| 前递/旁路 | EX 结果直接回送,不等 WB | 就绪时间提前到"结果产生拍" |
| WAR/WAW | 乱序写同名寄存器 | 顺序核不存在;乱序核靠重命名解决(A5) |

二、记分牌模型:每类冒险都折算成"发射拍的后移"
顺序单发射核的最小可用模型,是一个记分牌(scoreboard)。它不模拟数据通路里流动的 0/1,只回答一个问题:第 i 条指令最早能在第几拍发射? 三条规则把三类冒险统一成"把发射拍往后推":
- 结构冒险:
issue = max(issue, 资源空闲拍)。模型里每个非流水化或端口数受限的部件记一个freeAt。 - RAW 数据冒险:扫描源寄存器,
issue = max(issue, ready[rs1], ready[rs2])。 - 控制冒险:taken 且预测错误时,
cycle += 惩罚(冲刷已取的错误路径指令)。
发射后更新目标寄存器的就绪时间,这里正是前递开关起作用的地方:
uint64_t issue = cyc; // 1-wide in-order issue
if (in.kind == LOAD) { // 结构冒险:单一 load 端口
if (loadFree > issue) { str += loadFree - issue; issue = loadFree; }
loadFree = issue + 1;
}
uint64_t dep = 0; // RAW:操作数就绪时间
for (int s : {in.s1, in.s2}) if (s >= 0) dep = max(dep, ready[s]);
if (dep > issue) { raw += dep - issue; issue = dep; }
cyc = issue + 1;
if (in.dst >= 0) {
uint64_t val = (in.kind == LOAD) ? cyc + loadLatency - 1 : cyc;
ready[in.dst] = forwarding ? val : val + 1; // 无前递须等 WB
}
if (in.kind == BRANCH && in.taken) { cyc += pen; ctrl += pen; }这就是 A2 那个"常数惩罚"的结构化来源:load-use 气泡从 ready[rs] 与发射拍的差值里长出来,分支气泡从深度参数里长出来。改一个参数(前递有无、load 延迟、流水线级数),气泡自动重算,不必重设 IPC。
三、实测:同一段求和循环,结构参数决定 IPC
配套实验 demos/cpu-model-core-mem-lab/(make && ./lab 1,纯 C++17,CentOS 7 + devtoolset-10 / GCC 10.2.1 实测)喂入与 A2 相同的数组求和循环(动态 164 条指令),只改微架构参数:。(公开仓库镜像:derekzhuo/geek-doc.cn)
================ [A3] Parametric in-order pipeline: stalls from structure ================
array-sum loop, dynamic instructions = 164
config cycles IPC RAW struct ctrl
fwd L1-use=1 pen=0 164 1.000 0 0 0
fwd load-use=3 pen=2 290 0.566 64 0 62
fwd load-use=3 pen=5 383 0.428 64 0 155
NO fwd load-use=3 pen=5 447 0.367 128 0 155
deep pen=15 load-use=5 757 0.217 128 0 465逐行读这张表,能把三类机制一次看清:
- 理想行 IPC=1.0:load 结果 1 拍可用、分支零惩罚,记分牌里没有任何后移,164 条指令恰好 164 拍。
- 前递的价值可被隔离测量:同样
load-use=3, pen=5,打开前递 383 拍(RAW 气泡 64),关掉前递 447 拍(RAW 气泡翻倍到 128)。差的 64 拍正是"必须等 WB 写回才能用"的额外周期,这是给"要不要加旁路网络"做面积/性能取舍时的直接输入。 - 控制冒险随流水线深度放大:
pen从 2→5→15,ctrl 列 62→155→465,IPC 0.566→0.428→0.217。分支误预测惩罚本质上≈"被冲刷的深度",深流水线在高频的同时把这一项放大——这也是为什么深核必须配好的预测器(衔接 A4)。 - latency 不等于 stall:load 部件本身延迟 3~5 拍,但只有
add acc, r9这种真依赖才吃气泡;32 次迭代里 RAW 稳定出现,正是因为求和的累加依赖链。
四、超标量的"每拍槽位"与吞吐上限
单发射模型每拍只处理一条指令,代码里就是 cycle = issue + 1。超标量把这一步改成"每拍最多发射 W 条、提交 C 条",但前面三条冒险规则一条不少,只是每拍要在一个候选窗口里同时解决多条指令:
- 取一个取指包(受 A4 前端 供给限制),按程序序尝试填充 W 个发射槽;
- 对包内每条指令并行查结构表(ALU/乘法/load 端口各剩几个)与就绪时间,能发的发、不能发的连同其后指令一起留下一拍(顺序发射),或在乱序核里跳过等待者(A5);
- 取指包内若遇 taken 分支,包内后续指令位置无效,该拍最多只用到分支之前的槽位——这是分支密度限制前端宽度的机理。
由此得到一个重要区分:峰值 IPC(=发射宽度 W)与实际 IPC。即使每条指令零延迟、零冒险,若前端每拍平均只供得上 2.5 条、或 load 端口只有 2 个,4 发射核的稳态 IPC 也到不了 4。模型里实际吞吐取"前端供给、结构端口、依赖就绪、提交宽度"几项每拍可取指令数的最小值。这也是为什么加宽到 4/6 发射后,收益越来越依赖预测器、cache 和乱序窗口是否跟得上——宽度只是上限,不是 IPC 本身。教学实验为突出单条依赖链的气泡而保持单发射,但把记分牌的"单个 issue 时刻"换成"W 个槽位 + 每类部件计数",即可平滑升级为超标量顺序模型。
六、参数从哪来、模型怎么校准
记分牌模型的价值取决于参数是否可信,常见来源与 性能模型校准 的口径一致:load-use 延迟、分支解析拍、功能部件延迟优先取自上一代硅后实测(PMU/微基准)或 RTL 关键路径仿真;新结构没有数据时,先用分析估计再在 RTL 就绪后回填。校准不是只对一个总 IPC,而要分项对拍:分别构造能单独触发某类冒险的微基准(纯依赖链触发 RAW、可预测/不可预测分支触发控制冒险、满载 load 触发结构冒险),让模型在每个微基准上的 stall 分项都与 RTL 接近,再跑混合负载验证总 IPC。只拟合总 IPC 容易出现"各项都错、恰好抵消"的过拟合,换一个负载就失真。
另一个工程要点是统计式与结构式模型的层级分工:早期一周要扫几十个设计点时,用 A2 的概率 CPI 快速筛掉明显差的配置;进入少数候选后,再用本篇的资源/依赖记分牌做结构级精算,定位气泡来源;最终用周期精确模型或 RTL 对关键候选签字。三者不是替代关系,而是不同决策阶段的精度-速度权衡(A1 抽象层级)。实验里"理想行 IPC=1、深流水行 IPC=0.217"这类结果,正是在告诉架构师:对这种分支密集的循环,单纯堆流水线级数换频率会被控制冒险吞掉收益,必须同步加强预测器。
七、建模边界与常见坑
- 不要把延迟直接当气泡:load 延迟 200 拍不等于每条 load 停 200 拍;独立 load 可被后续指令掩盖。气泡只在真依赖时产生,必须用就绪时间表算。
- 控制冒险的惩罚是"解析拍 + 冲刷深度",不是拍常数:分支在第几段能解析、取指缓冲多深,共同决定惩罚;模型里应是流水线结构的函数。
- 前递并非万能:
load-use这类"结果在 MEM 段才有、下一条 EX 就要用"的情况,前递也救不了,至少一个气泡——实验里 RAW 列即来自此。 - 别在顺序模型里建 WAR/WAW:按序发射/按序提交时名字依赖不产生冒险;硬加会高估停顿。它们是乱序 + 重命名的议题。
- 统计式与结构式分层使用:早期快速选型可用 A2 的概率 CPI;要定位"气泡具体长在哪、加旁路/加端口值不值",必须上本篇的资源/依赖模型。
八、与系列其他篇的衔接
- 向上承接 A1 抽象层级 与 A2 ISA/ISS:本篇是 trace 之下的周期近似核模型第一块。
- 控制冒险的惩罚项由谁压低,看 A4 分支预测与取指建模。
- 单发射记分牌如何升级为重命名/ROB/发射队列的乱序引擎,看 A5 乱序执行建模。
- load 延迟与 miss 率从哪来,看 B1 Cache 建模;真机机制配合 流水线基础 阅读。
总结
流水线建模的本质是维护"资源空闲时间表 + 寄存器就绪时间表"两张账,把结构冒险、RAW 数据冒险和控制冒险统一折算成发射拍的后移——前递、load-use 延迟、发射宽度、流水线深度都只是这两张表上的参数,气泡与 IPC 由此从结构里逐拍长出来,而不是用概率常数拍出来。