【问题标题】:What is the best way to boilerplate the "cold/never_inline" error handling technique in C++?在 C++ 中样板化“cold/never_inline”错误处理技术的最佳方法是什么?
【发布时间】:2020-07-06 18:12:04
【问题描述】:

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 语句中的不太可能(...)是多余的吗?

【问题讨论】:

标签: c++ gcc macros


【解决方案1】:

想到了两种方法:

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

函数包装

就设计而言,最好的方法是将这个功能包装到一个封装属性和处理的函数中。为此,您传递一个您希望作为冷处理程序调用的回调(在本例中为 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)的函数标记为冷以改进优化在极少数情况下调用标记函数的热函数。

强调我的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-11-13
    • 2018-04-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-12
    • 2017-12-26
    • 2016-03-27
    相关资源
    最近更新 更多