Skip to content

Commit adee091

Browse files
committed
Add zero-overhead variants, CPPTRACE_CATCH_ALT, and unprefixed aliases
1 parent 73ca2b5 commit adee091

File tree

7 files changed

+305
-25
lines changed

7 files changed

+305
-25
lines changed

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ if(NOT CPPTRACE_STD_FORMAT)
317317
target_compile_definitions(${target_name} PUBLIC CPPTRACE_NO_STD_FORMAT)
318318
endif()
319319

320+
if(CPPTRACE_UNPREFIXED_TRY_CATCH)
321+
target_compile_definitions(${target_name} PUBLIC CPPTRACE_UNPREFIXED_TRY_CATCH)
322+
endif()
323+
320324
if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
321325
SET(CMAKE_C_ARCHIVE_FINISH "<CMAKE_RANLIB> -no_warning_for_no_symbols -c <TARGET>")
322326
SET(CMAKE_CXX_ARCHIVE_FINISH "<CMAKE_RANLIB> -no_warning_for_no_symbols -c <TARGET>")

README.md

Lines changed: 104 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Cpptrace also has a C API, docs [here](docs/c-api.md).
2929
- [Utilities](#utilities)
3030
- [Configuration](#configuration)
3131
- [Traces From All Exceptions](#traces-from-all-exceptions)
32+
- [Removing the `CPPTRACE_` prefix](#removing-the-cpptrace_-prefix)
3233
- [How it works](#how-it-works)
3334
- [Performance](#performance)
3435
- [Traced Exception Objects](#traced-exception-objects)
@@ -371,14 +372,14 @@ namespace cpptrace {
371372
### Traces From All Exceptions
372373

373374
Cpptrace provides `CPPTRACE_TRY` and `CPPTRACE_CATCH` macros that allow a stack trace to be collected from the current
374-
thrown exception object, with no overhead in the non-throwing path:
375+
thrown exception object, with minimal or no overhead in the non-throwing path:
375376

376377
```cpp
377378
CPPTRACE_TRY {
378379
foo();
379380
} CPPTRACE_CATCH(const std::exception& e) {
380-
std::cout<<"Exception: "<<e.what()<<std::endl;
381-
std::cout<<cpptrace::from_current_exception().to_string(true)<<std::endl;
381+
std::cerr<<"Exception: "<<e.what()<<std::endl;
382+
cpptrace::from_current_exception().print();
382383
}
383384
```
384385

@@ -396,36 +397,121 @@ API functions:
396397
- `cpptrace::from_current_exception`: Returns a resolved `const stacktrace&` from the current exception. Invalidates
397398
references to traces returned by `cpptrace::raw_trace_from_current_exception`.
398399

400+
There is a performance tradeoff with this functionality: Either the try-block can be zero overhead in the
401+
non-throwing path with potential expense in the throwing path, or the try-block can have very minimal overhead
402+
due to bookkeeping with guarantees about the expense of the throwing path. More details on this tradeoff
403+
[below](#performance). Cpptrace provides macros for both sides of this tradeoff:
404+
- `CPPTRACE_TRY`/`CPPTRACE_CATCH`: Minimal overhead in the non-throwing path (one `mov` on x86, and this may be
405+
optimized out if the compiler is able)
406+
- `CPPTRACE_TRYZ`/`CPPTRACE_CATCHZ`: Zero overhead in the throwing path, potential extra cost in the throwing path
407+
408+
Note: It's important to not mix the `Z` variants with the non-`Z` variants.
409+
410+
Unfortunately the try/catch macros are needed to insert some magic to perform a trace during the unwinding search phase.
411+
In order to have multiple catch alternatives, either `CPPTRACE_CATCH_ALT` or a normal `catch` must be used:
412+
```cpp
413+
CPPTRACE_TRY {
414+
foo();
415+
} CPPTRACE_CATCH(const std::exception&) { // <- First catch must be CPPTRACE_CATCH
416+
// ...
417+
} CPPTRACE_CATCH_ALT(const std::exception&) { // <- Ok
418+
// ...
419+
} catch(const std::exception&) { // <- Also Ok
420+
// ...
421+
} CPPTRACE_CATCH(const std::exception&) { // <- Not Ok
422+
// ...
423+
}
424+
```
425+
426+
Note: The current exception is the exception most recently seen by a `CPPTRACE_CATCH` macro.
427+
428+
```cpp
429+
CPPTRACE_TRY {
430+
throw std::runtime_error("foo");
431+
} CPPTRACE_CATCH(const std::exception& e) {
432+
cpptrace::from_current_exception().print(); // the trace for std::runtime_error("foo")
433+
CPPTRACE_TRY {
434+
throw std::runtime_error("bar");
435+
} CPPTRACE_CATCH(const std::exception& e) {
436+
cpptrace::from_current_exception().print(); // the trace for std::runtime_error("bar")
437+
}
438+
cpptrace::from_current_exception().print(); // the trace for std::runtime_error("bar"), again
439+
}
440+
```
441+
442+
#### Removing the `CPPTRACE_` prefix
443+
444+
`CPPTRACE_TRY` is a little cumbersome to type. To remove the `CPPTRACE_` prefix you can use the
445+
`CPPTRACE_UNPREFIXED_TRY_CATCH` cmake variable or define `CPPTRACE_UNPREFIXED_TRY_CATCH` for the preprocessor:
446+
447+
```cpp
448+
TRY {
449+
foo();
450+
} CATCH(const std::exception& e) {
451+
std::cerr<<"Exception: "<<e.what()<<std::endl;
452+
cpptrace::from_current_exception().print();
453+
}
454+
```
455+
456+
This is not done by default for macro safety/hygiene reasons. If you do not want `TRY`/`CATCH` macros defined, as they
457+
are common macro names, you can easily modify the following snippet to provide your own aliases:
458+
459+
```cpp
460+
#define TRY CPPTRACE_TRY
461+
#define CATCH(param) CPPTRACE_CATCH(param)
462+
#define TRYZ CPPTRACE_TRYZ
463+
#define CATCHZ(param) CPPTRACE_CATCHZ(param)
464+
#define CATCH_ALT(param) CPPTRACE_CATCH_ALT(param)
465+
```
466+
399467
#### How it works
400468
401469
C++ does not provide any language support for collecting stack traces when exceptions are thrown, however, exception
402470
handling under both the Itanium ABI and by SEH (used to implement C++ exceptions on windows) involves unwinding the
403-
stack twice, the first unwind searches for an appropriate `catch` handler, the second actually unwinds the stack and
471+
stack twice. The first unwind searches for an appropriate `catch` handler, the second actually unwinds the stack and
404472
calls destructors. Since the stack remains intact during the search phase it's possible to collect a stack trace with
405-
zero overhead when the `catch` is considered for matching the exception.
473+
zero overhead when the `catch` is considered for matching the exception. The try/catch macros for cpptrace set up a
474+
special try/catch setup that can collect a stack trace when considered during a search phase.
406475
407476
N.b.: This mechanism is also discussed in [P2490R3][P2490R3].
408477
409478
#### Performance
410479
411-
`CPPTRACE_CATCH` internally generates lightweight raw traces when considered in the search phase. These are quite fast
412-
to generate and are only resolved when `cpptrace::from_current_exception` is called.
413-
414-
Currently `CPPTRACE_CATCH` always generates a raw trace when considered as a candidate. That means that if there is a
415-
nesting of handlers, either directly in code or as a result of the current call stack, the current stack may be traced
416-
mutliple times until the appropriate handler is found.
417-
418-
This should not matter for the vast majority applications given that performance very rarely is critical in throwing
419-
paths, how exception handling is usually used, and the shallowness of most call stacks. However, it's something to be
420-
aware of.
480+
The fundamental mechanism for this functionality is generating a trace when a catch block is considered during an
481+
exception handler search phase. Internally a lightweight raw trace is generated upon consideration, which is quite
482+
fast. This raw trace is only resolved when `cpptrace::from_current_exception` is called, or when the user manually
483+
resolves a trace from `cpptrace::raw_trace_from_current_exception`.
484+
485+
It's tricky, however, from the library's standpoint to check if the catch will end up matching. The library could simply
486+
generate a trace every time a `CPPTRACE_CATCH` is considered, however, in a deep nesting of catch's, e.g. as a result of
487+
recusion, where a matching handler is not found quickly this could cause notable overhead due to tracing the stack
488+
multiple times. Thus, there is a performance tradeoff between a little book keeping to prevent duplicate tracing or
489+
biting the bullet, so to speak, in the throwing path and unwinding multiple times.
490+
491+
> [!TIP]
492+
> The choice between the `Z` and non-`Z` (zero-overhead and non-zero-overhead) variants of the exception handlers should
493+
> not matter 99% of the time, however, both are provided in the rare case that it does.
494+
>
495+
> `CPPTRACE_TRY`/`CPPTRACE_CATCH` could only hurt performance if used in a hot loop where the compiler can't optimize
496+
> away the internal bookkeeping, otherwise the bookkeeping should be completely negligible.
497+
>
498+
> `CPPTRACE_TRYZ`/`CPPTRACE_CATCHZ` could only hurt performance when there is an exceptionally deep nesting of exception
499+
> handlers in a call stack before a matching handler.
500+
501+
More information on performance considerations with the zero-overhead variant:
502+
503+
Tracing the stack multiple times in throwing paths should not matter for the vast majority applications given that:
504+
1. Performance very rarely is critical in throwing paths and exceptions should be exceptionally rare
505+
2. Exception handling is not usually used in such a way that you could have a deep nesting of handlers before finding a
506+
matching handler
507+
3. An that most call stacks are fairly shallow
421508
422509
To put the scale of this performance consideration into perspective: In my benchmarking I have found generation of raw
423510
traces to take on the order of `100ns` per frame. Thus, even if there were 100 non-matching handlers before a matching
424511
handler in a 100-deep call stack the total time would stil be on the order of one millisecond.
425512
426-
It's possible to avoid this by adding some bookkeeping to the `CPPTRACE_TRY` block. With the tradeoff between
427-
zero-overhead try-catch in the happy path and a little extra overhead in the unhappy throwing path I decided to keep
428-
try-catch zero-overhead. Should this be a concern to anyone, I'm happy to facilitate both solutions.
513+
Nonetheless, I chose a default bookkeeping behavior for `CPPTRACE_TRY`/`CPPTRACE_CATCH` since it is safer with better
514+
performance guarantees for the most general possible set of users.
429515
430516
### Traced Exception Objects
431517

cmake/OptionVariables.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ option(CPPTRACE_WERROR_BUILD "" OFF)
174174
option(CPPTRACE_POSITION_INDEPENDENT_CODE "" ON)
175175
option(CPPTRACE_SKIP_UNIT "" OFF)
176176
option(CPPTRACE_STD_FORMAT "" ON)
177+
option(CPPTRACE_UNPREFIXED_TRY_CATCH "" OFF)
177178
option(CPPTRACE_USE_EXTERNAL_GTEST "" OFF)
178179
set(CPPTRACE_ZSTD_REPO "https://github.com/facebook/zstd.git" CACHE STRING "")
179180
set(CPPTRACE_ZSTD_TAG "794ea1b0afca0f020f4e57b6732332231fb23c70" CACHE STRING "") # v1.5.6

include/cpptrace/from_current.hpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,19 @@ namespace cpptrace {
4141
exception_unwind_interceptor(1);
4242
return 0; // EXCEPTION_CONTINUE_SEARCH
4343
}
44+
CPPTRACE_FORCE_NO_INLINE inline int unconditional_exception_filter() {
45+
collect_current_trace(1);
46+
return 0; // EXCEPTION_CONTINUE_SEARCH
47+
}
4448
#else
4549
class CPPTRACE_EXPORT unwind_interceptor {
4650
public:
4751
virtual ~unwind_interceptor();
4852
};
53+
class CPPTRACE_EXPORT unconditional_unwind_interceptor {
54+
public:
55+
virtual ~unconditional_unwind_interceptor();
56+
};
4957

5058
CPPTRACE_EXPORT void do_prepare_unwind_interceptor(char(*)(std::size_t));
5159

@@ -81,6 +89,16 @@ namespace cpptrace {
8189
} __except(::cpptrace::detail::exception_filter()) {} \
8290
}(); \
8391
} catch(param)
92+
#define CPPTRACE_TRYZ \
93+
try { \
94+
[&]() { \
95+
__try { \
96+
[&]() {
97+
#define CPPTRACE_CATCHZ(param) \
98+
}(); \
99+
} __except(::cpptrace::detail::unconditional_exception_filter()) {} \
100+
}(); \
101+
} catch(param)
84102
#else
85103
#define CPPTRACE_TRY \
86104
try { \
@@ -92,6 +110,22 @@ namespace cpptrace {
92110
#define CPPTRACE_CATCH(param) \
93111
} catch(::cpptrace::detail::unwind_interceptor&) {} \
94112
} catch(param)
113+
#define CPPTRACE_TRYZ \
114+
try { \
115+
try {
116+
#define CPPTRACE_CATCHZ(param) \
117+
} catch(::cpptrace::detail::unconditional_unwind_interceptor&) {} \
118+
} catch(param)
119+
#endif
120+
121+
#define CPPTRACE_CATCH_ALT(param) catch(param)
122+
123+
#ifdef CPPTRACE_UNPREFIXED_TRY_CATCH
124+
#define TRY CPPTRACE_TRY
125+
#define CATCH(param) CPPTRACE_CATCH(param)
126+
#define TRYZ CPPTRACE_TRYZ
127+
#define CATCHZ(param) CPPTRACE_CATCHZ(param)
128+
#define CATCH_ALT(param) CPPTRACE_CATCH_ALT(param)
95129
#endif
96130

97131
#endif

src/from_current.cpp

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,16 @@ namespace cpptrace {
4848
return false;
4949
}
5050

51+
CPPTRACE_FORCE_NO_INLINE
52+
bool unconditional_exception_unwind_interceptor(const std::type_info*, const std::type_info*, void**, unsigned) {
53+
collect_current_trace(1);
54+
return false;
55+
}
56+
57+
using do_catch_fn = decltype(intercept_unwind);
58+
5159
unwind_interceptor::~unwind_interceptor() = default;
60+
unconditional_unwind_interceptor::~unconditional_unwind_interceptor() = default;
5261

5362
#if IS_LIBSTDCXX
5463
constexpr size_t vtable_size = 11;
@@ -194,10 +203,7 @@ namespace cpptrace {
194203
}
195204
#endif
196205

197-
// allocated below, cleaned up by OS after exit
198-
void* new_vtable_page = nullptr;
199-
200-
void perform_typeinfo_surgery(const std::type_info& info) {
206+
void perform_typeinfo_surgery(const std::type_info& info, do_catch_fn* do_catch_function) {
201207
if(vtable_size == 0) { // set to zero if we don't know what standard library we're working with
202208
return;
203209
}
@@ -243,12 +249,13 @@ namespace cpptrace {
243249
}
244250

245251
// allocate a page for the new vtable so it can be made read-only later
246-
new_vtable_page = allocate_page(page_size);
252+
// the OS cleans this up, no cleanup done here for it
253+
void* new_vtable_page = allocate_page(page_size);
247254
// make our own copy of the vtable
248255
memcpy(new_vtable_page, type_info_vtable_pointer, vtable_size * sizeof(void*));
249256
// ninja in the custom __do_catch interceptor
250257
auto new_vtable = static_cast<void**>(new_vtable_page);
251-
new_vtable[6] = reinterpret_cast<void*>(intercept_unwind);
258+
new_vtable[6] = reinterpret_cast<void*>(do_catch_function);
252259
// make the page read-only
253260
mprotect_page(new_vtable_page, page_size, memory_readonly);
254261

@@ -273,7 +280,11 @@ namespace cpptrace {
273280
if(!did_prepare) {
274281
cpptrace::detail::intercept_unwind_handler = intercept_unwind_handler;
275282
try {
276-
perform_typeinfo_surgery(typeid(cpptrace::detail::unwind_interceptor));
283+
perform_typeinfo_surgery(typeid(cpptrace::detail::unwind_interceptor), intercept_unwind);
284+
perform_typeinfo_surgery(
285+
typeid(cpptrace::detail::unconditional_unwind_interceptor),
286+
unconditional_exception_unwind_interceptor
287+
);
277288
} catch(std::exception& e) {
278289
std::fprintf(
279290
stderr,

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ if(NOT CPPTRACE_SKIP_UNIT)
8484
unit/object_trace.cpp
8585
unit/stacktrace.cpp
8686
unit/from_current.cpp
87+
unit/from_current_z.cpp
8788
unit/traced_exception.cpp
8889
)
8990
target_compile_features(unittest PRIVATE cxx_std_20)

0 commit comments

Comments
 (0)