x64 totally changes the way exceptions on the PC are implemented. There are two major paradigms for exceptions :
1. Push/pop tracking and unwinding. Each time you enter a function some unwind info is pushed, and when you leave it is popped. When an exception fires it walks the current unwind stack, calling destructors and looking for handlers. This makes executables a lot larger because it adds lots of push/pop instructions, and it also adds a speed hit everywhere even when exceptions are never thrown becuase it still has to push/pop all the unwind info. MSVC/Win32 has done it this way. ("code driven")
2. Table lookup unwinding. A table is made at compile time of how to unwind each function. When an exception is thrown, the instruction pointer is used to find the unwind info in the table. This is obviously much slower at throw time, but much faster when not throwing, and also doesn't bloat the code size (if you don't count the table, which doesn't normally get loaded at all). I know Gcc has done this for a while (at least the SN Systems version that I used a while ago). ("data driven")
All x64 code uses method #2 now. This is facilitated by the new calling convention. Part of that is that the "frame pointer omission" optimization is forbidden - you must always push the original stack and return address, because that is what is walked to unwind the call sequence when a throw occurs. If you write your own ASM you have to provide prolog/epilog unwind info, which goes into the unwind table to tell it how to unwind your stack.
This has been rough and perhaps slightly wrong. Read here for more :
X64 Unwind Information - FreiK's WebLog - Site Home - MSDN Blogs
Uninformed - vol 4 article 1
Introduction to x64 debugging, part 3 � Nynaeve
Introduction to x64 debugging, part 2 � Nynaeve
Exception Handling (x64)
BTW many people (Pierre, Jeff, etc.) think the overhead of Win32's "code driven" style of exception handling is excessive. I agree to some extent, however, I don't think it's terribly inefficient. That is, it's close to the necessary amount of code and time that you have to commit if you actually want to catch all errors and be able to handle them. The way we write more efficient code is simply by not catching all errors (or not being able to continue or shut down cleanly from them). That is we decide that certain types of errors will just crash the app. What I'm saying is if you actually wrote error checkers and recovery for every place you could have a failure, it would be comparable to the overhead of exception handling. (ADDENDUM : nah this is just nonsense)
3 comments:
I think the push/pop style is bad when you have I-cache pressure. This is somewhat mitigated if you have some kind of profile-guided optimization that is doing hot/cold code layout optimizations, but catch() {} is a pretty strong static signal that what transpires is going to be rare, enough that the much slower table-based approach (which is much nicer on the hot instruction stream compared to the push/pops) ends up being an optimization.
There is another issue: your error handling can be done non-locally. You can have code that throws deep in your stack, and you don't have to write code to marshal that error to the handler. That's one way where error-handling with tables ends up being more compact (and therefore, optimistically faster) than the push/pop style.
Oh yeah, you are correct on all counts, I didn't mean to imply that I thought the push/pop method was okay.
In some bizarro app where exceptions were thrown more often than not, it might be good, but in the normal case where exceptions are very very rare, the data driven method is clearly better.
So IMO it's a good thing they've done to build that style it into x64.
"That is, it's close to the necessary amount of code and time that you have to commit if you actually want to catch all errors and be able to handle them. The way we write more efficient code is simply by not catching all errors (or not being able to continue or shut down cleanly from them). That is we decide that certain types of errors will just crash the app."
I disagree.
"Cleanup stack"-based unwinding incurs a cost on every single function, which means it's equivalent to checking for error conditions in every single function. That is a very bad way of implementing error handling; a method that works much better is to just remember that an error occurred, but substitute
valid data as soon as possible.
That is, separate "tactical" error handling (which just needs to make sure your program ends up in a safe and consistent state) from "strategical" error handling (which is usually at a pretty high level in an app and might involve user interaction), and try to keep most intermediate layers unaware of both.
I consider this good practice in general, not least because immediately escalating error conditions not only makes for hard to understand control flow, but also a bad user experience. Take broken P4 connections, copies of large directories, things like that. Asking me on every single problem is just poor design, but it reflects the underlying model of the application reacting to every error code. Unless there's no way you can sensibly go on, just make a list of things that went wrong and show it to me at the end. That's not just a better user experience, it also tends to be quite easy if it's designed in from the beginning.
Post a Comment