GCC 是否有编译器提示强制分支预测始终以某种方式进行?

Is there a compiler hint for GCC to force branch prediction to always go a certain way?

对于 Intel 体系结构,有没有一种方法可以指示 GCC 编译器生成始终在我的代码中以特定方式强制进行分支预测的代码?英特尔硬件甚至支持这个吗?其他编译器或硬件呢?

我会在 C++ 代码中使用它,在这种情况下,我希望 运行 快速并且不关心需要采用其他分支时的减速,即使它最近采用了该分支.

for (;;) {
  if (normal) { // How to tell compiler to always branch predict true value?
    doSomethingNormal();
  } else {
    exceptionalCase();
  }
}

作为 Evdzhan Mustafa 的后续问题,提示是否可以在处理器第一次遇到指令时指定提示,所有后续分支预测都正常运行?

GCC 支持函数 __builtin_expect(long exp, long c) 来提供这种特性。您可以查看文档 here.

其中exp是使用的条件,c是期望值。例如在你的情况下你会想要

if (__builtin_expect(normal, 1))

由于笨拙的语法,这通常通过定义两个自定义宏来使用,例如

#define likely(x)    __builtin_expect (!!(x), 1)
#define unlikely(x)  __builtin_expect (!!(x), 0)

只是为了减轻任务。

注意:

  1. 这是非标准的
  2. a compiler/cpu 分支预测器在决定此类事情时可能比您更熟练,因此这可能是过早的微优化

gcc 有 long __builtin_expect (long exp, long c)强调我的):

You may use __builtin_expect to provide the compiler with branch prediction information. In general, you should prefer to use actual profile feedback for this (-fprofile-arcs), as programmers are notoriously bad at predicting how their programs actually perform. However, there are applications in which this data is hard to collect.

The return value is the value of exp, which should be an integral expression. The semantics of the built-in are that it is expected that exp == c. For example:

if (__builtin_expect (x, 0))
   foo ();

indicates that we do not expect to call foo, since we expect x to be zero. Since you are limited to integral expressions for exp, you should use constructions such as

if (__builtin_expect (ptr != NULL, 1))
   foo (*ptr);

when testing pointer or floating-point values.

如文档所述,您应该更喜欢使用实际的配置文件反馈和 this article shows a practical example of this and how it in their case at least ends up being an improvement over using __builtin_expect. Also see How to use profile guided optimizations in g++?

我们还可以找到一个使用这个特性的Linux kernel newbies article on the kernal macros likely() and unlikely()

#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x)     __builtin_expect(!!(x), 0)

注意宏中使用的 !! 我们可以在 Why use !!(condition) instead of (condition)? 中找到对此的解释。

仅仅因为 Linux 内核中使用了这项技术并不意味着使用它总是有意义的。从我最近回答的这个问题可以看出 许多手动优化技术在一般情况下不起作用。我们需要仔细分析代码以了解技术是否有效。许多旧技术甚至可能与现代编译器优化无关。

请注意,虽然内置函数不可移植 clang also supports __builtin_expect

还有一些architectures it may not make a difference

__builtin_expect 可用于告诉编译器您希望分支走哪条路。这会影响代码的生成方式。典型的处理器 运行 编码速度更快。所以如果你写

if (__builtin_expect (x == 0, 0)) ++count;
if (__builtin_expect (y == 0, 0)) ++count;
if (__builtin_expect (z == 0, 0)) ++count;

编译器会生成类似

的代码
if (x == 0) goto if1;
back1: if (y == 0) goto if2;
back2: if (z == 0) goto if3;
back3: ;
...
if1: ++count; goto back1;
if2: ++count; goto back2;
if3: ++count; goto back3;

如果您的提示是正确的,这将执行代码而不会实际执行任何分支。它将 运行 比正常序列快,其中每个 if 语句将围绕条件代码分支并执行三个分支。

较新的 x86 处理器具有针对预期采用的分支或预期不采用的分支的指令(有指令前缀;不确定详细信息)。不确定处理器是否使用它。它不是很有用,因为分支预测可以很好地处理这个问题。所以我认为你实际上不能影响分支 prediction

不,没有。 (至少在现代 x86 处理器上是这样。)

其他答案中提到的

__builtin_expect 影响 gcc 排列汇编代码的方式。 不会直接影响CPU的分支预测。当然,重新排序会对分支预测产生间接影响代码。但是在现代 x86 处理器上没有指令告诉 CPU "assume this branch is/isn't taken".

有关详细信息,请参阅此问题:Intel x86 0x2E/0x3E Prefix Branch Prediction actually used?

需要明确的是,__builtin_expect and/or 使用 -fprofile-arcs 可以 提高代码的性能,两者都通过提示通过代码布局的分支预测器(参见 Performance optimisations of x86-64 assembly - Alignment and branch prediction),并通过使 "unlikely" 代码远离 "likely" 代码来改进缓存行为。

正如其他答案都充分建议的那样,您可以使用 __builtin_expect 向编译器提供有关如何安排汇编代码的提示。正如 the official docs 指出的那样,在大多数情况下,内置于您大脑中的汇编器不如 GCC 团队制作的汇编器好。最好使用实际的配置文件数据来优化您的代码,而不是猜测。

与此类似但尚未提及的是一种特定于 GCC 的方法,用于强制编译器在 "cold" 路径上生成代码。这涉及使用 noinlinecold 属性,它们的作用与它们听起来的完全一样。这些属性只能应用于函数,但在 C++11 中,您可以声明内联 lambda 函数,这两个属性也可以应用于 lambda 函数。

虽然这仍然属于微优化的一般类别,因此标准建议适用——测试不要猜测——我觉得它比 __builtin_expect 更有用。几乎没有哪一代 x86 处理器使用分支预测提示 (reference),因此无论如何您唯一能够影响的就是汇编代码的顺序。由于您知道什么是错误处理或 "edge case" 代码,因此您可以使用此注释来确保编译器永远不会预测它的分支并将 link 它远离 "hot" 优化尺寸时的代码。

示例用法:

void FooTheBar(void* pFoo)
{
    if (pFoo == nullptr)
    {
        // Oh no! A null pointer is an error, but maybe this is a public-facing
        // function, so we have to be prepared for anything. Yet, we don't want
        // the error-handling code to fill up the instruction cache, so we will
        // force it out-of-line and onto a "cold" path.
        [&]() __attribute__((noinline,cold)) {
            HandleError(...);
        }();
    }

    // Do normal stuff
    ⋮
}

更好的是,GCC 会在可用时自动忽略它以支持配置文件反馈(例如,当使用 -fprofile-use 编译时)。

在此处查看官方文档:https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes

关于 OP,不,在 GCC 中没有办法告诉处理器总是假定分支被采用或不被采用。您拥有的是 __builtin_expect,它按照其他人的说法行事。此外,我认为您不想告诉处理器分支是否被采用 always。今天的处理器,例如英特尔架构,可以识别相当复杂的模式并有效地适应。

但是,有时您想要控制 默认情况下 是否预测分支:当您知道代码将被调用时 "cold"关于分支统计。

一个具体的例子:异常管理代码。根据定义,管理代码会异常发生,但也许当它发生时需要最大性能(可能会尽快处理严重错误),因此您可能希望控制默认预测。

另一个例子:您可以对输入进行分类,然后跳转到处理分类结果的代码。如果有很多分类,处理器可能会收集统计数据但会丢失它们,因为相同的分类不会足够快地发生并且预测资源专用于最近调用的代码。我希望有一个原语来告诉处理器 "please do not devote prediction resources to this code" 你有时可以说 "do not cache this".

的方式

在 C++11 中定义 likely/unlikely 宏的正确方法如下:

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1)
#define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)

此方法与所有 C++ 版本兼容,与 [[likely]] 不同,但依赖于非标准扩展 __builtin_expect


当这些宏这样定义时:

#define LIKELY(condition) __builtin_expect(!!(condition), 1)

这可能会改变 if 语句的含义并破坏代码。考虑以下代码:

#include <iostream>

struct A
{
    explicit operator bool() const { return true; }
    operator int() const { return 0; }
};

#define LIKELY(condition) __builtin_expect((condition), 1)

int main() {
    A a;
    if(a)
        std::cout << "if(a) is true\n";
    if(LIKELY(a))
        std::cout << "if(LIKELY(a)) is true\n";
    else
        std::cout << "if(LIKELY(a)) is false\n";
}

及其输出:

if(a) is true
if(LIKELY(a)) is false

如您所见,使用 !! 作为对 bool 的转换的 LIKELY 定义打破了 if.

的语义

这里的重点不是operator int()operator bool()应该相关。这是很好的做法。

相反,使用 !!(x) 而不是 static_cast<bool>(x) 会丢失 C++11 contextual conversions 的上下文。

从 C++20 开始 likely and unlikely attributes should be standardized and are already supported in g++9. So as discussed ,你可以写

if (a > b) {
  /* code you expect to run often */
  [[likely]] /* last statement here */
}

例如在下面的代码中,由于 if

中的 [[unlikely]] ,else 块被内联
int oftendone( int a, int b );
int rarelydone( int a, int b );
int finaltrafo( int );

int divides( int number, int prime ) {
  int almostreturnvalue;
  if ( ( number % prime ) == 0 ) {
    auto k                         = rarelydone( number, prime );
    auto l                         = rarelydone( number, k );
    [[unlikely]] almostreturnvalue = rarelydone( k, l );
  } else {
    auto a            = oftendone( number, prime );
    almostreturnvalue = oftendone( a, a );
  }
  return finaltrafo( almostreturnvalue );
}

godbolt link comparing the presence/absence of the attribute