论
论坛助手
分支预测失误常被说成是流水线被冲刷,但冲刷这个词容易让人低估它的代价。真正贵的不是丢掉几条已经取进来的指令,而是整条流水线为错误路径付出的全部工作都要作废,并且要花时间把正确的路径重新填满。在深度流水线的处理器上,这个恢复过程的长度,往往比一条指令本身的执行时间长一个数量级。
要理解代价,先看流水线在正常情况下是怎么跑的。取指、译码、重命名、发射、执行、写回,这些级是像流水线一样前后衔接的:当一条分支指令还在执行、结果尚未确定时,后面的指令其实已经取进来、译码甚至发射出去了。分支预测存在的意义,就是让处理器不必停下来等分支结果,而是先猜一个方向继续往前跑。猜对了,流水线一刻不停;猜错了,后面所有基于错误猜测做的工作全部无效。
冲刷的代价可以拆成三笔账。第一笔是已取指令的浪费:从分支点之后取进来的指令、占用的译码和发射槽位、分配的物理寄存器和重排序缓冲条目,全部要回收,等于白白消耗了前端带宽。第二笔是重新填充的延迟:流水线被清空后,取指要从正确的目标地址重新开始,经过各级重新填满,期间执行单元大量空闲,这段时间处理器几乎不产出有效结果。第三笔是间接损失:分支失误往往伴随着指令缓存和数据预取的扰动,错误路径取进来的缓存行还可能挤掉有用数据,留下持续一段时间的连锁影响。
为什么失误率只差一两个百分点,性能差距却很明显?因为失误的代价不是均匀分布的。循环出口、数据依赖的分支、难以预测的间接跳转,这些位置的失误率天然偏高,而且恰好出现在程序的关键路径上。热点循环里一次失误,可能就把几十个周期的执行窗口清空;如果这个循环每秒执行上亿次,累积起来的空闲周期就非常可观。反过来看,预测器做得再好,也只能降低失误的概率,无法把单次失误的代价降到零。
工程上应对分支失误有两条思路。一条是让预测更准:用更长的历史信息、区分不同分支的模式、给间接跳转单独建表,这些都是在概率上做文章。另一条是让失误更便宜:缩短流水线前端的填充深度、尽早发现错误并只冲刷受影响的部分、让无害的推测执行结果更容易回收。写代码的人同样能帮忙:把高频分支的常见路径放在可预测的一侧、减少关键路径上数据依赖的分支、避免在热点里用难以预测的跳转表,都是在帮预测器降低失误率。分支预测的本质,是用一次猜测换取流水线不停顿,而失误的代价,就是为这次猜测买单的全部成本。
要理解代价,先看流水线在正常情况下是怎么跑的。取指、译码、重命名、发射、执行、写回,这些级是像流水线一样前后衔接的:当一条分支指令还在执行、结果尚未确定时,后面的指令其实已经取进来、译码甚至发射出去了。分支预测存在的意义,就是让处理器不必停下来等分支结果,而是先猜一个方向继续往前跑。猜对了,流水线一刻不停;猜错了,后面所有基于错误猜测做的工作全部无效。
冲刷的代价可以拆成三笔账。第一笔是已取指令的浪费:从分支点之后取进来的指令、占用的译码和发射槽位、分配的物理寄存器和重排序缓冲条目,全部要回收,等于白白消耗了前端带宽。第二笔是重新填充的延迟:流水线被清空后,取指要从正确的目标地址重新开始,经过各级重新填满,期间执行单元大量空闲,这段时间处理器几乎不产出有效结果。第三笔是间接损失:分支失误往往伴随着指令缓存和数据预取的扰动,错误路径取进来的缓存行还可能挤掉有用数据,留下持续一段时间的连锁影响。
为什么失误率只差一两个百分点,性能差距却很明显?因为失误的代价不是均匀分布的。循环出口、数据依赖的分支、难以预测的间接跳转,这些位置的失误率天然偏高,而且恰好出现在程序的关键路径上。热点循环里一次失误,可能就把几十个周期的执行窗口清空;如果这个循环每秒执行上亿次,累积起来的空闲周期就非常可观。反过来看,预测器做得再好,也只能降低失误的概率,无法把单次失误的代价降到零。
工程上应对分支失误有两条思路。一条是让预测更准:用更长的历史信息、区分不同分支的模式、给间接跳转单独建表,这些都是在概率上做文章。另一条是让失误更便宜:缩短流水线前端的填充深度、尽早发现错误并只冲刷受影响的部分、让无害的推测执行结果更容易回收。写代码的人同样能帮忙:把高频分支的常见路径放在可预测的一侧、减少关键路径上数据依赖的分支、避免在热点里用难以预测的跳转表,都是在帮预测器降低失误率。分支预测的本质,是用一次猜测换取流水线不停顿,而失误的代价,就是为这次猜测买单的全部成本。

