Exception handling is an unavoidable part of writing code. Using the exception mechanism correctly requires a basic understanding of its performance and the implementation behind it. The purpose of this article is to provide a simple test of the C++ Exception mechanism and a brief analysis of its implementation to help C++ programmers make better use of Exception.

Many programming languages have Exception mechanisms. With the Exception mechanism, one piece of code can bypass the normal code execution path to notify another that some unexpected event or error condition has occurred. Another common exception/error handling mechanism is ErrorCode. Those familiar with THE C language should be familiar with it. For example, the operating system provides many interfaces to determine whether an exception occurs in the form of ErrorCode.

C++ does not force programmers to use Exception as Java does, but it is inevitable to handle Exception in C++, such as when the new operator throws STD ::bad_alloc when memory is low. There are also other issues with using ErrorCode alone to flag exceptions in C++ :

  1. There is no unified standard for ErrorCode. There is no strict standard on whether to return -1 for Error or 0 for Error, so you need to use enumeration in addition.

  2. ErrorCode may be ignored, and you may forget to add nodiscard even though C++17 has the [[nodiscard]] attribute! After all, forgetting to add nodiscard isn’t much harder than forgetting to handle ErrorCode.

Therefore, it is necessary to understand the principles of C++ Exception and how to use them correctly. At the same time, C++ is still the preferred programming preface in high performance programming scenarios. Many students dare not use C++ Exception for performance reasons. They only know that Exception is slow, but they don’t know exactly why and where.

The purpose of this article is to perform a simple test and analysis of C++ Exception. First, the performance of Exception is evaluated to explore the impact of C++ Exception on program performance. Then, the implementation mechanism of C++ Exception is briefly explored to let everyone understand the impact of Exception on program operation, and then write higher-quality code.

A Benchmark,

First, let’s take a look at the impact of adding exceptions on application performance through performance testing.

Investigating the Performance Overhead of C++ Exceptions (pspdfkit.com/blog/2020/p… We changed its test cases. To briefly explain our test code, we define a function that calls the target function based on probability:

const int randomRange = 2;
const int errorInt = 1;
int getRandom() { return random() % randomRange; }

template<typename T>
T testFunction(const std::function<T()>& fn) {
    auto num = getRandom();
    for (int i{0}; i < 5; ++i) {
        if (num == errorInt) {
            returnfn(); }}}Copy the code

When testFunction is executed, the target function fn is called 50% of the time.

void exitWithStdException() {
    testFunction<void> ([] () - >void {
        throw std::runtime_error("Exception!");
    });
}

void BM_exitWithStdException(benchmark::State& state) {
    for (auto _ : state) {
        try {
            exitWithStdException();
        } catch (conststd::runtime_error &ex) { BLACKHOLE(ex); }}}Copy the code

BM_exitWithStdException is used to test the function exitWithStdException, which throws an Exception and is immediately caught in BM_exitWithStdException, after which we do nothing.

Similarly, the code we designed to test the ErrorCode schema is as follows:

void BM_exitWithErrorCode(benchmark::State& state) {
    for (auto _ : state) {
        auto err = exitWithErrorCode();
        if (err < 0) {
            // handle_error()
            BLACKHOLE(err);
        }
    }
}

int exitWithErrorCode() {
    testFunction<int>([]() -> int {
        return -1;
    });

    return 0;
}
Copy the code

Put the ErrorCode test code into the try{… }catch{… } Whether the test only enters the try will affect performance:

void BM_exitWithErrorCodeWithinTry(benchmark::State& state) {
    for (auto _ : state) {
        try {
            auto err = exitWithErrorCode();
            if (err < 0) { BLACKHOLE(err); }}catch(...). {}}}Copy the code

Start our test with gtest/banchmark:

BENCHMARK(BM_exitWithStdException);
BENCHMARK(BM_exitWithErrorCode);
BENCHMARK(BM_exitWithErrorCodeWithinTry);

BENCHMARK_MAIN();
Copy the code

Test results:

2021-07-08 20:59:44
Running ./benchmarkTests/benchmarkTests
Run on (12 X 2600 MHz CPU s)
CPU Caches:
  L1 Data 32K (x6)
  L1 Instruction 32K (x6)
  L2 Unified 262K (x6)
  L3 Unified 12582K (x1)
Load Average: 2.06.1.88.1.94
***WARNING*** Library was built as DEBUG. Timings may be affected.
------------------------------------------------------------------------
Benchmark                              Time             CPU   Iterations
------------------------------------------------------------------------
BM_exitWithStdException             1449 ns         1447 ns       470424
BM_exitWithErrorCode                 126 ns          126 ns      5536967
BM_exitWithErrorCodeWithinTry        126 ns          126 ns      5589001
Copy the code

This is what I tested on my MAC, using GCC Version 10.2.0 with an exception model DWARF2. As you can see, when the Error/Exception rate is 50%, Exception is more than 10 times slower than returning ErrorCode. Also, add try{… to a piece of code that does not throw an exception. }catch{… } has no impact on performance. We can adjust the incidence of Error/Exception to lower test:

const int errorInt = 1;
int getRandom() { return random() % randomRange; }
Copy the code

We reduced the probability of an anomaly to 1% and continued testing:

Running ./benchmarkTests/benchmarkTests
Run on (12 X 2600 MHz CPU s)
CPU Caches:
  L1 Data 32K (x6)
  L1 Instruction 32K (x6)
  L2 Unified 262K (x6)
  L3 Unified 12582K (x1)
Load Average: 2.80, 2.22, 1.93
***WARNING*** Library was built as DEBUG. Timings may be affected.
------------------------------------------------------------------------
Benchmark                              Time             CPU   Iterations
------------------------------------------------------------------------
BM_exitWithStdException              140 ns          140 ns      4717998
BM_exitWithErrorCode                 111 ns          111 ns      6209692
BM_exitWithErrorCodeWithinTry        113 ns          113 ns      6230807
Copy the code

As you can see, the Performance of the Exception mode is significantly improved, approaching that of the ErrorCode mode.

From the experimental results, we can draw the following conclusions:

  • In cases where throws occur very frequently (50%), the Exception mechanism is much slower than ErrorCode;

  • In cases where throws do not happen very often (1%), the Exception mechanism is not slower than ErrorCode.

From this conclusion, the following suggestions can be obtained:

Do not use try{throw… }catch(){… } to act as your code control flow, which can cause your C++ to be ridiculously slow. Exceptions should be used for real exceptions, such as memory overruns, data formatting errors, and other more serious but infrequent scenarios.

Libc++ Exception implements shallow exploration

In the previous section, we verified that C++ exceptions can slow down application performance in the case of frequent exceptions. This section begins by trying to find out what causes this phenomenon.

First, the Exception mechanism is implemented in the C++ standard library, and since there is no Exception mechanism in C, we can try to link a relocatable binary compiled from.cpp with the throw keyword to a binary compiled from.c containing the main function. The goal is to find out what additional functions libc++ added to the executable we eventually generated for the throw keyword.

throw.h:

/// throw.h
struct Exception {};

#ifdef __cplusplus
extern "C" {
#endif

    void raiseException();

#ifdef __cplusplus
};
#endif
Copy the code

Throw. CPP:

#include "throw.h" extern "C" { void raiseException() { throw Exception(); }}Copy the code

The raiseException function simply throws an exception. Extern “C” tells the C++ compiler to generate temporary function names according to C rules so that the resulting relocatable object file can be linked by the subsequent C main function. The main. C is as follows:

/// main.c
#include "throw.h"

int main() {
    raiseException();
    return 0;
}
Copy the code

We compile throw.cpp and main.c respectively:

> g++ -c -o throw.o -O0 -ggdb throw.cpp
> gcc -c -o main.o -O0 -ggdb main.c
Copy the code

Intuitively, we should be able to complete the link, after all, the function raiseException is completely defined. Try it out:

> gcc main.o throw.o -o app

Undefined symbols for architecture x86_64:
  "__ZTVN10__cxxabiv117__class_type_infoE", referenced from:
      __ZTI9Exception in throw.o
  NOTE: a missing vtable usually means the first non-inline virtual member function has no definition."___cxa_allocate_exception", referenced from:
      _raiseException in throw.o
  "___cxa_throw", referenced from:
      _raiseException in throw.o
ld: symbol(s) not found for architecture x86_64
collect2: error: ld returned 1 exit status
Copy the code

The link failed, and the error message looks like it’s understood — it should be related to Exception, but obviously we don’t fully understand — what are these three undefined symbols?

These three symbols:

_ _ZTVN10 _ cxxabiv117 class_type_infoE, _cxa_allocate_exception, _ _cxa_throw,

Both represent entry functions for the corresponding Exception handling mechanism in libc++. Is the part that the compiler adds at compile time. When linking, the compiler will look for the full definition of the three symbols in libc++.

When we link, we use GCC instruction and only link liBC. There is no definition of these three symbols in C language, so we will report an error when we link. Using g++ links does work:

> g++ main.o throw.o -o app                   
> ./app
terminate called after throwing an instance of 'Exception'
[1]    37016 abort      ./app
Copy the code

As we can see from this demo, g++ does do some extra work during compilation and linking to help us implement the throw keyword. For the try {… } catch () {… } for the same, link will link to libc++ in the corresponding function implementation, we through assembly code to experience again:

void raise() {
    throw Exception();
}

void try_but_dont_catch() {
    try {
        raise();
    } catch(Fake_Exception&) {
        printf("Running try_but_dont_catch::catch(Fake_Exception)\n");
    }

    printf("try_but_dont_catch handled an exception and resumed execution");
}
Copy the code

Corresponding compilation (after simplification) :

    call    __cxa_allocate_exception
    call    __cxa_throw

_Z18try_but_dont_catchv:
    .cfi_startproc
    .cfi_personality 0,__gxx_personality_v0
    .cfi_lsda 0,.LLSDA1
    ...
    call    _Z5raisev
    jmp     .L8
Copy the code

_ _cxa_allocate_exception allocates space for Exception, and _ _cxa_throw is implemented in libc++. This function is the entry point to the subsequent Exception handling mechanism. Ignore the first three lines of z18try_but_dont_catchv and look directly at Call _Z5raisev. If _Z5raisev executes properly, the.l8 program will exit normally.

    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
Copy the code

If the execution of a try fails, this code will be executed:

    cmpl    $1, %edx
    je      .L5

.LEHB1:
    call    _Unwind_Resume
.LEHE1:

.L5:
    call    __cxa_begin_catch
    call    __cxa_end_catch
Copy the code

L5 = _Unwind_Resume = _Unwind_Resume = _Unwind_Resume = _Unwind_Resume

Obviously, parts of.l5 correspond to the catch keyword in the code, and.l5 also jumps to.l8 after execution, which should exit normally.

Unwind_Resume should be a function in libc++ again. This function is a handler that goes to other stack frames to find exceptions of this type.

Here, in fact, we can understand the cause of the previous test results:

1. If no exception is thrown after a try, the program execution process will not execute _ _cax_throw.

2._ _CAx_throw is the entry for subsequent exception judgment and stack rollback. If this function is not executed, performance will not be affected.

3._ _CAx_throw will search for exception handlers from stack frame to stack frame, which will cause serious performance loss.

Third, summary

In this paper, the performance of C++ Exception is simply done a test, from the test results we have a reasonable conjecture: C++ Exception behind the processing process is implemented by the corresponding function in libc++, and the conjecture is verified.

In fact, the full implementation of C++ Exception has many in-depth details that interested students can explore further.

Author’s brief introduction

He Zhiqiang

Tencent Database RESEARCH and development junior engineer, graduated from the High Energy Efficiency Computing Laboratory of University of Science and Technology of China with a master’s degree, research direction is embedded system. Tencent database team focuses on continuously optimizing database kernel and architecture capabilities to improve database performance stability and performance.