Digging into the CoreCLRExceptional costs, Part I
Note, this post was written by Federico.
One guideline which is commonly known is: "Do not use exceptions for flow control." You can read more about it in many places, but this is good compendium of the most common arguments. If you are not acquainted with the reasons, give them a read first; I’ll wait.
Many of the reasons focus on the readability of the code, but remember, my work (usually) revolves around writing pretty disgusting albeit efficient code. So even though I care about readability it is mostly achieved through very lengthy comments on the code on why you shouldn't touch something if you cannot prove something will be faster.
Digression aside the question is still open. What is the impact of using exceptions for control flow (or having to deal with someone else throwing exceptions) in your performance sensitive code? Let's examine that in detail.
For that we will use a very simple code to understand what can happen.
This is a code that is simple enough so that the assembler won’t get too convoluted, but at the same time sport at least some logic we can use as markers.
Let's first inspect the method CanThrow, in there what we can see is how the throwing of exceptions happen:
As you can see there is a lot of things to be done just to throw the exception. There in the last call we will use jump to the proper place in the stack and continue in the catch statement that we hit.
So here is the code of our simple method. At the assembler level, our try statement has a very important implication. Each try-catch forces the method to deal with a few control flow issues. First it has to store the exception handler in case anything inside would throw, then it has to do the actual work. If there is no exception (the happy path) we move forward and end. But what happen if we have an exception? We first need to remove the handler (we don't want to recheck this handler if we end up throwing inside the catch, right?) Then execute the catch and be done.
But now let’s contrast that to the generated code if no try-catch statement happens. The avid reader will realize that the happy path will never be executed because we are throwing, but don’t worry, the code is the same if there is no inlining happening.
We will talk about why the code ends up like this in a follow up post, but suffice to say that all this trouble cannot beat a check for a Boolean if you needed to fail (and could do something about it).
It is also important to remember that this kind of work is only relevant if you are in the hot path. If you are not calling a function at least a few tens of thousands a second, don’t even bother, your performance costs are elsewhere. This is micro optimization land.
More posts in "Digging into the CoreCLR" series:
- (25 Nov 2016) Some bashing on the cost of hashing
- (12 Aug 2016) Exceptional costs, Part II
- (11 Aug 2016) Exceptional costs, Part I
- (10 Aug 2016) JIT Introduction
Nice writeup, it's cool to see someone digging into .NET/CoreCLR like this.
BTW I did some perf tests on the costs of throwing/catching exceptions that you might find interesting
Basically exceptions are expensive, but the cost really goes up if/when you access the stack trace.
@Matt Great work there. The difference between 7ns and 8000ns for tight code is hardly something I could leave hanging when optimizing tight code :D
Learned something new, thanks a lot and @Matt, great work