16

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

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__);

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

4

4 回答 4

22

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

如果您更喜欢使用unlikely()语义表示法(以使代码更易于阅读),那也可以,但它本身并不是最佳的。

于 2017-06-26T09:45:49.867 回答
9

我应该使用“如果不太可能”来解决硬崩溃错误吗?

对于这样的情况,我更喜欢将抛出的代码移动到标记为noreturn. 这样,您的实际代码不会被大量与异常相关的代码(或任何您的“硬崩溃”代码)“污染”。与接受的答案相反,您不需要将其标记为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__));
}

编译器将生成大量指令来处理构造和抛出此异常。您还引入了对std::runtime_error. 如果您的函数中只有三个类似的检查,请查看生成的代码的外观test

在此处输入图像描述

第一个改进:将其移至独立功能

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__));
}

这样您就可以避免在函数中生成所有与异常相关的代码。立即生成的指令变得更简单、更清晰,并减少对执行检查的实际代码生成的指令的影响: 在此处输入图像描述

仍有改进的余地。既然你知道你my_runtime_error不会返回,你应该让编译器知道它,这样它就不需要在调用之前保留寄存器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);
...

当您在代码中多次使用它时,您会看到生成的代码要小得多,并减少对实际代码生成的指令的影响:

在此处输入图像描述

如您所见,这种方式编译器在调用您的my_runtime_error.

我还建议不要将错误字符串与__FILE__整体__LINE__错误消息字符串连接起来。将它们作为独立参数传递,并简单地创建一个宏来传递它们!

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 条指令),但总大小实际上更小,因为常量字符串上保存的大小远大于额外代码大小。

另外,请注意,这些代码示例有助于展示将“硬崩溃”函数设为外部函数的好处。需要noreturn在实际代码中变得更加明显,例如

#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)),您将看到完全不同的生成程序集在此处输入图像描述

作为最后一点(这很明显是 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);
}

还有一点:clang 实际上认识到,如果启用了警告,这种类型的功能会从中受益noreturn发出-Wmissing-noreturn警告:

警告:函数 'my_runtime_error' 可以用属性 'noreturn' [-Wmissing-noreturn] { ^

于 2017-06-26T19:02:53.757 回答
5

这取决于。

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

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

更新:

编译器会自动推断代码的哪一部分应该被缓存,或者这是一个真正有用的事情吗?

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

汇编/编译器编码规则 3.(M 影响,H 通用性)安排代码与静态分支预测算法一致:使条件分支之后的贯穿代码成为具有前向目标的分支的可能目标,并使条件分支之后的贯穿代码不太可能成为具有后向目标的分支的目标。

据我所知,这是现代 x86 中静态分支预测所剩下的唯一内容,并且可能/不太可能的属性可能仅用于“覆盖”此默认行为。

于 2017-06-26T09:51:19.680 回答
3

既然你无论如何都“努力崩溃”,我会选择

#include <cassert>

...
assert(a != nullptr);

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

于 2017-06-26T15:02:57.130 回答