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

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

对于英特尔体系结构,有没有一种方法可以指示GCC编译器生成代码,在我的代码中总是强制使用分支预测的特定方法?Intel硬件是否支持此功能?其他编译器或硬件呢?

我会在C++代码中使用这个代码,在这里我知道我希望快速运行的情况,也不关心其他分支需要采取的速度慢,即使它最近已经占领了该分支。

1
2
3
4
5
6
7
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)来提供这种特性。您可以在这里查看文档。

其中,exp是使用的条件,c是预期值。例如,在您的情况下,您希望

1
if (__builtin_expect(normal, 1))

由于语法不好,通常通过定义两个自定义宏来使用

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

只是为了减轻任务。

介意:

  • 这不是标准的
  • 编译器/CPU分支预测器可能比您更擅长决定这些事情,因此这可能是一个过早的微优化。

  • GCC有long_uu builtin_expect(long exp,long c)(emphasis mine):

    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:

    1
    2
    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

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

    when testing pointer or floating-point values.

    正如文档所指出的,您应该更喜欢使用实际的概要文件反馈,本文给出了一个实际的例子,以及在这种情况下,它是如何比使用__builtin_expect得到改进的。另请参见如何在g++中使用按配置优化?.

    我们还可以找到一篇关于kernal macros likely()和unsible()的Linux内核新手文章,其中使用了以下功能:

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

    注意宏中使用的!!,我们可以在"为什么使用"中找到解释!!(条件)而不是(条件)?.

    仅仅因为这种技术在Linux内核中使用并不意味着使用它总是有意义的。我们可以从这个问题看出,我最近回答了将参数作为编译时常量或变量传递时函数性能之间的差异,许多手动滚动优化技术在一般情况下不起作用。我们需要仔细分析代码以了解一种技术是否有效。许多旧技术甚至可能与现代编译器优化无关。

    请注意,虽然内置设备不是可移植的,但是clang也支持内置设备expect。

    另外,在某些架构上,它可能不会有什么不同。


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

    其他答案中提到的__builtin_expect会影响GCC安排汇编代码的方式。它不会直接影响CPU的分支预测器。当然,重新排序代码会对分支预测产生间接影响。但在现代x86处理器上,没有指令告诉CPU"假设这个分支被/不被采用"。

    有关详细信息,请参阅此问题:实际使用的Intel x86 0x2e/0x3e前缀分支预测?

    明确地说,__builtin_expect和/或使用-fprofile-arcs可以通过代码布局向分支预测器提供提示(请参阅x86-64程序集对齐和分支预测的性能优化),并通过保持"不可能"代码远离"可能"代码来改善缓存行为,从而提高代码的性能。


    在C++ 11中定义可能的/不太可能的宏的正确方法是:

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

    当这些宏以这种方式定义时:

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #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
    "
    ;
        if(LIKELY(a))
            std::cout <<"if(LIKELY(a)) is true
    "
    ;
        else
            std::cout <<"if(LIKELY(a)) is false
    "
    ;
    }

    其输出:

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

    如您所见,可能使用!!作为强制转换到bool的定义破坏了if的语义。

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

    相反,使用EDCOX1〔11〕代替EDCOX1〔12〕,就失去了C++ 11上下文转换的上下文。


    由于其他答案都有充分的建议,您可以使用__builtin_expect向编译器提供有关如何安排汇编代码的提示。正如官方文档所指出的,在大多数情况下,嵌入到你大脑中的汇编程序将不如GCC团队所设计的那样好。最好使用实际的概要数据来优化代码,而不是猜测。

    沿着类似的行(但还没有提到),是一种强制编译器在"冷"路径上生成代码的GCC特定方法。这涉及到noinlinecold属性的使用,这些属性的作用与它们听起来的作用完全相同。这些属性只能应用于函数,但是用C++ 11,可以声明内联lambda函数,这两个属性也可以应用于lambda函数。

    虽然这仍然属于微观优化的一般范畴,因此标准建议应用test-don-guess-我觉得它比__builtin_expect更普遍有用。几乎没有任何一代x86处理器使用分支预测提示(引用),所以您无论如何都能影响的唯一事情就是程序集代码的顺序。由于您知道什么是错误处理或"边缘大小写"代码,所以可以使用此注释来确保编译器不会预测到它的分支,并在优化大小时将其与"热"代码链接起来。

    样品使用情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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
        ?
    }

    更好的是,当配置文件反馈可用时(例如,在使用-fprofile-use编译时),GCC会自动忽略这一点,而倾向于配置文件反馈。

    参见官方文档:https://gcc.gnu.org/onlinedocs/gcc/common-function-attributes.html common-function-attributes


    _ builtin-expect可以用来告诉编译器一个分支应该朝哪个方向发展。这会影响代码的生成方式。典型的处理器按顺序运行代码更快。所以如果你写

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

    编译器将生成类似

    1
    2
    3
    4
    5
    6
    7
    8
    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处理器对预期要执行的分支或预期不执行的分支具有指令(有一个指令前缀;不确定详细信息)。不确定处理器是否使用它。它不是很有用,因为分支预测可以很好地处理这个问题。所以我认为你不能真正影响分支预测。


    关于操作,不,在GCC中没有办法告诉处理器总是假设分支被占用或不被占用。你所拥有的是内在的期待,它会像别人所说的那样。此外,我认为您不想告诉处理器是否总是使用分支。今天的处理器,如英特尔体系结构,可以识别相当复杂的模式并有效地适应。

    但是,有时您希望控制默认情况下是否使用分支:当您知道分支统计信息的代码将被称为"cold"时。

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

    另一个例子:您可以对输入进行分类,并跳转到处理分类结果的代码中。如果有许多分类,处理器可能会收集统计数据,但会丢失它们,因为相同的分类不会很快发生,并且预测资源专门用于最近调用的代码。我希望有一个原语告诉处理器"请不要将预测资源投入到这段代码中",就像你有时说的"不要缓存这段代码"。