本节摘要:控制冒险的解法是赌——取指站在分支结果揭晓之前就押注下一条指令的位置,赌对了零代价,赌错了冲刷流水线重来。本节先算清赌桌赔率(预测精度、流水线深度、分支频度如何共同决定惩罚),再讲静态预测、两位饱和计数器、分支目标缓冲这套由简到繁的赌具,最后解释 RISC-V 为什么拒绝分支延迟槽,以及投机执行与精确异常的交接。
取指站每个周期都要回答"下一条取哪",而条件分支的方向要到执行站才见分晓。不做任何预测,就只能停等——每个分支白白冻结数拍,而通用程序里分支指令的密度相当高,代价无法承受。所以现代处理器无一例外选择赌。
赌桌的赔率由三个变量决定:预测精度(押对的比例)、冲刷深度(押错时作废的流水线级数)、分支频度(每条指令摊到的分支数)。全局惩罚可以粗算为三者乘积的形式——精度越低、流水线越深、分支越密,误预测的账单越重。这个乘积解释了两件事:为什么深流水核(如追求高主频的设计)必须配更准的预测器,以及为什么预测器精度每提升一点,在高主频设计上的回报被放大。流水线深度与预测器精度是一对绑定的谈判条款,单独谈任何一边都会看错价格。
编译器或硬件固定一条规则:向后跳的分支预测为"跳转"(循环大概率继续),向前跳的预测为"不跳"。零硬件成本,对循环密集代码效果尚可,但条件复杂时精度很快见顶。静态预测适合两类场景:面积极度敏感的小核,以及作为动态预测器的冷启动缺省。
给每个分支地址配一个两位状态机,按历史结果调整预测。它的妙处在"饱和":连续两次押错才会改变预测方向,偶发的方向抖动不会让状态机来回翻面。
状态机按分支地址索引,构成一张预测表。表越大,不同分支互相覆盖的概率越低,精度越高——又一张"容量对精度"的存储账单。两位计数器简单到可以在教学核里手写,却已能吃掉大部分规律性分支,性价比极高。
两位计数器只赌方向,不赌地址。跳转目标地址的计算本身也要走执行站,于是出现了分支目标缓冲:一张缓存记住"这个地址的分支上次跳去了哪",取指站直接拿目标取指,连目标计算都省了。再往上是利用分支历史全局相关性的一族技术——用过去若干次分支的结果模式索引预测表,捕捉"奇偶交替""三拍一循环"这类规律。高档预测器的面积与验证成本随之陡升,第八章对比的开源核里,预测器规模差异正是代际差距的主项之一。
| 赌具 | 赌什么 | 硬件成本 | 适用 |
|---|---|---|---|
| 静态规则 | 方向 | 零 | 微控制器、冷启动 |
| 两位计数器表 | 方向 | 一张小存储 | 通用小核的主力 |
| 目标缓冲 | 方向加地址 | 一张带标签的存储 | 有跳转密集负载的核 |
| 历史相关预测 | 方向加模式 | 多表加逻辑 | 高性能核的前端标配 |
预测失败的处理流程是一个标准的"发现、作废、重取"三部曲:执行站判定真实方向,与预测不符时发出冲刷信号;取指与执行之间所有站内的指令被标记作废(它们的写回被禁止——还记得 2.1 的无副作用条款吗,就是为此刻准备的);程序计数器被纠正为正确地址,重新取指填充。作废指令不能留下任何架构状态痕迹,这一性质是投机执行的合法性根基。
投机执行把"赌"推到更远:不止取指,被预测路径上的指令可以直接开始执行——预取数据、预算结果——只要提交被押后到分支确认为止。赌对,白赚数拍;赌错,作废重来的范围稍大一点而已。投机与第五章精确异常的交界处有个著名的安全议题:投机路径上的指令不得产生真实的副作用(比如把数据真正写入内存),开源核实现里通常用"投机装载进队列、提交时才放行写回"的缓冲结构来守门。
历史上有一种把赌责转嫁给编译器的方案:分支延迟槽——规范规定分支后的一条指令"无论跳不跳都执行",由编译器填入有用指令或空操作。它省了硬件,却把流水线级数写进了规范:延迟槽条数与实现强耦合,更深或更浅的流水线都要为兼容付出扭曲设计的代价。RISC-V 明确拒绝了延迟槽——又一次"合约不绑微架构"的决策,与 3.2 的硬件互锁同出一门。代价是编译器少了一条显式调度的抓手,收益是指令集对所有流水线深度中立。评估这类取舍时的问题永远是:灵活性的收益归谁,兼容的枷锁戴多久。
空讲乘积不如算一遍。设某程序分支指令占比约五分之一;一颗五级流水核在分支解析前有两拍不确定窗口,误预测冲刷深度按两拍计;预测精度九成。每条分支的期望惩罚约为误预测率乘以冲刷深度,折到每条指令头上大约是百分之四的额外周期——可容忍。换到十级深流水、解析窗口六拍、同精度九成的高主频核:每条分支期望惩罚翻了三倍,折到每条指令约百分之十二——开始刺眼。若预测器升档把精度提到九成七,惩罚立刻缩到三成——这解释了为什么深流水设计必须同步升级赌具。三个变量里,精度是设计者最能使力的杠杆,因为它可以用存储换:预测表更大、历史更长、组件更多。而冲刷深度由流水线结构决定,改它就是改产品定位。把这套乘积背下来,看任何核的预测器配置时都能现场估出它的必要性与性价比。
投机执行的边界与安全。赌赢的收益再大,也有两条铁律不能破:副作用押后——投机路径上的装载与运算可以提前做,但真正的写回与写内存必须等分支确认(3.4 的重排序缓冲就是干这个的);异常记账不清算——投机指令触发的异常(比如缺页)先记在案,只有它所在的投机路径被确认后才真正陷入。第二条引出一个安全细节:如果投机路径上的访问把别的地址拉进了缓存,攻击者可以通过测量缓存侧信道"读出"这些越权地址的痕迹——现代高性能核为此做了专门的加固设计。投机是把刀,刀刃上的两条规矩就是刀鞘。
问:循环为什么好预测? 因为方向高度规律——绝大多数迭代向后跳。静态的"向后跳预测为跳"已经能吃掉大部分循环收益,这正是静态预测在小核上够用的原因;动态预测器的优势在方向不规则的分支上。
问:预测失败和异常的冲刷有什么区别? 机制相似(作废与重取),发起者不同:预测失败由执行站的分支判定发起,异常由提交前的裁决发起(5.2 详述);恢复来源也不同——一个从预测器纠正后的地址重取,一个从档案袋取回陷入处理入口。
💡 关键直觉:分支预测器的本质是"用一块小存储记住程序的行为规律"。预测精度提升的边际收益递减,而冲刷深度会随流水线加深而放大惩罚——先定流水线深度,再定预测器档位,顺序反了就会为过度昂贵的赌具付账。
到这里,顺序流水线的三大故障都有了疗法,但也撞上了它的天花板——下一节换引擎,进入乱序执行的世界。