如果不太可能出现硬崩溃错误,我应该使用吗?

Should I use if unlikely for hard crashing errors?

我经常发现自己编写的代码看起来像这样:

if(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);

我应该更喜欢使用 if unlikely 处理错误吗?

if unlikely(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);

编译器会自动推断出应该缓存代码的哪一部分吗?或者这是一件真正有用的事情吗?为什么我没有看到很多人处理这样的错误?

是的,你可以做到。但更好的是将 throw 移动到一个单独的函数中,并用 __attribute__((cold, noreturn)) 标记它。这将消除在每个调用站点说 unlikely() 的需要,并且可以通过将异常抛出逻辑完全移到快乐路径之外来改进代码生成,从而提高指令缓存效率和内联可能性。

如果您更喜欢使用 unlikely() 作为语义符号(以使代码更易于阅读),那也很好,但它本身并不是最佳选择。

视情况而定。

首先,您绝对可以这样做,而且这很可能(双关语)不会损害您的应用程序的性能。但请注意,likely/unlikely 属性是特定于编译器的,应进行相应的修饰。

其次,如果您想要提高性能,结果将取决于目标平台(以及相应的编译器后端)。如果我们谈论的是 'default' x86 架构,您将不会在现代芯片上获得太多利润 - 这些属性将产生的唯一变化是代码布局的变化(与早期支持 x86 的软件分支不同预言)。对于小型分支机构(如您的示例),它对缓存利用率的影响很小 and/or 前端延迟。

更新:

Will the compiler automatically deduce which part of the code should be cached or is this an actually useful thing to do?

这其实是一个非常广泛和复杂的话题。编译器将做什么取决于特定的编译器、它的后端(目标架构)和编译选项。同样,对于 x86,这里有以下规则(取自 Intel® 64 and IA-32 Architectures Optimization Reference Manual):

Assembly/Compiler Coding Rule 3. (M impact, H generality) Arrange code to be consistent with the static branch prediction algorithm: make the fall-through code following a conditional branch be the likely target for a branch with a forward target, and make the fall-through code following a conditional branch be the unlikely target for a branch with a backward target.

据我所知,这是现代 x86 中静态分支预测唯一剩下的东西,likely/unlikely 属性可能仅用于 "overwrite" 这种默认行为。

既然你是 "crashing hard" 无论如何,我会选择

#include <cassert>

...
assert(a != nullptr);

这是独立于编译器的,应该给你接近最佳的性能,在调试器中 运行 时给你一个断点,不在调试器中时生成核心转储,并且可以通过设置禁用NDEBUG 预处理器符号,许多构建系统默认为发布构建执行此操作。

Should I use "if unlikely" for hard crashing errors?

对于这种情况,我更愿意将抛出的代码移至标记为 noreturn 的独立外部函数。这样,您的实际代码就不会 "polluted" 包含大量与异常相关的代码(或任何您的 "hard crashing" 代码)。与接受的答案相反,您不需要将其标记为 cold,但您确实需要 noreturn 让编译器不要尝试生成代码来保留寄存器或任何状态,并且基本上假设在没有回头路了。

例如,如果你这样写代码:

#include <stdexcept>

#define _STR(x) #x
#define STR(x) _STR(x)

void test(const char* a)
{
    if(a == nullptr)
        throw std::runtime_error("error at " __FILE__ ":" STR(__LINE__));
}

如果您在 test 函数中只有三个检查,编译器将生成 lots of instructions that deal with constructing and throwing this exception. You also introduce dependency on std::runtime_error. Check out how generated code will look like

第一个改进:将其移动到 a standalone function:

void my_runtime_error(const char* message);

#define _STR(x) #x
#define STR(x) _STR(x)

void test(const char* a)
{
    if (a == nullptr)
        my_runtime_error("error at " __FILE__ ":" STR(__LINE__));
}

这样您就可以避免在函数中生成所有与异常相关的代码。立即生成指令 become simpler and cleaner 并减少对执行检查的实际代码生成的指令的影响:

还有改进的余地。因为你知道你的 my_runtime_error 不会 return 你应该让编译器知道它,所以它 wouldn't need to preserve registers before calling my_runtime_error:

#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((__noreturn__))
#endif

void NORETURN my_runtime_error(const char* message);
...

当您在代码中 use it multiple times 时,您可以看到生成的代码要小得多,并且减少了对实际代码生成的指令的影响:

如您所见,这样编译器在调用您的 my_runtime_error.

之前不需要保留寄存器

我还建议不要将带有 __FILE____LINE__ 的错误字符串连接成单一的错误消息字符串。 Pass them as standalone parameters 并简单地制作一个宏来传递它们!

void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)

void test(const char* a)
{
    if (a == nullptr)
        MY_ERROR("error");
    if (a[0] == 'a')
        MY_ERROR("first letter is 'a'");
    if (a[0] == 'b')
        MY_ERROR("first letter is 'b'");
}

似乎每次 my_runtime_error 调用都会生成更多代码(在 x64 构建的情况下多了 2 条指令),但总大小实际上更小,因为常量字符串上保存的大小是大于额外的代码大小。

另请注意,这些代码示例有助于展示将 "hard crashing" 函数设为外部函数的好处。 noreturn 的需求在实际代码中变得更加明显,for example:

#include <math.h>

#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((noreturn))
#endif

void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)

double test(double x)
{
    int i = floor(x);
    if (i < 10)
        MY_ERROR("error!");
    return 1.0*sqrt(i);
}

生成的程序集:

尝试删除 NORETURN,或将 __attribute__((noreturn)) 更改为 __attribute__((cold)),您会看到 completely different generated assembly

作为最后一点(这是明显的 IMO,被省略了)。你需要定义你的 my_runtime_error 某些 cpp 文件中的函数。由于它只会是一个副本,您可以在此函数中放入任何您想要的代码。

void NORETURN my_runtime_error(const char* message, const char* file, int line)
{
    // you can log the message over network,
    // save it to a file and finally you can throw it an error:
    std::string msg = message;
    msg += " at ";
    msg += file;
    msg += ":";
    msg += std::to_string(line);
    throw std::runtime_error(msg);
}

还有一点:如果启用了 -Wmissing-noreturn 警告,clang 实际上认识到这种类型的函数将受益于 noreturnwarns about it

warning: function 'my_runtime_error' could be declared with attribute 'noreturn' [-Wmissing-noreturn] { ^