I wrote a program that throws an exception and catches it to warm up, and then throws a second exception.
void first() {
throw "";
}
void second() {
throw 2;
}
int main(int argc, char *argv[]) {
try {
first();
} catch (...) {
}
second();
}
The second() function with LLVM libcxx using normal DWARF exceptions calls pthread_rwlock_rdlock SIX TIMES and calls pthread_rwlock_wrlock one time. Overall, this program triggers ~4000 function calls according to --ftrace (see https://justine.lol/ftrace/)On the other hand, if I use SJLJ, then this program triggers 269 function calls. It never locks anything. Although, like the first program, it does call pthread_once three times. That's doesn't cost anything though. It's the rwlocks you need to worry about.
For an example of a modern C++ toolchain that uses SJLJ exceptions, see https://github.com/jart/cosmopolitan/releases/tag/3.7.1
Please note that SJLJ exceptions carries a tradeoff. While SJLJ will make throwing exceptions much faster, you'll incur a slight performance degradation on normal code that isn't throwing exceptions. Using GCC in SJLJ mode will cause most of your C++ functions to have extra code inserted into the prologue and epilogue which calls _Unwind_SjLj_Register and _Unwind_SjLj_Unregister. Modern DWARF exception handling makes the intentional tradeoff of making exception throwing extremely slow, so that zero overhead is added for normal non-exceptional code. Both methods (SJLJ and DWARF) will bloat your binary size. With SJLJ it's just a few instructions inserted into prologues and epilogues. But with DWARF you get a lot of mandatory .eh_frame junk that's generated by everything, and it's centralized into a particular section of your binary that's only touched if exceptions are actually thrown.