是否可以告诉分支预测器跟随分支的可能性有多大?
Posted
技术标签:
【中文标题】是否可以告诉分支预测器跟随分支的可能性有多大?【英文标题】:Is it possible to tell the branch predictor how likely it is to follow the branch? 【发布时间】:2010-12-23 12:07:13 【问题描述】:为了清楚起见,我不打算在这里进行任何形式的可移植性,因此任何将我绑定到某个盒子的解决方案都可以。
基本上,我有一个 if 语句,它将 99% 的时间评估为 true,并且我正在努力争取每一个性能的最后一个时钟,我可以发出某种编译器命令(使用 GCC 4.1.2 和 x86 ISA,如果重要的话)告诉分支预测器它应该为那个分支缓存?
【问题讨论】:
使用配置文件引导优化编译(-fprofile-generate,在一些测试数据上运行,-fprofile-use)。然后 gcc 将知道每个分支的统计信息,并能够为快速路径优化代码布局。但是对于有帮助的地方,builtin_expect 仍然是一个好主意,以防在没有 PGO 的情况下编译代码。 Linux 内核为此提供了一些很好的宏(例如,可能()和不太可能()),因为很难为内核生成配置文件数据。 MS 也提供 PGO -- blogs.msdn.com/vcblog/archive/2008/11/12/pogo.aspx. 【参考方案1】:可以,但会产生no效果。例外是 Netburst 之前的旧(过时)架构,即便如此,它也没有做任何可衡量的事情。
英特尔在 Netburst 架构中引入了一个“分支提示”操作码,并在一些旧架构上为冷跳转(向后预测采用,向前预测未采用)提供了默认静态分支预测。 GCC 使用 __builtin_expect (x, prediction)
实现这一点,其中预测通常为 0 或 1。
编译器发出的操作码在所有较新的处理器架构 (>= Core 2) 上都忽略。这实际上做某事的小角落案例是旧的 Netburst 架构上的冷跳案例。英特尔现在建议不要使用静态分支提示,可能是因为他们认为代码大小的增加比可能的边际加速更有害。
除了预测器的无用分支提示之外,__builtin_expect
也有其用处,编译器可能会重新排序代码以提高缓存使用率或节省内存。
它不能按预期工作的原因有很多。
处理器可以完美地预测小循环 (n 处理器可以完美地预测小的重复模式 (n~7)。 处理器本身可以比编译器/程序员在编译时更好地估计运行时分支的概率。 分支的可预测性(= 分支被正确预测的概率)远比分支被采用的概率重要得多。不幸的是,这高度依赖于架构,并且众所周知,预测分支的可预测性非常困难。在 Agner Fogs manuals 上阅读有关分支预测内部工作的更多信息。 另请参阅 gcc mailing list。
【讨论】:
如果你能引用/指向它说提示在较新的架构上被忽略的确切部分会很好。 我给的链接中的第3.12章“静态预测”。 当你说可以完美预测较小的循环时,这是否意味着循环必须完成一次(可能错误预测边缘),然后让所有迭代来完美预测下一次循环执行?【参考方案2】:是的。 http://kerneltrap.org/node/4705
__builtin_expect
是一种方法 gcc(版本> = 2.96)提供 程序员指示分支 预测信息到 编译器。的返回值__builtin_expect
是第一个参数(只能是整数) 传递给它。
if (__builtin_expect (x, 0))
foo ();
[This] would indicate that we do not expect to call `foo', since we
expect `x' to be zero.
【讨论】:
在 Microsoft 环境中,if 语句被预测为始终为真。有些版本确实有配置文件引导优化。 另见:***.com/questions/109710/…【参考方案3】:Pentium 4(又名 Netburst 微架构)有分支预测器提示作为 jcc 指令的前缀,但只有 P4 对它们做过任何事情。 见http://ref.x86asm.net/geek32.html。和 Section 3.5 of Agner Fog's excellent asm opt guide,来自http://www.agner.org/optimize/。他也有 C++ 优化指南。
早期和以后的 x86 CPU 会默默地忽略这些前缀字节。 Are there any performance test results for usage of likely/unlikely hints? 提到 PowerPC 有一些跳转指令,其中有一个分支预测提示作为编码的一部分。这是一个非常罕见的建筑特征。在编译时静态预测分支很难准确地完成,因此通常最好让硬件来解决。
关于最新的 Intel 和 AMD CPU 中的分支预测器和分支目标缓冲区的具体行为方式,官方并未公布太多信息。优化手册(在 AMD 和 Intel 的网站上很容易找到)提供了一些建议,但没有记录具体行为。有些人已经运行测试来尝试预测实现,例如Core2 有多少 BTB 条目...无论如何,明确暗示预测器的想法已被放弃(现在)。
记录的内容是,例如,Core2 有一个分支历史缓冲区,如果循环始终运行恒定的短迭代次数(优秀。
另请参阅Why did Intel change the static branch prediction mechanism over these years?:英特尔,因为 Sandybridge 根本不使用静态预测,据我们从试图对 CPU 的功能进行逆向工程的性能实验中得知。 (当动态预测未命中时,许多较旧的 CPU 将静态预测作为后备。正常的静态预测是不采用前向分支而采用后向分支(因为后向分支通常是循环分支)。)
likely()
/unlikely()
宏使用 GNU C 的 __builtin_expect
的效果(就像 Drakosha 的回答提到的那样)不直接将 BP 提示插入 asm。 (gcc -march=pentium4
可能会这样做,但在编译其他任何东西时不会这样做)。
实际效果是对代码进行布局,使快速路径的分支更少,总指令可能更少。这将有助于在静态预测发挥作用的情况下进行分支预测(例如,动态预测器很冷,在确实回退到静态预测而不是让分支在预测器缓存中相互别名的 CPU 上。)
有关代码生成的具体示例,请参阅What is the advantage of GCC's __builtin_expect in if else statements?。
即使在完美预测的情况下,采用分支的成本也略高于未采用分支。当 CPU 以 16 字节的块获取代码以并行解码时,采用的分支意味着该获取块中的后续指令不是要执行的指令流的一部分。它会在前端产生气泡,这可能会成为高吞吐量代码的瓶颈(不会因缓存未命中而在后端停滞,并且具有高指令级并行性)。
在不同的块之间跳转也可能会触及更多的代码缓存行,增加 L1i 缓存占用空间,如果它是冷的,可能会导致更多的指令缓存未命中。 (以及潜在的 uop-cache 占用空间)。所以这是让快速路径短而线性的另一个优势。
GCC 的配置文件引导优化通常使可能/不太可能的宏变得不必要。编译器收集每个分支用于代码布局决策的运行时数据,并识别热块/冷块/函数。 (例如,它会在热函数中展开循环,但不会在冷函数中展开循环。)参见 -fprofile-generate
和 -fprofile-use
in the GCC manual。 How to use profile guided optimizations in g++?
否则,如果您没有使用可能/不太可能的宏并且没有使用 PGO,GCC 必须使用各种启发式方法进行猜测。 -fguess-branch-probability
在-O1
及更高版本中默认启用。
https://www.phoronix.com/scan.php?page=article&item=gcc-82-pgo&num=1 在至强可扩展服务器 CPU 上使用 gcc8.2 获得 PGO 与常规的基准测试结果。 (Skylake-AVX512)。每个基准测试都至少有小幅加速,有些则受益约 10%。 (其中大部分可能来自热循环中的循环展开,但其中一些可能来自更好的分支布局和其他效果。)
【讨论】:
顺便说一句,如果您使用配置文件引导优化,您可能不需要使用 builtin_expect。 PGO 记录每个分支的路径,因此当您使用 -fprofile-use 进行编译时,gcc 知道每个分支的常见情况。使用 builtin_expect 告诉它快速路径仍然没有什么坏处,以防你的代码将在没有 PGO 的情况下构建。【参考方案4】:我建议不要担心分支预测,而是分析代码并优化代码以减少分支数量。一个例子是循环展开,另一个例子是使用布尔编程技术而不是使用if
语句。
大多数处理器喜欢预取语句。通常,分支语句会在处理器内生成一个fault,导致它刷新预取队列。这是最大的惩罚。为了减少这种惩罚时间,重写(和设计)代码以减少可用的分支。此外,一些处理器可以有条件地执行指令而无需分支。
通过使用循环展开和大型 I/O 缓冲区,我将程序的执行时间从 1 小时优化到了 2 分钟。在这种情况下,分支预测不会节省太多时间。
【讨论】:
“布尔编程技术”是什么意思? @someonewithrpc 通过使用按位运算将多个案例组合成一个案例。一个(愚蠢但仍然)示例:替换 a = b&1 ? 0:1;由 a = b&1;【参考方案5】:SUN C Studio 为这种情况定义了一些编译指示。
#pragma很少调用()
如果条件表达式的一部分是函数调用或以函数调用开头,则此方法有效。
但是没有办法标记一个通用的 if/while 语句
【讨论】:
【参考方案6】:不,因为没有汇编命令让分支预测器知道。别担心,分支预测器很聪明。
另外,关于过早优化及其邪恶的强制性评论。
编辑:Drakosha 提到了 GCC 的一些宏。但是,我认为这是代码优化,实际上与分支预测无关。
【讨论】:
谢谢克努斯先生。如果这不是一场比赛,看谁的解决方案运行得绝对最快,我完全同意。 如果你需要每个循环,为什么不直接使用内联汇编? 完整引用:“我们应该忘记小的效率,比如说大约 97% 的时间:过早的优化是万恶之源。但我们不应该放弃那关键的 3% 的机会. 一个好的程序员不会被这样的推理所迷惑,他会明智地仔细查看关键代码;但只有在识别出该代码之后。” (强调我的) 分支预测器在对分支一无所知时有一个静态规则:取向后分支,不取向前分支。如果您考虑一下 for 循环是如何工作的,您就会明白为什么这是有道理的,因为您跳回循环顶部的次数比不跳回的次数要多得多。所以GCC宏控制的是GCC如何在内存中布局操作码,这样前向/后向分支预测规则是最有效的。 这是完全错误的,实际上有一个汇编命令可以让分支预测器知道。但是,除了 Netburst,它在所有架构上都被忽略了。【参考方案7】:在我看来,这听起来有点矫枉过正——这种类型的优化将节省少量时间。例如,使用更现代的 gcc 版本将对优化产生更大的影响。另外,尝试启用和禁用所有不同的优化标志;它们并不能提高性能。
基本上,与许多其他富有成效的道路相比,这似乎不太可能产生任何重大影响。
编辑:感谢 cmets。我创建了这个社区 wiki,但将其保留在其中以便其他人可以看到 cmets。
【讨论】:
不可以有有效的用例。例如,有些编译器将直接代码输出到 c,并在每一行放置一个“if (break) break_into_debugger()”,以提供独立于平台的调试解决方案。 实际上,在深度流水线处理器上,分支预测错误非常昂贵,因为它们需要完整的流水线刷新。一个指令执行的成本是指令执行成本的 20 倍是一个合理的估计。如果他的基准测试告诉他分支预测有问题,那么他做的是正确的事情。顺便说一句,VTune 会为您提供非常好的数据,如果您还没有尝试过的话。以上是关于是否可以告诉分支预测器跟随分支的可能性有多大?的主要内容,如果未能解决你的问题,请参考以下文章