从我的大学课程中,我听说,按照惯例,最好放置更多可能的条件if
而不是in else
,这可能有助于静态分支预测器.例如:
if (check_collision(player, enemy)) { // very unlikely to be true
doA();
} else {
doB();
}
Run Code Online (Sandbox Code Playgroud)
可以改写为:
if (!check_collision(player, enemy)) {
doB();
} else {
doA();
}
Run Code Online (Sandbox Code Playgroud)
我发现了一篇博客文章分支模式,使用GCC,它更详细地解释了这种现象:
为if语句生成前向分支.使它们不可能被采用的基本原理是处理器可以利用分支指令之后的指令可能已经被放置在指令单元内的指令缓冲器中的事实.
旁边,它说(强调我的):
在编写if-else语句时,总是使"then"块比else块更可能被执行,因此处理器可以利用已经放在指令获取缓冲区中的指令.
最终,有一篇由英特尔,分支和循环重组编写的文章,以防止错误预测,其中总结了两个规则:
当微处理器遇到分支时没有收集数据时使用静态分支预测,这通常是第一次遇到分支.规则很简单:
- 正向分支默认不采用
- 向后分支默认采用
为了有效地编写代码以利用这些规则,在编写if-else或switch语句时,首先检查最常见的情况,然后逐步处理最不常见的情况.
据我所知,这个想法是流水线CPU可以遵循指令缓存中的指令,而不会通过跳转到代码段内的另一个地址来破坏它.但是,我知道,在现代CPU微体系结构的情况下,这可能会过于简单化.
但是,看起来GCC不尊重这些规则.鉴于代码:
extern void foo();
extern void bar();
int some_func(int n)
{
if (n) {
foo();
}
else {
bar();
}
return 0; …
Run Code Online (Sandbox Code Playgroud) 我在Linux内核代码中遇到了这两个宏.我知道它们是编译器(gcc)的指令,用于在分支的情况下进行优化.我的问题是,我们可以在用户空间代码中使用这些宏吗?它会进行任何优化吗?任何例子都会非常有用.
C ++ 20引入的属性[[likely]]
和[[unlikely]]
该语言,其可以被用于允许编译器优化为的情况下一个执行路径或者多更容易或远小于可能比其他的。
考虑到错误分支预测的成本,这似乎是一个在代码的性能关键部分可能非常有用的功能,但我不知道它实际上会导致编译器做什么。
是否有一段简单的代码可以添加[[likely]]
和[[unlikely]]
属性更改编译器的程序集输出?也许更重要的是,这些变化有什么作用?
我为自己的理解创建了一个简单的示例,以查看程序集是否有任何差异,但似乎这个示例太简单了,无法实际显示对程序集的任何更改:
void true_path();
void false_path();
void foo(int i) {
if(i) {
true_path();
} else {
false_path();
}
}
void bar(int i) {
if(i) [[likely]] {
true_path();
} else [[unlikely]] {
false_path();
}
}
Run Code Online (Sandbox Code Playgroud)
这个问题是关于C++ 20的[[likely]]
/ [[unlikely]]
功能,而不是编译器定义的宏.
这个文档(cppreference)只给出了一个将它们应用于switch-case语句的例子.这个switch-case示例与我的编译器完美编译(g ++ - 7.2),所以我假设编译器已经实现了这个功能,尽管它还没有在当前的C++标准中正式引入.
但是当我像这样使用它们时if (condition) [[likely]] { ... } else { ... }
,我收到了一个警告:
"警告:语句开头的属性被忽略[-Wattributes]".
那么我应该如何在if-else语句中使用这些属性呢?
假设我有一个由三个函数A,B和C组成的编译单元.从编译单元外部的函数调用一次(例如,它是一个入口点或回调函数); A被A多次调用(例如,它在紧密循环中被调用); 每次调用B都会调用一次C(例如,它是一个库函数).
通过A(通过B和C)的整个路径对性能至关重要,尽管A本身的性能不是关键的(因为大部分时间花在B和C上).
什么是应该注释的最小函数集,__attribute__ ((hot))
以实现对此路径的更积极的优化?假设我们不能使用-fprofile-generate
.
等价:__attribute__ ((hot))
是指"优化此函数的主体","优化对此函数的调用","优化此函数所做的所有后代调用",还是它们的某种组合?
GCC信息页面没有明确解决这些问题.
开发人员可以使用__builtin_expect
内置函数来帮助编译器了解分支可能走向哪个方向.
将来,我们可能会为此目的获得一个标准属性,但截至今天至少全部clang
,icc
并gcc
支持非标准属性__builtin_expect
.
但是,icc
当你使用它时,似乎会生成奇怪的代码1.也就是说,无论使用哪个方向进行预测,使用内置函数的代码都严格地比没有内置代码的代码更糟糕.
以下面的玩具功能为例:
int foo(int a, int b)
{
do {
a *= 77;
} while (b-- > 0);
return a * 77;
}
Run Code Online (Sandbox Code Playgroud)
在三个编译器中,icc
唯一一个将其编译为3个指令的最佳标量循环:
foo(int, int):
..B1.2: # Preds ..B1.2 ..B1.1
imul edi, edi, 77 #4.6
dec esi #5.12
jns ..B1.2 # Prob 82% #5.18
imul eax, edi, 77 #6.14
ret
Run Code Online (Sandbox Code Playgroud)
无论 …
假设我有一个表达式,其中只有一部分是不太可能的,但另一个是统计中性的:
if (could_be || very_improbable) {
DoSomething();
}
Run Code Online (Sandbox Code Playgroud)
如果我将非常不可能的位放在unlikely()
宏中,它会以任何方式帮助编译器吗?
if (could_be || unlikely(very_improbable)) {
DoSomething();
}
Run Code Online (Sandbox Code Playgroud)
注意:我不是在问马科斯是如何工作的 - 我理解这一点.这里的问题是关于GCC,如果我只暗示其中的一部分,它是否能够优化表达式.我也认识到它可能在很大程度上取决于所讨论的表达方式 - 我对那些有这些宏经验的人很有吸引力.
我正在使用GCC编译器测试C/C++中的各种优化.我目前有一个包含多个嵌套if语句的循环.条件是在程序执行开始时计算的.看起来有点像这样:
bool conditionA = getA();
bool conditionB = getB();
bool conditionC = getC();
//Etc.
startTiming();
do {
if(conditionA) {
doATrueStuff();
if(conditionB) {
//Etc.
} else {
//Etc.
}
} else {
doAFalseStuff();
if(conditionB) {
//Etc.
} else {
//Etc.
}
}
} while (testCondition());
endTiming();
Run Code Online (Sandbox Code Playgroud)
doATrueStuff()
内联函数在哪里进行一些简单的数值计算,因此调用它没有任何开销.
不幸的是,不能事先定义条件,它们必须在运行时计算.我们甚至无法可靠地预测他们是真是假的可能性.getA()
也许是rand()%2
.但经过计算,它们的价值永远不会改变.
我想到了两个解决方案,一个是全局函数指针,用于在循环中调用适当的函数,如下所示:
void (*ptrA)(void);
//Etc.
int main(int argc, char **argv) {
//...
if (conditionA) {
ptrA=&aTrueFunc;
} else {
ptrA=&aFalseFunc;
}
//...
do {
(*ptrA)();
} while (testCondition());
//... …
Run Code Online (Sandbox Code Playgroud) 从这里我知道英特尔近年来实施了几种静态分支预测机制:
80486年龄:永远不被采取
Pentium4年龄:未采取后退/前锋
像Ivy Bridge,Haswell这样的新型CPU变得越来越无形,请参阅Matt G的实验.
英特尔似乎不想再谈论它,因为我在英特尔文档中找到的最新资料大约是十年前写的.
我知道静态分支预测(远远不是)比动态更重要,但在很多情况下,CPU将完全丢失,程序员(使用编译器)通常是最好的指南.当然,这些情况通常不是性能瓶颈,因为一旦频繁执行分支,动态预测器就会捕获它.
由于英特尔不再在其文档中明确声明动态预测机制,因此GCC的builtin_expect()只能从热路径中删除不太可能的分支.
我不熟悉CPU的设计,我不知道究竟是什么机制,目前英特尔使用其静态预测,但我还是觉得英特尔的最佳机制应该清楚地记录他的CPU",我打算去当动态预测失败,向前或向后',因为通常程序员是当时最好的指南.
更新:
我发现你提到的主题逐渐超出我的知识范围.这里涉及一些动态预测机制和CPU内部细节,我在两三天内无法学习.所以请允许我暂时退出你的讨论并充电.
这里仍然欢迎任何答案,也许会帮助更多人
compiler-construction x86 intel cpu-architecture branch-prediction
我阅读了有关 C++ 20 的更多内容,最近注意到了[[likely]]
or[[unlikely]]
属性。这似乎是一个有趣的概念,在以前的 C++ 版本中没有发现。根据官方 CPP 参考资料:
允许编译器针对以下情况进行优化:包含该语句的执行路径比不包含此类语句的任何替代执行路径的可能性更大或更小。
这到底意味着什么?
这篇博文反对使用它们,因为它看起来更像是不成熟的优化形式和一些其他细节。https://blog.aaronballman.com/2020/08/dont-use-the-likely-or-unlikely-attributes/