News for hackers. Stuff that matters.
Re:The Cost of Exception Handling by saurik on Saturday November 12, 2022 @02:47AM (#33571012)
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 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 :(.)

Re:The Cost of Exception Handling by mrjin on Saturday November 12, 2022 @04:28AM (#33571544)
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.
Re:The Cost of Exception Handling by saurik on Saturday November 12, 2022 @05:18AM (#33571795)
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.
Re:The Cost of Exception Handling by gpderetta on Tuesday November 15, 2022 @05:16AM (#33607371)
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.
Re:The Cost of Exception Handling by bregma on Saturday November 12, 2022 @07:04AM (#33572309)
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.
Re:The Cost of Exception Handling by saurik on Saturday November 12, 2022 @10:56AM (#33574200)
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.
Re:The Cost of Exception Handling by bregma on Friday November 18, 2022 @04:01PM (#33662110)
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.