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++ :
-
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.
-
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.