断言中的分支预测提示

15

我在C++应用程序中使用一个自定义的ASSERT(...)宏。

#include <stdlib.h>
#include <iostream>

/// ASSERT(expr) checks if expr is true.  If not, error details are logged
/// and the process is exited with a non-zero code.
#ifdef INCLUDE_ASSERTIONS
#define ASSERT(expr)                                                      \
    if (!(expr)) {                                                        \
        char buf[4096];                                                   \
        snprintf (buf, 4096, "Assertion failed in \"%s\", line %d\n%s\n", \
                 __FILE__, __LINE__, #expr);                              \
        std::cerr << buf;                                                 \
        ::abort();                                                        \
    }                                                                     \
    else // This 'else' exists to catch the user's following semicolon
#else
#define ASSERT(expr)
#endif

最近我在阅读一些Linux内核模块代码时,发现了likely(...)unlikely(...)宏的存在。它们向CPU提供了一个提示,表明给定分支更可能被执行,因此流水线应优化该路径。

断言通常情况下应该为真(即likely)。

我能否在我的ASSERT宏中提供类似的提示?这里的基本机制是什么?

显然,我会测量任何性能差异,但理论上应该有任何差异吗?

我只在Linux上运行我的代码,但也想知道是否有跨平台的方法。我还使用gcc,但也想支持clang。


1
<cassert>中的assert有什么问题吗? - Mat
1
除非你要在性能关键的循环中放置ASSERT,否则它真的不会有任何影响。此外,分支预测对于这些一致的分支来说已经相当不错了,因此即使在性能关键的循环中,在现代CPU上也不应该有太大的差异。 - Paul R
1
除非你的断言在热路径上,否则我怀疑你不会注意到任何差异,即使有差异,我也怀疑差异不会很大。另外,你的问题是什么?“我能在我的ASSERT宏中提供类似的提示吗?”是的,如果你愿意,当然可以使用likelyunlikely - user703016
顺便提一下:你不需要那个 else。在 C 语言中,空语句是完全可以接受的,而且不会改变代码的含义。if (foo) {};if (foo) {} 没有任何区别。 - Blacklight Shining
优化你的ASSERT是易于实现的。明智的做法是把它搞对然后继续前进。 - Sergei
显示剩余2条评论
3个回答

15

性能提升可能不会很显著,但这就是 Linux 内核宏的定义方式:

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

因此,您可以修改条件,如下所示(假设期望expr为真,因此期望!(expr)为假):

if (__builtin_expect(!(expr), 0)) {

或者您可以将相同的宏定义为内核并在代码中使用它们以获得更好的可读性。

这是gcc内置的功能,所以当然不具备可移植性。

这篇文章表明clang也支持此内置功能。否则,您可以使用上述宏,并在不支持内置功能的编译器上通过条件定义来定义它们,例如#define likely(x) (x)

在您的情况下,预测结果将非常好(要么就是程序停止),因此不应该存在悲观情况的风险。但是,如果您考虑更广泛地使用内置功能,则来自gcc文档的建议是:

一般而言,您应该更倾向于使用实际的分析反馈(-fprofile-arcs),因为程序员对他们的程序实际执行情况的预测往往有误。


2
gcc也有-fprofile-generate标志来使用分支剖析仪对程序进行检测,然后运行程序以生成剖析数据,并使用-fprofile-use重新编译。它将自动设置类似于expect的预测。 - keltar
@keltar,说得好。我添加了一句引用自文档的话,也建议进行性能反馈。 - eerorika
很可能是相当无意义的,因为当大脑/断言达到它时会失去许多许多许多周期。 - Quonux

11

对于许多CPU来说,likelyunlikely(或其他任何内容)都不会向CPU提供分支提示(只有编译器会使用它来进行类似于基于性能分析的优化),原因很简单,这是无法实现的。

例如,自P4以来,x86定义了分支提示。在此之前,它们没有效果,但更糟糕的是,它们对除P4以外的所有东西都没有影响。所以它们是无用的(但会浪费空间和带宽),据我所知,GCC不会发出它们。

ARM也没有(还没有?)分支提示。 PPC,IA64和SPARC具有暗示的分支,我不知道GCC是否对它们使用likelyunlikely,但至少它可以这样做。


+1 是为了指出指令带宽的重要性,即使我们生活在一个充满 16 字节行的 CPU 和多重分派功能的花哨世界中...我猜很少人知道 :) - Quonux
3
我几乎可以确定 "expect" 更像是提示编译器重新排列指令以[可能]减少条件跳转; 即使没有实际的指定提示指令,也可能会有性能提升。但效果可能会受到CPU自身的硬件分支预测的影响。 - keltar
这并不完全正确。当分支预测器没有收集到统计数据时,它仍然必须进行一些预测。而默认的预测(至少在英特尔上)是向后跳转被采取(循环),而向前跳转不被采取(if语句)。通过重新排列代码,编译器隐含地提供了这些提示。 - Yakov Galka
@ybungalobill 不一定,Core2 只是使用预测器中的任何垃圾,无论它是否实际上与分支相关。因此,提示就不起作用了。但是它也没有计数。依赖默认值特别是缺少提示。 - harold
@harold:不知道Core2。但我无法理解你的最后一句话。没有人定义“提示”必须是操作码前缀。也没有人说可能/不可能会发出这样的前缀。利用处理器所做的记录假设的代码布局与任何其他有效提示一样。可能/不可能以及PGO会导致编译器重新排列代码以匹配这些假设。最终,您的可能/不可能成为支持此协议的处理器的隐式提示。 - Yakov Galka
@ybungalobill 不一定是前缀,显然。暗示是个矛盾修辞法。不告诉CPU任何东西不是提示,它什么也不是。真的什么都没有。这就像在大物体上堆叠小物体并不是提示物理学让堆不倒。不是提示 - 只是利用现有的行为。 - harold

2

不需要任何额外的注释。编译器已经知道abort被非常罕见地调用(每个程序执行最多一次),因此编译器将会将包含abort的分支视为不太可能的分支。您可以通过查看abort的声明来验证这一点。在glibc中,它声明为

extern void abort (void) __THROW __attribute__ ((__noreturn__));

在Visual Studio 2013中:

_CRTIMP __declspec(noreturn) void __cdecl abort(void);

关于编译器对 abort 的了解,你有参考资料吗? - Cody Gray
@CodyGray 我添加了一些参考资料。 - pentadecagon
1
noreturn 意味着它不通过常规方式返回。这样的函数仍然可以被多次甚至频繁地调用,但是通过其他方式返回,比如 longjmp - Yakov Galka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接