Fundamentals 13 min read

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.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Performance Evaluation and Implementation Overview of C++ Exceptions

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 iterations

When 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 iterations

From 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
}
#endif

The 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_catch

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

performancec++assemblyBenchmarkExceptionerrorCodelibc++
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.