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

Wil*_*mKF 113 c++ gcc intel pragma branch-prediction

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

我会在C++代码中使用它,我知道我希望快速运行的情况,并且不关心当另一个分支需要被采取时,即使它最近采用了该分支.

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

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

Jac*_*ack 83

GCC支持__builtin_expect(long exp, long c)提供此类功能的功能.您可以在此处查看文档.

exp使用的条件在哪里,c是预期值.例如,你想要的情况

if (__builtin_expect(normal, 1))
Run Code Online (Sandbox Code Playgroud)

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

#define likely(x)    __builtin_expect (!!(x), 1)
#define unlikely(x)  __builtin_expect (!!(x), 0)
Run Code Online (Sandbox Code Playgroud)

只是为了缓解任务.

请注意:

  1. 这是非标准的
  2. 编译器/ cpu分支预测器在决定这些事情时可能比你更熟练,所以这可能是一个过早的微优化

  • @Columbo:我不认为`constexpr`函数_can_替换这个宏.它必须直接在'if`声明中我相信.同样的原因`assert`永远不会是'constexpr`函数. (21认同)
  • @Columbo使用宏的一个原因是因为这是C或C++中为数不多的几个地方之一,其中宏在语义上比函数更正确*.该函数似乎只是因为优化而起作用(它*是一个优化:`constexpr`只讨论值语义,而不是特定于实现的程序集的内联); 代码的直接解释(没有内联)是没有意义的.根本没有理由为此使用函数. (7认同)
  • 是否有理由显示宏而不是`constexpr`功能? (3认同)
  • @Leushenko认为`__builtin_expect`本身是一个优化提示,因此认为简化其使用的方法取决于优化是......不具说服力.另外,我没有添加`constexpr`说明符使其首先工作,但是使它在常量表达式中工作.是的,有理由使用一个功能.例如,我不想用一个可爱的小名称来污染我的整个命名空间,例如`possible`.我必须使用例如`LIKELY`来强调它是一个宏并避免碰撞,但这简直是丑陋的. (2认同)
  • 如果没有 PGO,编译器几乎没有关于分支可能性的信息,因为它几乎没有上下文信息。使用了各种启发式方法,例如“不太可能采用返回常量的分支,因为这是常见的错误处理模式”,但它们的使用是有限的并且可能是完全错误的。另一方面,CPU 中的动态分支预测器更有可能使事情正确,但这或多或少是无关紧要的,因为代码是在那时生成的。源提示不会干扰预测器。 (2认同)

Sha*_*our 44

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

您可以使用__builtin_expect为编译器提供分支预测信息.一般来说,你应该更喜欢使用实际的配置文件反馈(-fprofile-arcs),因为程序员在预测程序实际执行情况方面是出了名的.但是,有些应用程序难以收集此数据.

返回值是exp的值,它应该是一个整数表达式.内置的语义是期望exp == c.例如:

if (__builtin_expect (x, 0))
   foo ();
Run Code Online (Sandbox Code Playgroud)

表示我们不期望调用foo,因为我们期望x为零.由于您仅限于exp的整数表达式,因此您应该使用诸如的结构

if (__builtin_expect (ptr != NULL, 1))
   foo (*ptr);
Run Code Online (Sandbox Code Playgroud)

在测试指针或浮点值时.

正如文档中指出的那样,您应该更喜欢使用实际的配置文件反馈,本文将展示一个实际的示例,以及它在这种情况下如何最终成为对使用的改进__builtin_expect.另请参阅如何在g ++中使用配置文件引导的优化?.

我们还可以找到关于使用此功能的kernal宏可能()和不太可能()Linux内核新手文章:

#define likely(x)       __builtin_expect(!!(x), 1)
#define unlikely(x)     __builtin_expect(!!(x), 0)
Run Code Online (Sandbox Code Playgroud)

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

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

注意,虽然builtins不是便携式,但clang也支持__builtin_expect.

对某些架构而言,它可能没有什么区别.


Art*_*ius 41

不,那里没有.(至少在现代x86处理器上.)

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

有关更多详细信息,请参阅此问题:英特尔x86 0x2E/0x3E前缀分支预测实际使用?

要明确__builtin_expect和/或使用-fprofile-arcs 可以提高代码的性能,通过代码布局给分支预测器提供提示(请参阅x86-64程序集的性能优化 - 对齐和分支预测),还可以改进缓存行为保持"不太可能"的代码远离"可能的"代码.

  • 这是不正确的.在x86的所有现代版本中,默认的预测算法是预测不采用前向分支和后向分支(参见https://software.intel.com/en-us/articles/branch-and-loop-reorganization -to-防止-错误预测).因此,通过重新排列代码,您可以*有效地给CPU提示.这正是GCC在使用`__builtin_expect`时所做的事情. (8认同)
  • @Nemo,你读过我答案的第一句话了吗?您所说的一切都由我的答案或给出的链接涵盖.问题是你是否可以"强制分支预测总是以某种方式",答案是"否",我觉得其他答案对此并不清楚. (6认同)
  • 好的,我应该仔细阅读.在我看来,这个答案在技术上是正确的,但没用,因为提问者显然在寻找`__builtin_expect`.所以这应该只是一个评论.但这不是假的,所以我删除了我的downvote. (4认同)
  • 在我看来,这并非毫无用处;它有用地阐明了 CPU 和编译器的实际工作原理,这可能与使用/不使用这些选项的性能分析相关。例如,您通常不能使用“__builtin_expect”来简单地创建一个测试用例,您可以使用“perf stat”来测量该测试用例,该测试用例将具有非常高的分支错误预测率。它只影响分支*布局*。顺便说一句,自 Sandybridge 或至少 Haswell 以来,英特尔“不”使用太多/根本不使用静态预测;BHT 中总会有一些预测,无论它是否是过时的别名。https://xania.org/201602/bpu-part-two (3认同)
  • 有关现代 Intel CPU(缺乏)静态预测的更多详细信息:[为什么 Intel 这些年来改变了静态分支预测机制?](/sf/ask/3627591201/) (2认同)

Max*_*kin 22

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

#define LIKELY(condition) __builtin_expect(static_cast<bool>(condition), 1)
#define UNLIKELY(condition) __builtin_expect(static_cast<bool>(condition), 0)
Run Code Online (Sandbox Code Playgroud)

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

#define LIKELY(condition) __builtin_expect(!!(condition), 1)
Run Code Online (Sandbox Code Playgroud)

这可能会改变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";
}
Run Code Online (Sandbox Code Playgroud)

它的输出:

if(a) is true
if(LIKELY(a)) is false
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,LIKELY的定义使用强制转换!!bool破坏语义if.

这里的要点不是那个operator int(),operator bool()应该是相关的.哪个是好习惯.

而是使用!!(x)而不是static_cast<bool>(x)丢失C++ 11上下文转换的上下文.

  • 在您的示例中,您仅使用了“(条件)”,而不是“ !!(条件)”。更改后,两者均为“ true”(已通过g ++ 7.1测试)。您可以构建一个示例来实际演示您使用!!进行布尔化时所谈论的问题吗? (2认同)
  • 正如彼得·科德斯(Peter Cordes)所指出的,您说“当这些宏以这种方式定义时:”,然后使用'!!'显示一个宏,“可能会改变if语句的含义并破坏代码。请考虑以下代码:” ...然后显示不使用“ !!”的代码 根本没有-甚至在C ++ 11之前就已被打破。请更改答案以显示给定宏(使用!!)出错的示例。 (2认同)

Cod*_*ray 15

由于其他答案都已充分建议,您可以使用__builtin_expect给编译器提供有关如何排列汇编代码的提示.正如官方文档指出的那样,在大多数情况下,构建在大脑中的汇编程序不如GCC团队制作的那样好.最好使用实际的配置文件数据来优化代码,而不是猜测.

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

虽然这仍然属于微优化的一般类别,因此标准建议适用 - 测试不要猜测 - 我觉得它比一般更有用__builtin_expect.几乎没有任何代x86处理器使用分支预测提示(参考),因此您唯一能够影响的是汇编代码的顺序.由于您知道什么是错误处理或"边缘情况"代码,因此您可以使用此批注来确保编译器不会预测到它的分支,并在优化大小时将其与"热"代码链接起来.

样品用法:

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
    ?
}
Run Code Online (Sandbox Code Playgroud)

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

请参阅此处的官方文档:https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes

  • 分支预测提示前缀被忽略,因为它们不是必需的; 只需重新排序代码即可达到完全相同的效果.(默认的分支预测算法是猜测采用后向分支而不是前向分支.)因此,您实际上可以给CPU一个提示,这就是`__builtin_expect`所做的.这根本没用.你是对的`冷'属性也是有用的,但你低估了我认为`__builtin_expect`的效用. (2认同)

pse*_*ert 11

从 C++20 开始,可能和不太可能的属性应该被标准化,并且已经在 g++9 中得到支持。所以正如这里所讨论的,你可以写

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

例如,在下面的代码中,由于[[unlikely]]inif块,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 );
}
Run Code Online (Sandbox Code Playgroud)

Godbolt 链接比较属性的存在/不存在


gna*_*729 5

__builtin_expect 可用于告诉编译器您期望分支的走向。这会影响代码的生成方式。典型的处理器按顺序运行代码更快。所以如果你写

if (__builtin_expect (x == 0, 0)) ++count;
if (__builtin_expect (y == 0, 0)) ++count;
if (__builtin_expect (z == 0, 0)) ++count;
Run Code Online (Sandbox Code Playgroud)

编译器会生成类似的代码

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;
Run Code Online (Sandbox Code Playgroud)

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

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


归档时间:

查看次数:

19325 次

最近记录:

8 年,5 月 前