There is much more than the superficial code analysis. Especially on Windows, there is a whole framework of how exceptions are handled etc.. To me, exceptions are super expensive, we should avoid if we can.
The OS should normally have no involvement in C++ exceptions, and that certainly isn't the case on any operating system except Windows, where you can request that exceptions are bridged with structured exception handling, but I honestly don't believe that is common anymore, and even if you do opt into that (which I will argue you should probably never do, though I've sometimes been shocked that people still give a shit about the MSVC ABI :/), I also do not believe it is going to add a direct runtime cost to the happy path on 64-but Windows (which is all that we are analyzing here anyway, as the legacy ABIs usually used for 32-bit Intel systems definitely had a non-zero cost) as I am quite confident it is also implemented with tables.
For a while C++ exceptions on MSVC were implemented on top of the OS SEH. People still have scars about catch(...) catching segmentation faults. This has changed more than a decade ago though and has never been the case for 64bit code.
In an ELF binary the exception tables are not intermingled with executable code but are coalesced into the .eh_frame section loaded in a separate read-only non-executable segment. The happy path is truly just the happy path standing alone.
Sorry, I don't know all of the correct terminology. Is it the "landing pads"? Here is the code that is generated for main (after removing that constructor so it ends up without he extra call), and this is all in the text segment... and this definitely isn't "the happy path standing alone".
0000000000401160 <main>:
401160: 53 push %rbx
401161: e8 8a 00 00 00 callq 4011f0 <construct>
401166: e8 85 00 00 00 callq 4011f0 <construct>
40116b: e8 80 00 00 00 callq 4011f0 <construct>
401170: e8 8b 00 00 00 callq 401200 <destruct>
401175: e8 86 00 00 00 callq 401200 <destruct>
40117a: e8 81 00 00 00 callq 401200 <destruct>
40117f: b8 08 00 00 00 mov $0x8,%eax
401184: 5b pop %rbx
401185: c3 retq
401186: 48 89 c7 mov %rax,%rdi
401189: e8 52 00 00 00 callq 4011e0 <__clang_call_terminate>
40118e: 48 89 c7 mov %rax,%rdi
401191: e8 4a 00 00 00 callq 4011e0 <__clang_call_terminate>
401196: 48 89 c7 mov %rax,%rdi
401199: e8 42 00 00 00 callq 4011e0 <__clang_call_terminate>
40119e: 48 89 c3 mov %rax,%rbx
4011a1: e8 5a 00 00 00 callq 401200 <destruct>
4011a6: e8 55 00 00 00 callq 401200 <destruct>
4011ab: eb 18 jmp 4011c5 <main+0x65>
4011ad: 48 89 c7 mov %rax,%rdi
4011b0: e8 2b 00 00 00 callq 4011e0 <__clang_call_terminate>
4011b5: 48 89 c7 mov %rax,%rdi
4011b8: e8 23 00 00 00 callq 4011e0 <__clang_call_terminate>
4011bd: 48 89 c3 mov %rax,%rbx
4011c0: e8 3b 00 00 00 callq 401200 <destruct>
4011c5: 48 89 df mov %rbx,%rdi
4011c8: e8 93 fe ff ff callq 401060 <_Unwind_Resume@plt>
4011cd: 48 89 c7 mov %rax,%rdi
4011d0: e8 0b 00 00 00 callq 4011e0 <__clang_call_terminate>
4011d5: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4011dc: 00 00 00
4011df: 90
The issue isn't the non-executable code: it is that it generates a lot more executable code that it intermixes with all of the rest of the code next to the code that relies on it, which is great for the performance of quickly being able to jump into the exceptional case but heavily dilutes the code cache and implies many more page faults and TLB misses.The happy path is 401160 through 401185. The unwinding code no more dilutes the code cache than any other subsequent and possibly unrelated functions in memory.
The unwinding code is not there to make it faster since it's generally reached after walking the stack twice. It's moved out of the happy path to make the happy path faster since it doesn't dilute the instruction cache and doesn't hit the pipeline.
Without exceptions:
With exceptions: Now, first off, I hope people can see that this is a much more fair comparison. I am not sure why the author of this article wanted to use -S, but it seems more than a bit disingenuous to be complaining about how "tidy" the happy path is after pasting a million assembler directives at us as if those actually have a runtime performance cost instead of actually looking at the resulting execution paths through the assembly that results.(And I want to be very clear here: this isn't because I somehow got a different result. If you carefully read through the assembly presented in the article and mentally skip all the assembler directives, you will see they got something similar, with a call to the constructor followed by three calls to destruct immediately one after the other. That said, I frankly don't think the author actually understood what they were looking at, as they didn't even provide the code for __ZN1AILj5EvEC2Ev, which is clearly part of the happy path :/.)
But... yeah: it added the cost of a single extra call indirection for some reason. I'm not entirely sure why, but this frankly isn't a ridiculous cost. I will also note that it actually generates identical code to the case without exceptions if you merely add a single "inline" keyword to the code on the constructor for A (note: I mean the one on line 9 that is ostensibly empty, not the one on line 16). That said, you also probably wouldn't actually WRITE such a silly no-op constructor in the first place, and it turns out deleting it also fixes the resulting code to be exactly the same with and without exceptions, so I'm not sure of the applicability here... this just feels like such a corner case :/.
(The actual reasonable complaint to make about "zero cost" exceptions--which this article didn't make, because the author seemed to be too busy staring at assembly instead of doing actual timing analysis, which is where I was hoping this article would go--is that the exception tables tend to get mixed in with the rest of the normal functions, which massively pads out the code space, which will lead to many more TLB misses and page fetches while executing the happy path. There should be a way to move these landing pads all to the end of the executable to be loaded into memory only if absolutely necessary; but, if that's already possible, I personally don't know how to do that :(.)