Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

gcc

macros

In this article, a technique is described to move error code out-of-line in gcc to help optimise the hot path for size as much as possible. An example of this would be:

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

This is great technique, but requires an absolute ton of boilerplate. What's the best way to wrap this to reduce the boilerplate as much as possible? Ideally C++14 compatible allowing gcc-specific functionality.

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

like image 966
Chuu Avatar asked Dec 30 '22 23:12

Chuu


1 Answers

There are two approaches that come to mind:

  • A function-wrapper approach, and
  • A macro-based approach

Function Wrapper

The nicest in terms of design would be to wrap this functionality into a function that encapsulates the attributes and handling. To do this, you pass a callback that you want invoked as the cold handler (in this case, a lambda). It can look as simple as this (using C++11-attributes instead of __attribute__ syntax):

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

You can also extend this solution to make use of the condition to test for, such as:

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

putting it all together, you have:

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

Here's how it looks on Compiler Explorer.

Macro-based approach

If passing an explicit lambda is not desired, then the only alternative that comes to mind is a macro-based solution that creates a lambda for you. To do this, you will need a utility that will invoke the lambda immediately, so that all you need is to define the function's body:

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

This is done as a class that is implicitly convertible from the function, so that you can write code like detail::invoker foo = []{ ... }. Then we want to take the first part of the definition up to the capture, and wrap this into a macro.

To do this, we will want a unique name for the variable, otherwise we may shadow or redefine variables if more than one handler is in the same scope. To work around this, I append the __COUNTER__ macro to a name; but this is nonstandard:

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

This simply wraps the creation of the auto invoker up until the point that the lambda is defined, so all you need to do is write COLD_HANDLER { ... }

The use would now look like:

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

Here's an example on compiler explorer


Both approaches produce identical assembly to just using the lambda directly, with only the labels and names being different. (Note: This comparison uses std::fprintf instead of stds::cerr so the assembly is smaller and easier to compare)


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

Reading GCC's documentation for __attribute__((cold)) seems to indicate that all branches leading to the cold function are marked unlikely, which should make the use of the unlikely macro redundant and unnecessary -- though it shouldn't hurt to have it.

From 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.

Emphasis mine.

like image 153
Human-Compiler Avatar answered Jan 14 '23 14:01

Human-Compiler