Performance Evaluation and Implementation Overview of C++ Exceptions
Benchmarking C++ exceptions against error‑code handling reveals that frequent throws incur more than tenfold slowdown due to libc++ runtime functions like __cxa_allocate_exception and stack unwinding, while rare exceptions and empty try blocks add negligible overhead, highlighting the trade‑off between safety and performance.
Exception handling is an unavoidable part of programming. To use C++ exceptions effectively, developers need a basic understanding of their performance impact and underlying implementation. This article presents a simple benchmark of the C++ exception mechanism and a brief analysis of its implementation in libc++.
Unlike Java, C++ does not force the use of exceptions, but they are inevitable in scenarios such as out‑of‑memory (e.g., std::bad_alloc ). An alternative error‑handling style is the use of error codes, which suffers from lack of a unified standard and the risk of being ignored.
Benchmark
The benchmark follows the methodology of "Investigating the Performance Overhead of C++ Exceptions". A helper function testFunction decides with a given probability whether to invoke a target function. The following code snippets illustrate the test setup:
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) {
return fn();
}
}
}Two test functions are defined:
void exitWithStdException() {
testFunction<void>([]() -> void {
throw std::runtime_error("Exception!");
});
}
void BM_exitWithStdException(benchmark::State& state) {
for (auto _ : state) {
try {
exitWithStdException();
} catch (const std::runtime_error &ex) {
BLACKHOLE(ex);
}
}
}Analogous code is provided for the ErrorCode style and for wrapping the ErrorCode call inside a try{...}catch{...} block. The benchmarks are registered with:
BENCHMARK(BM_exitWithStdException);
BENCHMARK(BM_exitWithErrorCode);
BENCHMARK(BM_exitWithErrorCodeWithinTry);
BENCHMARK_MAIN();Results with a 50 % exception probability (gcc 10.2.0, DWARF2) show that the exception version is more than ten times slower than the error‑code version:
BM_exitWithStdException 1449 ns 470424 iterations
BM_exitWithErrorCode 126 ns 5536967 iterations
BM_exitWithErrorCodeWithinTry 126 ns 5589001 iterationsWhen the exception probability is reduced to 1 %, the performance gap narrows dramatically:
BM_exitWithStdException 140 ns 4717998 iterations
BM_exitWithErrorCode 111 ns 6209692 iterations
BM_exitWithErrorCodeWithinTry 113 ns 6230807 iterationsFrom these measurements the following conclusions are drawn:
When exceptions are thrown frequently (e.g., 50 % of the time), the exception mechanism is considerably slower than returning error codes.
When exceptions are rare (e.g., 1 % of the time), the overhead becomes comparable to error‑code handling.
Placing a try{...}catch{...} block around code that never throws adds virtually no cost.
Shallow Exploration of libc++ Exception Implementation
A small demo is used to reveal which symbols libc++ adds for exception handling. The header throw.h declares a dummy Exception type and an extern "C" function raiseException :
/// throw.h
struct Exception {};
#ifdef __cplusplus
extern "C" {
#endif
void raiseException();
#ifdef __cplusplus
}
#endifThe implementation simply throws the exception:
/// throw.cpp
#include "throw.h"
extern "C" {
void raiseException() {
throw Exception();
}
}A C main.c calls this function:
/// main.c
#include "throw.h"
int main() {
raiseException();
return 0;
}Compiling with g++ -c -O0 -ggdb throw.cpp and gcc -c -O0 -ggdb main.c and linking with gcc main.o throw.o -o app fails because the C linker cannot resolve the C++ runtime symbols ( __ZTVN10__cxxabiv117__class_type_infoE , ___cxa_allocate_exception , ___cxa_throw ). Linking with g++ succeeds, demonstrating that the C++ compiler/linker adds the necessary support functions.
Inspecting the generated assembly reveals the core runtime calls:
_Z5raisev:
call __cxa_allocate_exception
call __cxa_throw
_Z18try_but_dont_catchv:
.cfi_startproc
.cfi_personality 0,__gxx_personality_v0
...
call _Z5raisev
jmp .L8
cmpl $1, %edx
je .L5
call _Unwind_Resume
.L5:
call __cxa_begin_catch
call __cxa_end_catchThe symbols __cxa_allocate_exception and __cxa_throw allocate the exception object and start the unwinding process. The personality function __gxx_personality_v0 and _Unwind_Resume walk the stack to find a matching handler. If no exception is thrown, these functions are never invoked, which explains why a try block without a throw incurs negligible overhead.
Conclusion
The article demonstrates that C++ exceptions can be significantly slower when they are thrown frequently, but their overhead becomes negligible when exceptions are rare. It also shows that the performance cost originates from the libc++ runtime functions involved in stack unwinding and exception object allocation.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.