views:

273

answers:

2

Hello:

I've been thinking about nested try/catch statements and started to think about under which conditions, if any, the JIT can perform an optimization or simplification of the compiled IL.

To illustrate, consider the following functionally-equivalent representations of an exception handler.

// Nested try/catch
try
{
  try
  {
    try
    {
      foo();
    }
    catch(ExceptionTypeA) { }
  }
  catch(ExceptionTypeB) { }
}
catch(ExceptionTypeC) { }

// Linear try/catch
try
{
  foo();
}
catch(ExceptionTypeA) { }
catch(ExceptionTypeB) { }
catch(ExceptionTypeC) { }

Assuming there are no additional variable references or function calls within the stack frames of the nested try statement, can the JIT conclude that the stack frames may be collapsed to the linear example?

Now how about the following example?

void Try<TException>(Action action)
{
  try
  {
    action();
  }
  catch (TException) { }
}

void Main()
{
  Try<ExceptionC>(Try<ExceptionB>(Try<ExceptionA>(foo)));
}

I don't think there is any way for the JIT to inline the delegate invocations, so this example can't be reduced to the previous one. However in the event of foo() throwing ExceptionC, does this solution perform poorer when compared to the linear example? I suspect there is an extra cost to tear down the stack frames from the delegate invocations, even though the extra data contained in the frames is minimal.

+7  A: 

It's worth noting that in the first case they're only functionally equivalent when you're doing nothing within the catch block. Otherwise, consider this:

try
{
    foo();
}
catch (IOException)
{
    throw new ArgumentException(); // Bubbles up to caller
}
catch (ArgumentException)
{
    Console.WriteLine("Caught");
}

vs

try
{
    try
    {
        foo();
    }
    catch (IOException)
    {
        throw new ArgumentException(); // Caught by other handler
    }
}
catch (ArgumentException)
{
    Console.WriteLine("Caught");
}

Now in this case the difference is obvious, but if the catch block calls some arbitrary method, how is the JIT meant to know what might be thrown? Best to be cautious.

That leaves us with the option of the JIT performing optimisations for empty catch blocks - a practice which is strongly discouraged in the first place. I don't want the JIT to spend time trying to detect bad code and make it run very slightly faster - if indeed there's any performance difference in the first place.

Jon Skeet
Thank you for the analysis Jon. In my mind, the empty catch handlers were performing no-throw operations. However, your analysis still applies since exceptions like OutOfMemoryException and ThreadAbortExcetpion may occur for reasons external to foo().
Steve Guidi
+4  A: 

My understanding of try/catch/finally regions with respect to performance is that such regions are transparent to the regular execution of code. That is, if your code does not throw any exceptions to catch, then the try/catch/finally regions have ZERO impact on code execution performance.

However, when an exception is raised, the runtime starts walking up the stack from the site at which it is raised, checking tables of metadata to see whether the site in question is contained within any of the critical try blocks. If one is found (and it has an eligible catch block or a finally block) then the relevant handler is identified and execution branches to this point.

The process of raising and handling exceptions is expensive from a performance perspective. Programmers should not use exceptions as a way of signalling or controlling program flow in anything other than exceptional circumstances (pun intended.)

Drew Noakes
+1 for explaining the runtime cost of try-catch blocks
SealedSun