Click here to Skip to main content
15,896,912 members
Articles / .NET

Why Exceptions Should Be Exceptional

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
20 Dec 2016CPOL5 min read 5.3K   2   1
Why Exceptions should be Exceptional

Meteor Hit on the Earth

According to the NASA ‘Near Earth Object Program’ asteroid 101955 Bennu (1999 RQ36) has a Cumulative Impact Probability of 3.7e-04, i.e. there is a 1 in 2,700 (0.0370%) chance of Earth impact, but more reassuringly there is a 99.9630% chance the asteroid will miss the Earth completely!

But how does this relate to exceptions in the .NET runtime, well let’s take a look at the official .NET Framework Design Guidelines for Throwing Exceptions (which are based on the excellent book Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries)

Framework Design Guidelines for Exceptions

So exceptions should be exceptional, unusual or rare, much like a asteroid strike!!

.NET Framework TryXXX() Pattern

In .NET, the recommended was to avoid exceptions in normal code flow is to use the TryXXX() pattern. As pointed out in the guideline section on Exceptions and Performance, rather than writing code like this, which has to catch the exception when the input string isn’t a valid integer:

C#
try
{
    int result = int.Parse("IANAN");
    Console.WriteLine(result);
}
catch (FormatException fEx)
{
    Console.WriteLine(fEx);
}

You should instead use the TryXXX API, in the following pattern:

C#
int result;
if (int.TryParse("IANAN", out result))
{
    // SUCCESS!!
    Console.WriteLine(result);
}
else
{
    // FAIL!!
}

Fortunately large parts of the .NET runtime use this pattern for non-exceptional events, such as parsing a string, creating a URL or adding an item to a Concurrent Dictionary.

The Performance Costs of Exceptions

So onto the performance costs, I was inspired to write this post after reading this tweet from Clemens Vasters:

Clemens Vasters tweet

I also copied/borrowed a large amount of ideas from the excellent post ‘The Exceptional Performance of Lil’ Exception’ by Java performance guru Aleksey Shipilëv (this post is in essence the .NET version of his post, which focuses exclusively on exceptions in the JVM)

So lets start with the full results (click for full-size image):

Exception Benchmark Results

(Full Benchmark Code and Results)

Rare Exceptions v Error Code Handling

Up front I want to be clear that nothing in this post is meant to contradict the best-practices outlined in the .NET Framework Guidelines (above), in fact I hope that it actually backs them up!

MethodMeanStdErrStdDevScaled
ErrorCodeWithReturnValue1.4472 ns0.0088 ns0.0341 ns1.00
RareExceptionStackTrace22.0401 ns0.0292 ns0.1132 ns15.24
RareExceptionMediumStackTrace61.8835 ns0.0609 ns0.2279 ns42.78
RareExceptionDeepStackTrace115.3692 ns0.1795 ns0.6953 ns79.76

Here we can see that as long as you follow the guidance and ‘DO NOT use exceptions for the normal flow of control’ then they are actually not that costly. I mean yes, they’re 15 times slower than using error codes, but we’re only talking about 22 nanoseconds, i.e. 22 billionths of a second, you have to be throwing exceptions frequently for it to be noticeable. For reference, here’s what the code for the first 2 results looks like:

C#
public struct ResultAndErrorCode<T>
{
    public T Result;
    public int ErrorCode;
}

[Benchmark(Baseline = true)]
public ResultAndErrorCode<string> ErrorCodeWithReturnValue()
{
    var result = new ResultAndErrorCode<string>();
    result.Result = null;
    result.ErrorCode = 5;
    return result;
}

[Benchmark]
public string RareExceptionStackTrace()
{
    try
    {
        RareLevel20(); // start all the way down
        return null; //Prevent Error CS0161: not all code paths return a value

    }
    catch (InvalidOperationException ioex)
    {
        // Force collection of a full StackTrace
        return ioex.StackTrace;
    }
}

Where the ‘RareLevelXX() functions look like this (i.e. will only trigger an exception once for every 2,700 times it’s called):

C#
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RareLevel1() { RareLevel2(); }
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RareLevel2() { RareLevel3(); }
... // several layers left out!!
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RareLevel19() { RareLevel20(); }
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RareLevel20()
{
    counter++;
    // will *rarely* happen (1 in 2700)
    if (counter % chanceOfAsteroidHit == 1) 
        throw new InvalidOperationException("Deep Stack Trace - Rarely triggered");            
}

Therefore RareExceptionMediumStackTrace() just calls RareLevel10() to get a medium stack trace and RareExceptionDeepStackTrace() calls RareLevel1() which triggers the full/deep one (the full benchmark code is available).

Stack traces

Now that we’ve seen the cost of calling exceptions rarely, we’re going to look at the effect the stack trace depth has on performance. Here are the full, raw results:

MethodMeanStdErrStdDevGen 0Allocated
Exception-Message9,187.9417 ns13.4824 ns48.6117 ns-148 B
Exception-TryCatch9,253.0215 ns13.2496 ns51.3154 ns-148 B
ExceptionMedium-Message14,911.7999 ns20.2448 ns78.4078 ns-916 B
ExceptionMedium-TryCatch15,158.0940 ns147.4210 ns737.1049 ns-916 B
ExceptionDeep-Message19,166.3524 ns30.0539 ns116.3984 ns-916 B
ExceptionDeep-TryCatch19,581.6743 ns208.3895 ns833.5579 ns-916 B
CachedException-StackTrace29,354.9344 ns34.8932 ns135.1407 ns-1.82 kB
Exception-StackTrace30,178.7152 ns41.0362 ns158.9327 ns-1.93 kB
ExceptionMedium-StackTrace100,121.7951 ns129.0631 ns499.8591 ns0.195315.71 kB
ExceptionDeep-StackTrace154,569.3454 ns205.2174 ns794.8034 ns3.613327.42 kB

Note: in these tests we are triggering an exception every-time a method is called, they aren’t the rare cases that we measured previously.

Exception handling without collecting the full StackTrace

First we are going to look at the results measuring the scenario where we don’t explicitly collect the StackTrace after the exception is caught, so the benchmark code looks like this:

C#
[Benchmark]
public string ExceptionMessage()
{
    try
    {
        Level20(); // start *all* the way down the stack
        return null; //Prevent Error CS0161: not all code paths return a value
    }
    catch (InvalidOperationException ioex)
    {
        // Only get the simple message from the Exception 
        // (don't trigger a StackTrace collection)
        return ioex.Message;
    }
}

In the following graphs, shallow stack traces are in blue bars, medium in orange and deep stacks are shown in green

Exception Handling - NOT Calculating StackTrace

So we clearly see there is an extra cost for exception handling that increases the deeper the stack trace goes. This is because when an exception is thrown the runtime needs to search up the stack until it hits a method than can handle it. The further it has to look up the stack, the more work it has to do.

Exception handling including collection of the full StackTrace

Now for the final results, in which we explicitly ask the run-time to (lazily) fetch the full stack trace, by accessing the StackTrace property. The code looks like this:

C#
[Benchmark]
public string ExceptionStackTrace()
{
    try
    {
        Level20(); // start *all* the way down the stack
        return null; //Prevent Error CS0161: not all code paths return a value
    }
    catch (InvalidOperationException ioex)
    {
        // Force collection of a full StackTrace
        return ioex.StackTrace;
    }
}

Exception Handling - Calculating StackTrace

Finally we see that fetching the entire stack trace (via StackTrace) dominates the performance of just handling the exception (ie. only accessing the exception message). But again, the deeper the stack trace, the higher the cost.

So thanks goodness we’re in the .NET world, where huge stack traces are rare. Over in Java-land they have to deal with nonesense like this (click to see the full-res version!!):

Huge Java Stack Trace

Conclusion

  1. Rare or Exceptional exceptions are not hugely expensive and they should always be the preferred way of error handling in .NET
  2. If you have code that is expected to fail often (such as parsing a string into an integer), use the TryXXX() pattern
  3. The deeper the stack trace, the more work that has to be done, so the more overhead there is when catching/handling exceptions
  4. This is even more true if you are also fetching the entire stack trace, via the StackTrace property. So if you don’t need it, don’t fetch it.

Discuss this post in /r/programming and /r/csharp

Further Reading

The stack trace of a StackTrace!!

The full call-stack that the CLR goes through when fetching the data for the Exception StackTrace property

The post Why Exceptions should be Exceptional first appeared on my blog Performance is a Feature!

This article was originally posted at http://www.mattwarren.org/atom.xml

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugNo images and code broken Pin
Klaus Luedenscheidt20-Dec-16 18:40
Klaus Luedenscheidt20-Dec-16 18:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.