Performance Implications of try/catch/finally, Part Two

In a previous blog entry Performance Implications of try/catch/finally I outlined that the conventional wisdom that there are no performance implications to try blocks unless an exception is thrown is false.  I have some clarifications and details to add.


My original tests used academic sample code like this:



        public int Method( )


        {


            int i = 10;


            try


            {


                i = 20;


                i = 30;


                Thread.Sleep(i);


            }


            finally


            {


                Thread.Sleep(i);


            }


            return i;


        }


As written, the optimized instructions generated by the JIT (Just-In-Time compiler) was showing that no optimizations were occurring in the try block in that the only obvious optimization–the assignment of 20 to i–wasn’t optimized away.


But, I’ve loaded-the-deck, so to speak.  Let’s consider what exception handling needs to be able to do.  In this case, I’ve explicitly said I need to do something in the event of an exception in the block of code I’ve wrapped with try{}.  The JIT takes me at my word and assumes every line in that block could throw an exception.  In order for the finally block to get accurate values for the current state–in the event of that exception–it can’t optimize any variables that are used in or past the finally block. In this case 10 must first be assigned to i, then 20 must be assigned to i then 30 must be assigned to i in the sequence specified in the source code; because if those assignments threw an exception, the finally block would need one of the values: 10, 20, or 30.


As you might be able to tell, I’ve loaded-the-deck because I don’t have much outside the try block and everything in the try block is used outside of it as well.  I haven’t given the JIT much to work with.  So, the “no optimizations are performed in try blocks” isn’t entirely accurate.  Obviously, in this case, the assignment of 10 to i wasn’t optimized as well.  But, the JIT is smart enough to attempt to optimize within the try block.  If we change the sample slightly to this:



        public int Method( )


        {


            int i = 10;


            try


            {



                int c = 2;


                c = 3;


                i = 20;


                i = 30;


                Thread.Sleep(c);


            }


            finally


            {


                Thread.Sleep(i);


            }


            return i;


        }


…the JIT knows that c is not used outside the try block and proceeds to optimize it away and converts the Sleep statement into Sleep(3).  Essentially it’s if I had written:



        public int Method( )


        {


            int i = 10;


            try


            {


                i = 20;


                i = 30;


                Thread.Sleep(3);


            }


            finally


            {


                Thread.Sleep(i);


            }


            return i;


        }


 


Okay, things aren’t that dire; at least the JIT is doing what it can within the try block.  But what about outside the try block; how drastic are the lack of optimizations there?  Again, the JIT is smart enough to do what it can. If we change our original sample slightly again:


 



        public int Method( )


        {


            int i = 10;


            int b = 9;


            b = 7;


            try


            {


                i = 20;


                i = 30;


                Thread.Sleep(i);


            }


            finally


            {


                Thread.Sleep(i);


            }


            return b;


        }


We see that the JIT is smart enough to attempt to optimize what’s outside the try block, and essentially results in this:



        public int Method( )


        {


            int i = 10;


            int b = 7;


            try


            {


                i = 20;


                i = 30;


                Thread.Sleep(i);


            }


            finally


            {


                Thread.Sleep(i);


            }


            return b;


        }


Better, it’s coalesced the writes (7 and 9) to b into a single write (7); but for some reason it still forces the assignment of 7 to b before the try block.  There’s nothing that could see b until the return, so it should be able to optimize it a bit further to this:



        public int Method( )


        {


            int i = 10;


            try


            {


                i = 20;


                i = 30;


                Thread.Sleep(i);


            }


            finally


            {


                Thread.Sleep(i);


            }


            return 7;


        }


So, while my previous statement wasn’t entirely accurate, I think what the JIT can optimize is neither better nor worse than “no optimizations are performed in try blocks”, just different. You should still pay close attention to what you’re doing in and around try blocks.


Also, an apples-to-oranges comparison; but methods with try/catch/finally blocks won’t be inlined.


Having described what the JIT appears to be doing, and the implication that optimizations to variables used before and after will result in only coalesce-ations prior to the try block (i.e. we’ve observed that it won’t optimize away a variable declared before a try block that is not used within the try or the finally); you should not rely on those side-effects.  As far a I’m concerned the JIT is free to perform that last optimization I described, and probably many others.

6 thoughts on “Performance Implications of try/catch/finally, Part Two

  1. Hi Peter,

    A couple of questions:

    1. How does the JIT handle variable enregistration in try/catch/finally blocks for C#?

    2. Doesn’t JIT optimization vary between compilers in .NET?

    2. Isn’t the try/catch/finally block really a type of CER (Constrained Execution Region)?

    3. How can you actually *see* what the JIT compiler outputs?

    Thanks for your time 🙂

    Dave

  2. Does this mean,
    if we use some code in try/finally then only performance can be hurt.

    Not in the case of try/catch.

    I am trying to understand , all your mentioned code snippet is type of try/finally. is same thing applicable for try/catch also?

  3. Hi Peter,

    A couple of questions:

    1. How does the JIT handle variable enregistration in try/catch/finally blocks for C#?

    2. Doesn’t JIT optimization vary between compilers in .NET?

    2. Isn’t the try/catch/finally block really a type of CER (Constrained Execution Region)?

    3. How can you actually *see* what the JIT compiler outputs?

    Thanks for your time 🙂

  4. @Dave 1: not sure I understand what you mean there. 2: each compiler generates specific IL to delineate the try/catch/finally regions–so, I don’t *think* there would be differences. 2b: from an IL standpoint yes. The limited optimizations is generally at the compiler level. 3: You can use debugging extensions; but, I just create a sample app that I run outside the debugger, then attach to it and force a break point. At that point I can view assembly and see what the JIT generated.

Leave a Reply to Dave Black Cancel reply

Your email address will not be published. Required fields are marked *