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


118

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

我会在C ++代码中使用此代码,因为我知道我希望快速运行的情况,并且即使在最近采用了另一个分支的情况下,也不必关心速度的降低。

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

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


如果有任何异常(也独立
Shep

Answers:


9

从C ++ 20开始,应将可能和不太可能的属性标准化,并已在g ++ 9中支持。因此,作为讨论在这里,你可以写

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

例如,在以下代码中,由于if块中的else块,所以可以内联了else [[unlikely]]

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链接,比较属性的存在/不存在


为什么要[[unlikely]]ifvs [[likely]]中使用else
WilliamKF

没有理由,只是在尝试了属性需要去的地方之后才最终进入了这个星座。
pseyfert

很酷 不幸的是,该方法不适用于较早的C ++版本。
Maxim Egorushkin

神奇的螺栓连接
Lewis Kelsey,

87

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

exp条件在哪里,c是期望值。例如,如果您想要

if (__builtin_expect(normal, 1))

由于语法笨拙,通常通过定义两个自定义宏(例如

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

只是为了减轻任务。

注意:

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

3
是否有原因显示了宏而不是constexpr函数?
哥伦布2015年

22
@Columbo:我认为constexpr函数不能代替此宏。if我认为必须直接在声明中。相同的原因assert永远不会成为constexpr函数。
Mooing Duck 2015年

1
@MooingDuck我同意,尽管有更多的理由断言
Shafik Yaghmour,2015年

7
@Columbo使用宏的原因之一是因为这是C或C ++中为数不多的在语义更正确的地方之一比函数的地方之一。该函数似乎仅由于优化而起作用(它一种优化:constexpr仅讨论值语义,而不涉及特定于实现的程序集的内联);代码的直接解释(无内联)是没有意义的。完全没有理由为此使用函数。
Leushenko

2
@Leushenko认为它__builtin_expect本身是一个优化提示,因此认为简化其使用的方法取决于优化是……令人信服。另外,我没有添加说明constexpr符以使其首先起作用,而是使其以常量表达式起作用。是的,有理由使用函数。例如,我不想用一个可爱的小名字来污染我的整个命名空间,例如likely。我不得不使用例如LIKELY来强调它是一个宏,并避免冲突,但这很丑陋。
哥伦布2015年

46

gcc具有long __builtin_expect(long exp,long c)重点是我的):

您可以使用__builtin_expect为编译器提供分支预测信息。通常,您应该更喜欢为此(-fprofile-arcs)使用实际的配置文件反馈,因为众所周知,程序员在预测其程序的实际执行方式方面很差。但是,在某些应用程序中很难收集此数据。

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

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

表示我们不希望调用foo,因为我们希望x为零。由于限于exp的整数表达式,因此应使用诸如

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

测试指针或浮点值时。

如文档所述,您应该更喜欢使用实际的配置文件反馈,并且本文显示了一个实际的示例,以及在这种情况下,它至少最终是对使用的改进__builtin_expect。另请参阅如何在g ++中使用配置文件引导的优化?

我们还可以找到有关使用此功能的内核宏possible()和可能性不大Linux内核新手文章

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

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

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

注意,尽管内置不是可移植的 clang也支持__builtin_expect

同样,在某些体系结构上,可能没有什么不同


对于Linux内核来说,足够好就不能满足C ++ 11的要求。
Maxim Egorushkin's

@MaximEgorushkin注意,我实际上并不建议使用它,实际上,我引用的gcc文档甚至没有使用该技术。我想说,我的答案的主要目的是在走这条路之前要仔细考虑替代方案。
Shafik Yaghmour's

44

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

__builtin_expect其他答案中提到的内容会影响gcc安排汇编代码的方式。它不会直接影响CPU的分支预测器。当然,对代码进行重新排序将对分支预测产生间接影响。但是在现代的x86处理器上,没有指令告诉CPU“假定已/未采用此分支”。

请参阅此问题以获取更多详细信息:实际使用过Intel x86 0x2E / 0x3E前缀分支预测吗?

要清楚,__builtin_expect和/或使用-fprofile-arcs 可以提高代码的性能,既可以通过代码布局为分支预测器提供提示(请参阅x86-64程序集的性能优化-对齐和分支预测),也可以改善缓存行为通过使“不太可能”的代码远离“可能”代码。


9
这是不正确的。在所有现代版本的x86上,默认的预测算法是预测不采用前向分支,而采用后向分支(请参阅software.intel.com/en-us/articles/…)。因此,通过重新排列代码,您可以有效地向CPU提供提示。这正是GCC在您使用时所做的__builtin_expect
Nemo

6
@Nemo,您是否已读完我回答的第一句话?我的回答或给定的链接涵盖了您所说的一切。这个问题问您是否可以“强制分支预测始终遵循某种方式”,答案是“否”,我对此还没有其他答案很清楚。
Artelius

4
好,我应该仔细阅读。在我看来,这个答案在技术上是正确的,但是没有用,因为发问者显然正在寻找__builtin_expect。所以这应该只是评论。但这不是错误的,因此我删除了我的反对意见。
Nemo

海事组织这不是没有用的。这是对CPU和编译器实际工作方式的有用说明,可能与使用/不使用这些选项的性能分析有关。例如,您通常不能轻易__builtin_expect地创建一个可以用perf stat其进行测量的测试用例,这将导致很高的分支错误预测率。它只影响分支布局。顺便说一句,自Sandybridge或至少Haswell起,Intel根本使用静态预测。BHT中总是有一些预测,无论它是否是陈旧的别名。 xania.org/201602/bpu-part-two
Peter Cordes

24

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

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

不同于[[likely]],此方法与所有C ++版本兼容,但依赖于非标准扩展__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

如您所见,LIKELY的定义!!用作强制转换来bool破坏的语义if

这里的关键不在于operator int()并且operator bool()应该是相关的。这是个好习惯。

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


请注意,上下文转换是通过2012年的缺陷而来的,即使在2014年末,实现上也存在分歧。实际上,看来我链接到的情况对于gcc仍然不起作用。
Shafik Yaghmour's

@ShafikYaghmour对于涉及到的上下文转换switch,这是一个有趣的观察,谢谢。这里涉及的上下文转换是类型bool特定的,并且在那里列出了五个特定的上下文,其中不包括switch上下文。
Maxim Egorushkin's

这只会影响C ++,对不对?因此,没有理由去更改现有的C项目以使用(_Bool)(condition),因为C没有运算符重载。
彼得·科德斯

2
在您的示例中,您仅使用了(condition),而不是!!(condition)。两者都true在更改之后(使用g ++ 7.1测试)。您可以构建一个示例来实际演示您在!!布尔化时所谈论的问题吗?
彼得·科德斯

3
正如彼得·科德斯(Peter Cordes)所指出的,您说“当这些宏以这种方式定义时:”,然后使用'!!'显示一个宏,“可能会改变if语句的含义并破坏代码。请考虑以下代码:” ...然后显示不使用“ !!”的代码 根本没有-甚至在C ++ 11之前就已被打破。请更改答案以显示给定宏(使用!!)出错的示例。
卡罗·伍德

18

由于其他答案均已得到充分建议,因此您可以使用 __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
    
}

更好的是,GCC会自动忽略它,而在有配置文件反馈时(例如,使用进行编译时-fprofile-use)会使用配置文件反馈。

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


2
分支预测提示前缀将被忽略,因为不需要它们。您只需重新排序代码就可以达到完全相同的效果。(默认的分支预测算法是猜测是否采用了后向分支,而没有采用前向分支。)因此,实际上,您可以给CPU一个提示,而这就是这样__builtin_expect做的原因。这一点都没有用。您认为该cold属性也很有用,但是您低估了__builtin_expect我认为的效用。
Nemo

现代的Intel CPU不使用静态分支预测。您所描述的算法@Nemo,在较早的处理器中使用了向后分支,而在未使用的分支中则预测了向后分支,直到奔腾M左右为止,但是现代设计基本上只是随机猜测,索引到它们的分支中表在哪里会想到找该分支和使用信息的任何信息存在(即使它可以基本上是垃圾)。因此,从理论上讲分支预测提示很有用,但实际上却没有用,这就是英特尔删除它们的原因。
科迪·格雷

需要明确的是,分支预测的实现极其复杂,注释中的空间限制迫使我过于简化。实际上,这本身就是一个完整的答案。在像Haswell这样的现代微体系结构中,仍然可能会有静态分支预测的痕迹,但是它并不像以前那样简单。
科迪·格雷

您是否有“现代Intel CPU不使用静态分支预测”的参考?英特尔自己的文章(software.intel.com/en-us/articles/…)则表示相反……但这是从2011年起
Nemo

真的没有官方参考@Nemo。英特尔对其芯片中使用的分支预测算法非常守口如瓶,将其视为商业秘密。已知的大部分已通过经验测试得出。与以往一样,Agner Fog的材料是最好的资源,但即使他说:“分支预测器似乎已在Haswell中进行了重新设计,但对其构造知之甚少。” 我不记得我第一次看到基准测试的地方,这表明不再使用静态BP。
科迪·格雷

5

__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处理器对预期采用的分支或预期不采用的分支具有指令(有指令前缀;不确定细节)。不知道处理器是否使用它。它不是很有用,因为分支预测可以很好地解决这一问题。因此,我认为您实际上无法影响分支预测


2

关于OP,不,在GCC中没有办法告诉处理器始终假定分支已被采用或未被采用。您拥有的是__builtin_expect,它可以完成别人所说的事情。此外,我认为您不想告诉处理器分支是否总是使用。当今的处理器(例如英特尔架构)可以识别相当复杂的模式并有效地进行调整。

但是,有时您想控制默认情况下是否预测会发生分支:如果您知道分支统计信息的代码将称为“冷”。

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

另一个示例:您可以对输入进行分类,然后跳入处理分类结果的代码。如果分类很多,则处理器可能会收集统计信息,但会丢失统计信息,因为同一分类不会足够快地发生,并且预测资源专用于最近调用的代码。我希望能有一个原始的方式告诉处理器“请不要将预测资源用于此代码”,有时您会说“请勿缓存此代码”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.