在 C++ 中样板化“cold/never_inline”错误处理技术的最佳方法是什么?

Chu*_*huu 4 c++ macros gcc

文章中,一种技术被描述为在移动的gcc错误代码外的线,以帮助优化尺寸为热路径尽可能。这方面的一个例子是:

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

bool testForTerriblyUnlikelyEdgeCase() {
  //test for error condition here
}

void example() {
  if (unlikely(testForTerriblyUnlikelyEdgeCase())) {
    [&]() __attribute__((noinline,cold)) {
       //error handling code here
    }();
  }
}
Run Code Online (Sandbox Code Playgroud)

这是一项很棒的技术,但需要大量的样板文件。包装它以尽可能减少样板的最佳方法是什么?理想情况下 C++14 兼容允许特定于 gcc 的功能。

额外问题: if 语句中的不太可能(...) 是否多余,因为 lambda 被显式标记为冷?

Hum*_*ler 9

想到了两种方法:

  • 一种函数包装方法,以及
  • 基于宏观的方法

函数包装器

最好的设计是将此功能包装到封装属性和处理的函数中。为此,您需要传递一个要作为冷处理程序调用的回调(在本例中为 lambda)。它看起来很简单(使用 C++11 属性而不是__attribute__语法):

template <typename Fn>
[[gnu::cold]] [[gnu::noinline]]
void cold_path(Fn&& fn)
{
    std::forward<Fn>(fn)();
}
Run Code Online (Sandbox Code Playgroud)

您还可以扩展此解决方案以利用要测试的条件,例如:

template <typename Expr, typename Fn>
void cold_path_if(Expr&& expr, Fn&& fn)
{
    if (unlikely(std::forward<Expr>(expr))) {
        cold_path(std::forward<Fn>(fn));
    }
}
Run Code Online (Sandbox Code Playgroud)

把它们放在一起,你有:

void example() {
  cold_path_if(testForTerriblyUnlikelyEdgeCase(), [&]{
    std::cerr << "Oh no, something went wrong" << std::endl;
    std::abort();
  });
}
Run Code Online (Sandbox Code Playgroud)

这是它在Compiler Explorer上的样子。

基于宏观的方法

如果不希望传递显式 lambda,那么想到的唯一替代方案是基于宏的解决方案,为您创建 lambda。为此,您需要一个可以立即调用 lambda 的实用程序,以便您只需定义函数的主体:

// A type implicitly convertible to any function type, used to make the 
// macro below not require '()' to invoke the lambda
namespace detail {
class invoker
{
public:
    template <typename Fn>
    /* IMPLICIT */ invoker(Fn&& fn){ fn(); }
};
}
Run Code Online (Sandbox Code Playgroud)

这是作为一个可从函数隐式转换的类完成的,因此您可以编写类似detail::invoker foo = []{ ... }. 然后我们想把定义的第一部分带到捕获,并将它包装成一个宏。

为此,我们需要一个唯一的变量名称,否则如果多个处理程序在同一范围内,我们可能会隐藏或重新定义变量。为了解决这个问题,我将__COUNTER__宏附加到一个名称;但这是非标准的:

#define COLD_HANDLER ::detail::invoker some_unique_name ## __COUNTER__ = [&]() __attribute__((noinline,cold))
Run Code Online (Sandbox Code Playgroud)

这只是将自动调用程序的创建包装起来,直到定义 lambda 为止,因此您需要做的就是编写 COLD_HANDLER { ... }

使用现在看起来像:

void example() {
  if (unlikely(testForTerriblyUnlikelyEdgeCase())) {
    COLD_HANDLER {
       //error handling code here
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

这是编译器资源管理器上的示例


两种方法都生成直接使用 lambda相同的程序集,只是标签和名称不同。(注:这个比较使用的是std::fprintf代替stds::cerr所以组装更小更容易比较)


额外问题: if 语句中的不太可能(...) 是否多余,因为 lambda 被显式标记为冷?

阅读 GCC 的文档__attribute__((cold))似乎表明所有导致冷函数的分支都被标记为unlikely,这应该使unlikely宏的使用变得多余和不必要——尽管拥有它应该没有什么坏处。

属性页面

Cold 属性用于通知编译器某个函数不太可能被执行。该函数针对大小而不是速度进行了优化,并且在许多目标上,它被放置在文本部分的特殊子部分中,因此所有冷函数看起来都靠得很近,从而提高了程序非冷部分的代码局部性。分支预测机制将导致代码中调用冷函数的路径标记为不太可能。因此,将用于处理不太可能的条件(例如 perror)的函数标记为冷函数是有用的,以改进在极少数情况下调用标记函数的热函数的优化。

强调我的。