12

在现代奔腾上,似乎不再可能向处理器提供分支提示。假设像 gcc 这样的配置文件编译器具有配置文件引导优化获得有关可能的分支行为的信息,它可以做些什么来生成执行更快的代码?

我知道的唯一选择是将不太可能的分支移动到函数的末尾。还有别的事吗?

更新。

http://download.intel.com/products/processor/manual/325462.pdf第 2a 卷,第 2.1.1 节说

“分支提示前缀 (2EH, 3EH) 允许程序向处理器提示最可能的分支代码路径。仅将这些前缀与条件分支指令 (Jcc) 一起使用。其他使用分支提示前缀和/或其他带有 Intel 64 或 IA-32 指令的未定义操作码被保留;这样的使用可能会导致不可预知的行为。”

我不知道这些是否真的有任何影响。

另一方面,第 3.4.1 节。http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf说_

" 编译器生成的代码可提高英特尔处理器中分支预测的效率。英特尔 C++ 编译器通过以下方式完成此任务:

  • 将代码和数据保存在不同的页面上
  • 使用条件移动指令消除分支
  • 生成与静态分支预测算法一致的代码
  • 在适当的地方内联
  • 如果迭代次数是可预测的,则展开

通过配置文件引导优化,编译器可以布置基本块以消除函数最常执行路径的分支,或者至少提高它们的可预测性。分支预测不必是源级别的关注点。有关详细信息,请参阅英特尔 C++ 编译器文档。"

http://cache-www.intel.com/cd/00/00/40/60/406096_406096.pdf在“PGO 的性能改进”中说

" PGO 最适用于具有许多在编译时难以预测的频繁执行分支的代码。一个示例是具有密集错误检查的代码,其中错误条件大部分时间都是错误的。不经常执行(冷)错误处理代码可以重新定位,因此很少会错误地预测分支。将交错到频繁执行(热)代码中的冷代码最小化可以改善指令缓存行为。

4

4 回答 4

7

您想要的信息有两种可能的来源:

  1. 有 Intel 64 和 IA-32 架构软件开发人员手册(3 卷)。这是一项经过数十年发展的巨大工作。这是我所知道的很多主题的最佳参考,包括浮点数。在这种情况下,您要查看第 2 卷,即指令集参考。
  2. 有 Intel 64 和 IA-32 架构优化参考手册。这将简要地告诉您对每个微体系结构的期望。

现在,我不知道您所说的“现代奔腾”处理器是什么意思,这是 2013 年,对吧?现在没有奔腾了...

指令集确实支持通过条件分支指令(例如 JC、JZ 等)的前缀来告诉处理器是否期望采用或不采用分支。请参阅(1)的第 2A 卷,第 2.1.1 节(我拥有的版本)指令前缀。未采取和采取分别有 2E 和 3E 前缀。

至于这些前缀是否真的有任何影响,如果我们能得到这些信息,它将在优化参考手册中,你想要的微架构部分(我相信它不会是奔腾)。

除了使用这些之外,优化参考手册中还有一个关于该主题的完整部分,即第 3.4.1 节(我拥有的版本)。

在这里复制是没有意义的,因为您可以免费下载手册。简要地:

  • 使用条件指令(CMOV、SETcc)消除分支,
  • 考虑静态预测算法(3.4.1.3),
  • 内联
  • 循环展开

此外,一些编译器,例如 GCC,即使 CMOV 是不可能的,也经常执行按位算术来选择计算的两个不同事物之一,从而避免分支。在向量化循环时,它尤其适用于 SSE 指令。

基本上,静态条件是:

  • 预计将采用无条件分支(......有点可预期......)
  • 预计不会采用间接分支(由于数据依赖性)
  • 预计将采用后向条件(适用于循环)
  • 预计不会采用前向条件

您可能想阅读整个第 3.4.1 节。

于 2013-06-06T18:31:11.447 回答
3

如果很明显一个循环很少进入,或者它通常迭代很少,那么编译器可能会避免展开循环,因为这样做会增加很多有害的复杂性来处理边缘条件(奇数迭代等) .)。在这种情况下,尤其应避免矢量化。

编译器可能会重新排列嵌套测试,以便最常导致捷径的测试可用于避免对通过率为 50% 的事物执行测试。

可以优化寄存器分配以避免在常见情况下出现很少使用的块强制寄存器溢出。

这些只是一些例子。我敢肯定还有其他我没有想到的。

于 2013-05-30T18:06:30.463 回答
2

在我的脑海中,你有两个选择。

选项#1:将提示通知编译器并让编译器适当地组织代码。例如,GCC 支持以下...

__builtin_expect((long)!!(x), 1L)  /* GNU C to indicate that <x> will likely be TRUE */
__builtin_expect((long)!!(x), 0L)  /* GNU C to indicate that <x> will likely be FALSE */

如果你把它们放在宏形式中,比如......

#if <some condition to indicate support>
    #define LIKELY(x)    __builtin_expect((long)!!(x), 1L)
    #define UNLIKELY(x)  __builtin_expect((long)!!(x), 0L)
#else
    #define LIKELY(x)   (x)
    #define UNLIKELY(x) (x)
#endif

...您现在可以将它们用作...

if (LIKELY (x != 0)) {
    /* DO SOMETHING */
} else {
    /* DO SOMETHING ELSE */
}

这使得编译器可以根据静态分支预测算法自由地组织分支,和/或如果处理器和编译器支持它,则可以使用指示哪个分支更有可能被采用的指令。

选项#2:使用数学来避免分支。

if (a < b)
    y = C;
else
    y = D;

这可以重写为...

x = -(a < b);   /* x = -1 if a < b, x = 0 if a >= b */
x &= (C - D);   /* x = C - D if a < b, x = 0 if a >= b */
x += D;         /* x = C if a < b, x = D if a >= b */

希望这可以帮助。

于 2013-05-30T21:31:24.510 回答
1

它可以使直通(即不采用分支的情况)成为最常用的路径。这有两个很大的影响:

  1. 每个时钟只能采用 1 个分支,或者在某些处理器上甚至每 2 个时钟可以采用,所以如果有任何其他分支(通常有,大多数重要的代码都在循环中),采用的分支是坏消息,非采取分支较少。
  2. 当分支预测器错误时,它必须执行的代码更有可能在代码缓存(或微操作缓存,如果适用)中。如果不是这样,那将是重新启动管道等待缓存未命中的双重打击。这在大多数循环中不是什么问题,因为分支的两端都可能在缓存中,但它在大循环和其他代码中发挥作用。

它还可以根据比启发式猜测更好的数据来决定是否进行 if 转换。if-conversions 可能看起来像“总是一个好主意”,但它们不是,它们只是“通常是一个好主意”。如果分支实现中的分支被很好地预测,则 if 转换的代码可能会更慢。

于 2013-05-31T08:14:10.020 回答