我发现分支预测错误可能会成为应用程序性能的热点瓶颈。人们通常展示揭示问题的汇编代码,并指出程序员通常可以预测分支走向大部分时间,从而避免分支预测错误。
我的问题是:
是否有一些高级编程技术(即无需汇编)可以避免分支预测错误?
在高级编程语言中编写友好于分支的代码时应注意什么(我主要关注C和C++)?
欢迎提供代码示例和基准测试。
我发现分支预测错误可能会成为应用程序性能的热点瓶颈。人们通常展示揭示问题的汇编代码,并指出程序员通常可以预测分支走向大部分时间,从而避免分支预测错误。
我的问题是:
是否有一些高级编程技术(即无需汇编)可以避免分支预测错误?
在高级编程语言中编写友好于分支的代码时应注意什么(我主要关注C和C++)?
欢迎提供代码示例和基准测试。
人们经常认为程序员通常可以预测一个分支会走向何方。
(*) 经验丰富的程序员经常提醒人们,人类程序员非常擅长这个。
1- 是否有可能使用一些高级编程技术(即没有汇编)来避免分支错误预测?
在标准的 C++ 或 C 中不行。至少对于单个分支而言是不行的。你可以尽量减小依赖链的深度,使分支错误预测不会产生任何影响。现代 CPU 会执行分支的两条路径并丢弃未被选择的那个。然而,这也有其限制,这就是为什么分支预测只在深层次的依赖链中起作用。
一些编译器提供了扩展功能,可以手动建议预测,例如 gcc 中的 __builtin_expect。这里有一个关于此的 stackoverflow question。更好的是,一些编译器(如 gcc)支持对代码进行分析,并自动检测最优预测。由于 (*) 的原因,使用分析比手动工作更为智能。
2- 我如何在使用高级编程语言(主要是 C 和 C++)编写分支友好的代码时需要注意什么?
首先,你应该记住,分支预测只会影响程序中最性能关键的部分,并且不要担心它,直到你已经测量并发现了问题。
但是当一些分析工具(如 valgrind、VTune 等)告诉我在 foo.cpp 的第 n 行存在分支预测惩罚时,我该怎么办呢?
Lundin 给出了非常明智的建议。
__builtin_expect
做了注释,我在这里引用(https://dev59.com/Zl0a5IYBdhLWcg3wmZzC)说,*你应该优先使用实际的性能反馈来进行优化(-fprofile-arcs),因为程序员经常无法准确预测他们的程序实际运行情况。* - Shafik Yaghmourstruct Foo
{
...
Foo& operator=(const Foo& other)
{
// Avoid unnecessary self-assignment.
if (this != &other)
{
...
}
return *this;
}
...
};
struct Foo
{
...
Foo& operator=(const Foo& other)
{
// Don't check for self-assignment.
...
return *this;
}
...
};
...这可以帮助,因为自我分配往往非常罕见。我们通过冗余地进行自我分配来减缓罕见情况,但是通过避免在所有其他情况下进行检查来加快常见情况。当然,由于分支中存在常见/罕见情况偏差,这不太可能显着降低分支错误预测的数量,但是嘿,不存在的分支无法被错误预测。
一个小向量的幼稚尝试
作为个人经历,我曾经在一个大型C代码库中工作,其中经常有许多类似于以下代码的代码:
char str[256];
// do stuff with 'str'
我们拥有相当庞大的用户群,自然会有一些稀有用户在我们的软件中输入超过255个字符长度的材料名称并溢出缓冲区,导致段错误。我们的团队开始使用C++,将许多源文件移植到C++并用以下代码替换这种代码:
std::string str = ...;
// do stuff with 'str'
...这些缓冲区溢出问题可以很容易地被解决。然而,至少在那时,像 std::string
和 std::vector
这样的容器结构是在堆(自由存储区)上分配的,我们发现我们为效率而牺牲了正确性/安全性。其中一些替换区域对性能至关重要(在紧密循环中调用),虽然我们通过这些大规模替换消除了很多错误报告,但用户开始注意到减速。
因此,我们希望有一种类似于这两种技术之间的混合体。我们想要能够将某些东西放在那里以实现对 C 风格固定缓冲区变量的安全性(这对于常见情况非常好且非常高效),但仍然适用于罕见情况,即缓冲区不足以容纳用户输入。我是团队中的性能极客之一,也是少数使用分析器的人之一(不幸的是,我与很多认为自己太聪明而不需要使用分析器的人一起工作),所以我被召唤来完成这项任务。
我的第一个天真尝试是这样的(大大简化了:实际使用了放置 new 等等,并且是完全符合标准的序列)。它涉及使用固定大小的缓冲区(在编译时指定大小)来处理常见情况,如果大小超过该容量,则使用动态分配的缓冲区。
template <class T, int N>
class SmallVector
{
public:
...
T& operator[](int n)
{
return num < N ? buf[n]: ptr[n];
}
...
private:
T buf[N];
T* ptr;
};
这次尝试彻底失败了。虽然构造过程中没有付出堆/自由存储器的代价,但在operator[]
的分支处理中,它比std::string
和std::vector<char>
还要糟糕,并且显示为剖析热点,而不是malloc
(我们供应商实现的std::allocator
和operator new
在后台使用malloc
)。于是我很快想到了在构造函数中将ptr
简单地赋值给buf
。现在,在常见情况下,ptr
指向buf
,并且现在可以这样实现operator[]
:
T& operator[](int n)
{
return ptr[n];
}
if (!try_something())
return error;
if (!try_something_else())
return error;
...
for each entity in world:
entity.do_something() // virtual call
for each human in world.humans():
human.do_something()
for each orc in world.orcs():
orc.do_something()
for each creature in world.creatures():
creature.do_something()
Linux内核基于gcc内置的__builtin_expect
宏定义了likely
和unlikely
宏:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
(请查看这里,以查看include/linux/compiler.h
中宏定义的内容)
您可以像这样使用它们:
if (likely(a > 42)) {
/* ... */
}
或者if (unlikely(ret_value < 0)) {
/* ... */
}
(1-exceptionChance)*overheadOfErrorHandlingInNormalCase
的时间。如果你每秒钟调用一个函数一千次,并且有一个错误返回值,那么必须每秒钟检查一千次。如果该错误是异常,编译器可以优化无错误的情况。如果错误被编码为负整数,则编译器没有这种指导。 - MSalters0x2E
)。 - MSalters1- 有没有可能使用一些高级编程技术(即不使用汇编)来避免分支预测错误?
避免?也许不行。减少?当然可以...
2- 我应该注意什么才能在高级编程语言中编写出友好于分支的代码(我主要关注C和C++)?
值得注意的是,为一台机器进行优化并不一定适用于另一台机器。考虑到这一点,基于您提供的任何测试输入,基于剖面的优化可以很好地重新排列分支。这意味着您不需要进行任何编程即可执行此优化,并且它应该相对适合您正在剖析的任何机器。显然,当您的测试输入和您剖析的机器大致符合常见期望时,将实现最佳结果...但这些也是任何其他优化(包括与分支预测相关的优化)的考虑因素。
_builtin_expect(condition, 1)
来强制编译器将条件视为未采取。即使分支的两侧都是微不足道的,无分支并不总是更好。当分支预测有效时,它比循环携带数据依赖更快。
参见gcc优化标志-O3使代码比-O2慢,其中gcc -O3
将一个if()
转换为无分支代码,尽管情况非常可预测,但使其变慢。
有时,您有信心某个条件是不可预测的(例如,在排序算法或二分搜索中)。或者,您更关心最坏情况不会比快速情况慢10倍,而不是快速情况比最坏情况快1.5倍。
cmov
)。x = x>limit ? limit : x; // likely to compile branchless
if (x>limit) x=limit; // less likely to compile branchless, but still can
x
,而第二种方法不在其中一个分支中修改x
。这似乎是一些编译器倾向于发出分支而不是cmov
的原因。即使x
是一个本地的int
变量,在寄存器中已经存在,所以“写入”它不涉及对内存的存储,只需更改寄存器中的值。cmov
好。我在那个答案中这样做是因为我知道编译器将使用单个指令生成所需的掩码(并从clang如何执行此操作中看到)。
__builtin_expect
。 - Basile Starynkevitch