编译器可以对分支信息做什么?

时间:2013-05-30 11:17:11

标签: optimization assembly compiler-construction branch-prediction x86

在现代奔腾上,似乎不再可能给处理器提供分支提示。假设一个分析编译器(如带有配置文件引导优化的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 个答案:

答案 0 :(得分:7)

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

  1. 有英特尔64和IA-32架构软件开发人员手册(3册)。这是一项已经发展了数十年的巨大工作。这是我所知道的很多主题的最佳参考,包括浮点数。在这种情况下,您需要检查第2卷,指令集参考。
  2. 有Intel 64和IA-32架构Optmization参考手册。这将以简短的术语告诉您对每个微体系结构的期望。
  3. 现在,我不知道你对“现代奔腾”处理器的意思,这是2013年,对吗?没有任何Pentiums ......

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

    至于这些前缀是否实际上有任何影响,如果我们可以获得该信息,它将在优化参考手册,您想要的微架构部分(我相信它不会是奔腾)。

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

    在此处重现这一点毫无意义,因为您可以免费下载该手册。 简言之:

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

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

    基本上,静态条件是:

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

    您可能希望阅读整个3.4.1节。

答案 1 :(得分:3)

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

编译器可能会重新排列嵌套测试,因此可以使用最常导致快捷方式的测试来避免对通过率为50%的内容执行测试。

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

这些只是一些例子。我确信还有其他一些我没有想过的。

答案 2 :(得分: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 */

希望这有帮助。

答案 3 :(得分:1)

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

  1. 每个时钟只能占用1个分支,或者甚至每2个时钟占用一些处理器,所以如果有任何其他分支(通常有大多数重要的代码都在循环中),则采用分支是坏消息,一个不采取的分支不那么。
  2. 当分支预测器错误时,它必须执行的代码更可能位于代码缓存(或μop缓存,如果适用)中。如果不是这样,那么重新启动管道等待缓存未命中会是双重打击。这在大多数循环中都不是问题,因为分支的两侧可能都在缓存中,但它在大循环和其他代码中发挥作用。
  3. 它还可以决定是否基于比启发式猜测更好的数据进行if转换。如果转换看起来像“总是一个好主意”,但它们不是,它们只是“通常是一个好主意”。如果分支实现中的分支被很好地预测,则if转换的代码可能会更慢。