关于x86:为什么英特尔这些年来改变了静态分支预测机制?

Why did Intel change the static branch prediction mechanism over these years?

从这里我了解到英特尔近年来实施了几种静态分支预测机制:

  • 80486岁:永远不被录取

  • Pentium4年龄:后取/前不取

  • 更新的CPU,如常春藤桥,哈斯韦尔已经变得越来越无形,见马特G的实验这里。

英特尔似乎不想再谈论它了,因为我在英特尔文档中找到的最新资料是十年前写的。

我知道静态分支预测是(far?)与动态相比不那么重要,但在很多情况下,CPU将完全丢失,程序员(使用编译器)通常是最好的指南。当然,这些情况通常不是性能瓶颈,因为一旦频繁执行分支,动态预测将捕获它。

由于Intel在其文档中不再清楚地声明动态预测机制,gcc的builtin_Expect()只能从热路径中删除不太可能出现的分支。

我不熟悉CPU的设计,我也不知道英特尔现在对静态预测器使用什么机制,但我仍然认为英特尔最好的机制应该是清楚地记录他的CPU"当动态预测器失效时,我计划去哪里,向前或向后",因为程序员通常是当时最好的指南。

更新:
我发现你提到的主题逐渐超出了我的知识范围。这里涉及到一些动态的预测机制和CPU内部的细节,我在两三天内无法了解。所以请允许我暂时退出您的讨论并重新充电。
这里欢迎任何答案,也许会帮助更多人。


静态预测在现代设计中不受欢迎的主要原因是,与动态预测相比,静态预测在管道中发生得太晚,甚至可能不存在。基本的问题是,在获取分支方向和目标位置之前必须先知道它们,但是静态预测只能在解码后进行(在获取之后进行)。好的。

更详细地说…好的。CPU流水线

简而言之,在执行期间需要从内存中获取指令,解码这些指令,然后执行它们1。在一个高性能的CPU上,这些阶段将被流水线处理,这意味着它们通常都是并行的——但在任何给定的时刻对于不同的指令都是如此。你可以在维基百科上读到一些这方面的内容,但请记住,现代CPU更复杂,通常具有更多的阶段。好的。

在现代的x86上,使用复杂的可变长度指令集进行解码,可能有许多管道"阶段"只涉及到获取和解码指令,可能有六个或更多。这些指令也是超标量的,能够同时执行多个指令。这意味着,当以最高效率执行时,在飞行中会有许多指令,处于获取、解码、执行等不同阶段。好的。重定向提取

执行分支对管道的整个初始部分(通常称为前端)都有影响:当您跳转到新地址时,需要从该新地址提取、从该新地址解码等。我们说执行分支需要重定向提取。这对分支预测可以用来高效执行的信息有一定的限制。好的。

考虑静态预测是如何工作的:它查看指令,如果它是一个分支,比较它的目标,看它是"向前"还是"向后"。所有这些都必须在译码发生后发生,因为那时实际指令是已知的。但是,如果检测到一个分支并进行了预测(例如,向后跳转),则预测器需要重新定向获取,这比许多管道阶段更早。在解码指令N之后,当fetch被重定向时,已经有许多后续指令在错误(未执行)路径上被提取和解码。那些必须扔掉。我们说在前端引入了一个气泡。好的。

所有这一切的结果是,即使静态预测是100%正确的,它是非常低效的采取分支案件,因为前端管道系统是失败的。如果fetch和decode结束之间有6个管道阶段,那么每个执行的分支都会在管道中产生一个循环6气泡,并假设预测本身和刷新坏路径指令采取"零循环"。好的。救援动态预测

然而,现代的x86 CPU能够每周期执行最多1个执行分支,即使对于完全预测的静态执行,也必须优于限制。为了实现这一点,预测器通常不能使用解码后可用的信息。它必须能够重定向每个周期的获取,并且在最后一次预测之后仅使用一个周期延迟的可用输入。本质上,这意味着预测器基本上是一个独立的过程,只使用它自己的输出作为下一个周期预测的输入。好的。

这是大多数CPU上的动态预测器。它预测下一个周期从何处获取数据,然后基于该预测,它预测下一个周期之后从何处获取数据,依此类推。它不使用任何有关解码指令的信息,只使用分支的过去行为。它最终会从执行单元获得关于分支实际方向的反馈,并基于此更新其预测,但这一切本质上是异步的,在相关指令通过预测器后的许多周期内都会发生。好的。加起来

所有这些都有助于削弱静态预测的有用性。好的。

首先,这个预测来得太晚了,所以即使工作得很好,它也意味着现代英特尔的分支机构出现了6-8个周期的泡沫(事实上,这些都是从英特尔所谓的"前端重新设点"中观察到的数据)。这大大改变了进行预测的成本/收益方程。当你在获取预测前有一个动态预测器时,你或多或少想要做一些预测,如果它的准确度甚至达到51%,它可能会有回报。好的。

然而,对于静态预测,如果你想做一个"采取"的预测,就需要有很高的准确性。例如,考虑8个周期的前端重新设计成本,而16个周期的"完全预测失误"成本。让我们假设在某些程序中,冷反向分支的使用频率是不使用的两倍。这应该是预测向后执行的静态分支预测的胜利,对吧(与总是"预测"2不执行的默认策略相比)?好的。

不要这么快!如果假设8个周期的再转向成本和16个周期的完全预测失误成本,它们最终会有10.67个周期的相同混合成本,因为即使在正确预测的情况下,8个周期的泡沫也会出现,但在失败的情况下,没有静态预测的情况也没有相应的成本。好的。

另外,没有静态预测的情况已经得到了静态预测的另一半是正确的(正向分支没有采取的情况),静态预测的效用没有人们想象的那么大。好的。

为什么现在要改变?也许是因为与其他部分相比,管道的前端部分延长了,或者是因为动态预测器的性能和内存的增加意味着能够进行静态预测的冷分支更少。静态预测器性能的提高也意味着对于冷分支,反向采取的预测变得不那么强,因为动态预测器更容易记住循环(这是反向采取规则的原因)。好的。节约动态预测资源

这种变化也可能是由于与动态预测的交互作用:动态预测的一种设计根本不使用任何分支预测资源来进行从未观察到的分支。由于这种分支很常见,因此可以节省大量的历史表和BTB空间。然而,这种方案与一个预测反向分支的静态预测器是不一致的:如果一个反向分支从来没有被采用过,你不希望静态预测器拾取这个分支,并预测它被采用了,从而使你为未被采用的分支保存资源的策略变得混乱。好的。

1…然后做更多的事情,比如退休,但是执行之后发生的事情对于我们的目的来说并不重要。好的。

我把"预测"放在这里是因为在某种程度上它甚至不是预测:不采取是在没有任何相反的预测的情况下获取和解码的默认行为,所以如果你根本不输入任何静态预测,它就是你得到的,而你的动态预测不会告诉你其他的。好的。好啊。


Intel优化手册第3.4.1.3节中讨论的静态分支预测如下:

    百万千克1预测要采取的无条件分支。百万千克1百万千克1预测不采用的条件转发分支。百万千克1百万千克1预测要采用的条件向后分支。百万千克1百万千克1预测不采取的间接分支。百万千克1

编译器可以相应地组织代码。同一部分说明了以下内容:

The Intel Core microarchitecture does not use the static prediction
heuristic. However, to maintain consistency across Intel 64 and IA-32
processors, software should maintain the static prediction heuristic
as the default.

此声明表明,第3.4.1.3节已多年未更新。

如果动态预测器无法预测所获取的字节之间是否存在分支指令,或者缓冲区中是否有遗漏,则提取单元将继续按顺序提取,因为没有其他有意义的选择,从而有效地对未获取进行静态预测。

但是,如果在指令队列单元中,发现提取的字节流中存在条件或间接分支指令,那么此时进行静态预测可能比不进行静态预测更好。特别是,预测条件直接向后分支。这可以减少动态预测器和未取回程单元失效的惩罚,尤其是前端性能如此关键。据我所知,在优化手册中没有明确的声明,说明在iqu中有这样的静态预测器,并且适用于现代处理器。然而,正如我在另一个答案中所讨论的,一些性能计数器的描述似乎意味着在iqu上可能存在这样的静态预测器。

总的来说,我认为这是Intel不再记录的一个实现细节。

编译器辅助的动态分支预测技术确实存在,并且可以像您建议的那样非常有用,但是它们没有在当前的英特尔处理器中使用。


我的理解是,在当前的设计中,现代的台阶分支方向预测器总是使用最近分支的执行/未执行历史对条目进行索引。(这可能会将单个分支的状态分散到许多内部状态上,从而使预测非常复杂的模式成为可能,如10元素BubbleSort)。

CPU不尝试检测别名,只使用它找到的预测来决定条件分支是否接受。即分支方向预测始终是动态的,而不是静态的。

但在分支解码之前,仍然需要一个目标预测,以防止前端停止工作。分支目标缓冲区通常被标记,因为别名的其他分支的目标不太可能有用。

正如@paul a clayton指出的那样,一个btb失误可以让CPU决定使用静态预测,而不是动态采取/不采取预测中发现的任何东西。我们可能只是看到,让动态预测器漏掉足够多的时间来测量静态预测要困难得多。

(我可能在歪曲事实。现代的tage预测器也可以预测间接分支的复杂模式,所以我不确定他们是否会尝试用taken/not taken来预测,或者第一步是否总是试图预测下一个地址,不管这是否是下一条指令。x86 64位模式下的索引分支开销。)

在正确预测的情况下,未执行的分支仍然稍微便宜一些,因为前端可以更容易地从UOP缓存中获取相同周期中的早期和后期指令。(SandyBridge系列中的UOP缓存不是跟踪缓存;UOP缓存线只能缓存来自x86机器代码连续块的UOP。)在高吞吐量代码中,执行分支可能是次要的前端瓶颈。它们还通常将代码分布在更多的L1I和UOP缓存行上。

对于间接分支,"默认"分支目标地址仍然是下一条指令,因此如果不能简单地将某个真正的分支目标作为下一条指令,那么将ud2或其他内容放在jmp rax之后可以很好地防止错误推测(尤其是放在非代码中)。(尤其是最常见的一种。)

分支预测是一种"秘密酱汁",CPU供应商不公布细节。

Intel实际上自己发布指令吞吐量/延迟/执行端口信息(通过IACA和一些文档),但实验测试相当简单(如http s://agner.org/optimize/和http://instlatx64.atw.hu/已经做过),所以即使Intel愿意,也不可能保守这个秘密。

使用性能计数器很容易测量分支预测的成功率,但是知道为什么一个特定分支预测错误或没有在一个特定的执行中被发现是非常困难的;即使是对于一个分支的单个执行来说,测量也很困难,除非您使用rdtscrdpmc或其他工具检测代码。