3 ms·
So, I went to replicate the bottom case, and particularly want to do it more "fairly" by actually isolating the resulting code rather than staring at a ton of c
by saurik 4y ago
So, I went to replicate the bottom case, and particularly want to do it more "fairly" by actually isolating the resulting code rather than staring at a ton of complex intermediate assembly :/. I'm thereby building a real binary--after carefully separating the compilation stages using -c and avoiding any linker optimization--instead of -S and then using objdump -d and then--because this is the whole point of talking about the happy path--I'm only providing the code of the actual functions and not any subsequent exception landing pads.
Without exceptions:
0000000000401110 <main>:
401110: 50 push %rax
401111: e8 2a 00 00 00 callq 401140 <construct>
401116: e8 25 00 00 00 callq 401140 <construct>
40111b: e8 20 00 00 00 callq 401140 <construct>
401120: e8 2b 00 00 00 callq 401150 <destruct>
401125: e8 26 00 00 00 callq 401150 <destruct>
40112a: e8 21 00 00 00 callq 401150 <destruct>
40112f: b8 08 00 00 00 mov $0x8,%eax
401134: 59 pop %rcx
401135: c3 retq
With exceptions:
0000000000401160 <main>:
401160: 50 push %rax
401161: 48 89 e7 mov %rsp,%rdi
401164: e8 37 00 00 00 callq 4011a0 <A<5u, void>::A()>
401169: e8 a2 00 00 00 callq 401210 <destruct>
40116e: e8 9d 00 00 00 callq 401210 <destruct>
401173: e8 98 00 00 00 callq 401210 <destruct>
401178: b8 08 00 00 00 mov $0x8,%eax
40117d: 59 pop %rcx
40117e: c3 retq
00000000004011a0 <A<5u, void>::A()>:
4011a0: 53 push %rbx
4011a1: e8 5a 00 00 00 callq 401200 <construct>
4011a6: e8 55 00 00 00 callq 401200 <construct>
4011ab: e8 50 00 00 00 callq 401200 <construct>
4011b0: 5b pop %rbx
4011b1: c3 retq
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 am not sure if the author actually understood what they were looking at, as they didn't 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 doesn't feel like 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 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 it seems that it didn't occur to the author to do an 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 :(.)
- mrjin 4y agoThere 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.
- saurik 4y agoThe 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.
- gpderetta 4y agoFor 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.
- bregma 4y agoIn 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.
- saurik 4y agoSorry, 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.