想到了两种方法:
函数包装
就设计而言,最好的方法是将这个功能包装到一个封装属性和处理的函数中。为此,您传递一个您希望作为冷处理程序调用的回调(在本例中为 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(); }
};
}
这是作为可从函数隐式转换的类完成的,因此您可以编写像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,因此程序集更小,更易于比较)
额外问题:由于 lambda 被显式标记为冷,if 语句中的不太可能(...)是多余的吗?
阅读 GCC 的 __attribute__((cold)) 文档似乎表明通向冷函数的所有分支都标记为 unlikely,这应该使用 unlikely 宏是多余且不必要的——虽然拥有它应该不会有什么坏处。
来自the attributes page:
cold 属性用于通知编译器某个函数不太可能执行。该函数针对大小而不是速度进行了优化,并且在许多目标上,它被放置在文本部分的特殊小节中,因此所有冷函数看起来很接近,从而提高了程序非冷部分的代码局部性。 分支预测机制将导致代码中冷函数调用的路径标记为不太可能。因此,将用于处理不太可能的条件(例如 perror)的函数标记为冷以改进优化在极少数情况下调用标记函数的热函数。
强调我的。