在 C++ 中对 "cold/never_inline" 错误处理技术进行样板化的最佳方式是什么?

What is the best way to boilerplate the "cold/never_inline" error handling technique in C++?

this 文章中,描述了一种在 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
    }();
  }
}

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

附加问题:由于 lambda 被显式标记为冷,if 语句中的 unlikely(...) 是否多余?

想到了两种方法:

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

函数包装器

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

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

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

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

综合起来,你有:

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

这是它在 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(); }
};
}

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

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

#define COLD_HANDLER ::detail::invoker some_unique_name ## __COUNTER__ = [&]() __attribute__((noinline,cold))

这只是简单地包装了自动调用程序的创建,直到定义了 lambda,所以您需要做的就是编写 COLD_HANDLER { ... }

使用现在看起来像:

void example() {
  if (unlikely(testForTerriblyUnlikelyEdgeCase())) {
    COLD_HANDLER {
       //error handling code here
    };
  }
}

这是 compiler explorer

上的示例

这两种方法都会导致 identical assembly 直接使用 lambda,只是标签和名称不同。 (注意:这个比较使用std::fprintf而不是stds::cerr所以程序集更小更容易比较)


Bonus Question: Is the unlikely(...) in the if statement redundant since the lambda is explicitly marked cold?

阅读 GCC 的 __attribute__((cold)) 文档似乎表明导致冷函数的所有分支都标记为 unlikely 应该 使用unlikely 宏是多余的和不必要的 -- 虽然拥有它应该不会有什么坏处。

来自 the attributes page:

The cold attribute is used to inform the compiler that a function is unlikely executed. The function is optimized for size rather than speed and on many targets it is placed into special subsection of the text section so all cold functions appears close together improving code locality of non-cold parts of program. The paths leading to call of cold functions within code are marked as unlikely by the branch prediction mechanism. It is thus useful to mark functions used to handle unlikely conditions, such as perror, as cold to improve optimization of hot functions that do call marked functions in rare occasions.

强调我的。