Don’t Use Exceptions To Control Application Flow


 


Why? Because they can be thousands of times slower than method return values!


Can you guess why?


First here’s an example of what not to do. (from the Patterns & Practices Guidance article)


static void ProductExists( string ProductId)
 {
 //… search for Product
 if ( dr.Read(ProductId) ==0 ) // no record found, ask to create
         {
        
throw( new Exception(“Product Not found”));
         }
 }


This should have been handled with a meaningful method return value (such as ‘null’ or ‘-1′ for example) and is a bad idea for couple of reasons:



  1. Not being able to find a product is not an exceptional circumstance. The program logic should be able to handle this with a method return value rather than an Exception. I’m sure you can think of other similar cases where Exceptions have been used inappropriately.

  2. Exceptions are excellent when used correctly but they come with a price – execution overhead. The CLR is much, much slower to process Exceptions than it is to process normal program flow and method return values.

 What actually happens when an Exception is thrown?(from the ‘Exceptions Overview’ section of the .NET Framework Developer’s Guide)


“When an exception occurs, the runtime begins a two-step process:


1.  The runtime searches the array for the first protected block that:


·         Protects a region that includes the currently executing instruction, and


·         Contains an exception handler or contains a filter that handles the exception.


2.  If a match occurs, the runtime creates an Exception object that describes the exception. The runtime then executes all finally or fault statements between the statement where the exception occurred and the statement handling the exception. Note that the order of exception handlers is important: the innermost exception handler is evaluated first. Also note that exception handlers can access the local variables and local memory of the routine that catches the exception, but any intermediate values at the time the exception is thrown are lost.


If no match occurs in the current method, the runtime searches each caller of the current method, and it continues this path all the way up the stack. If no caller has a match, the runtime allows the debugger to access the exception. If the debugger does not attach to the exception, the runtime raises the UnhandledException event. If there are no listeners for the UnhandledException event, the runtime dumps a stack trace and ends the program.”


That’s alot of non-program related processing going on. You can imagine how convoluted that can get with a deep call stack and nested Exceptions.


I was curious as to the real effect of all this processing compared to a plain old return value so I whipped up some embarrassingly crummy code to test it (code included below). I was astonished at the results! Here’s a copy of the results of a test run on my dual core 2.2GHz 4GB T61p Lenovo Thinkpad running Visual Studio 2008.


Return value elapsed time  = 10 uSec
Return value elapsed time  = 9 uSec
Return value elapsed time  = 8 uSec
Exception handling elapsed time  = 2104 mSec
Exception handling elapsed time  = 2066 mSec
Exception handling elapsed time  = 2078 mSec


On average return values was 231407 times faster than exception handling.
Return value elapsed time  = 8 uSec
Return value elapsed time  = 8 uSec
Return value elapsed time  = 8 uSec
Exception handling elapsed time  = 2084 mSec
Exception handling elapsed time  = 2099 mSec
Exception handling elapsed time  = 2100 mSec


On average return values was 245705 times faster than exception handling.
Completed 2 timing tests. Press any key to exit…


What a huge difference in performance! A few things to note about the output shown here:


1.       There are two timing test runs. Two is an arbitrary number. I just wanted to show that it varies slightly between runs.


2.       Each run is comprised of three return value test runs and three exception handler test runs. I did this to allow the execution environment to stabliize . You can see the slight bump in the first set of numbers.


3.       I do a quick calculation at the end of each run to estimate the overall performance difference and what a difference there is – 8 microseconds vs 2,100,000 microseconds.


Then I realised I had compiled in Debug mode. “Of course it’s going to be slow” I thought to myself. So I recompiled and ran it in Release mode … the difference was even greater!


Return value elapsed time  = 6 uSec
Return value elapsed time  = 5 uSec
Return value elapsed time  = 4 uSec
Exception handling elapsed time  = 2184 mSec
Exception handling elapsed time  = 2142 mSec
Exception handling elapsed time  = 2167 mSec


On average return values was 432866 times faster than exception handling.
Return value elapsed time  = 4 uSec
Return value elapsed time  = 4 uSec
Return value elapsed time  = 4 uSec
Exception handling elapsed time  = 2506 mSec
Exception handling elapsed time  = 2175 mSec
Exception handling elapsed time  = 2156 mSec


On average return values was 493703 times faster than exception handling.
Completed 2 timing tests. Press any key to exit…


Notice the return value timings are roughly half what they were in Debug mode. My guess is that because Debug code is full of MSIL ‘NOP’ (no-op) instructions (so break points can be inserted if needed), it takes longer to process the MSIL where every second instruction is a NOP. Maybe that slows the “return values” timing test down in Debug mode. It doesn’t seem to make much difference to the Exception processing time though.


So it’s clear to me: (A “Note to self” that I thought I’d share with you) 


“Exceptions are an exceptionally J good tool for dealing with exceptional circumstances at runtime but should rarely, if ever, be used for routine program flow control.”



Anyway, I found this little excursion into the land of Exception processing performance enlightening – I hope you did too.


 


 


Here’s the C# code I used…


using System;


 


namespace ExceptionsVsreturnValues


{


    class Program


    {


        static int itterations = 1000;


        static int nStabilizeLoops = 3;


        static int nTestLoops = 2;


        static DateTime start, end;


        static TimeSpan retElapsed, retElapsedTotal, exceptionElapsed, exceptionElapsedTotal;


 


        static void Main(string[] args)


        {


            for (int i = 0; i < nTestLoops; i++)


                  {


               // First the return value timing


                returnValueTimingTest();


 


                // Next the exception handling timing


                exceptionHandlingTimingtest();


 


                // Report the results


                Console.WriteLine(“\nOn average return values was “ +


                    (int)(exceptionElapsedTotal.TotalMilliseconds / retElapsedTotal.TotalMilliseconds * 1000) +


                    ” times faster than exception handling.”);


            }


            Console.Write(“Completed “ + nTestLoops + ” timing tests. Press any key to exit…”);


            Console.ReadKey(false);


        }


 


        private static void exceptionHandlingTimingtest()


        {


            // Run the test ‘nLoop’ times to allow the executio nenvironment to stabilise


            for (int i = 0; i < nStabilizeLoops; i++)


            {


                start = DateTime.Now;


                for (int j = 0; j < itterations; j++)


                {


                    try


                    {


                        // Deliberatley send a null in place of the expected string


                        // to cause a null pointer exception to be thrown


                        findCustomerWithExceptions(null);


                    }


                    catch //(Exception e)


                    {


                        // Uncomment this and above if you want proof that an exception is being thrown


                        // Console.WriteLine(“Exception \”” + e.Message + “\” caught”);


                    }


                }


                end = DateTime.Now;


                exceptionElapsed = end – start;


                if (exceptionElapsedTotal == null) exceptionElapsedTotal = exceptionElapsed;


                else exceptionElapsedTotal += exceptionElapsed;


                Console.WriteLine(“Exception handling elapsed time  = “ + exceptionElapsed.TotalMilliseconds + ” mSec”);


            }


        }


 


        private static void returnValueTimingTest()


        {


            // Run the test ‘nLoop’ times to allow the executio nenvironment to stabilise


            for (int i = 0; i < nStabilizeLoops; i++)


            {


                start = DateTime.Now;


                for (int j = 0; j < itterations * 1000 /* need to scale up the time from uSec to mSec because this is too fast for raw comparison! */; j++)


                {


                    findCustomer(“fred”);


                }


                end = DateTime.Now;


                retElapsed = end – start;


                if (retElapsedTotal == null) retElapsedTotal = retElapsed;


                else retElapsedTotal += retElapsed;


                Console.WriteLine(“Return value elapsed time  = “ + retElapsed.TotalMilliseconds + ” uSec”);


            }


        }


 


        private static void findCustomerWithExceptions(string p)


        {


            // causes a null pointer exception if p == null


            p.Trim();


        }


 


        private static int findCustomer(string p)


        {


            return 0;


        }


    }


}



 


P.S. The story is similar with Java. I did a quick port to JDK 6 Update 4 and found return values are in the order of 4,000 times faster than Exception handling in the Sun JVM. Not as wide a gap as the CLR but still enough to avoid using Exceptions inappropriately no matter what platform you choose.


 


 


Comments (5)

  1. floyd domino says:

    I applaud this post.

    The title is absolutely correct: exceptions should not be used for application flow.  However, making the argument in terms of performance is like saying you shouldn’t run red lights because you might get a ticket, when the real reason is that it’s dangerous and wrong.

    If making the performance argument convinces some developers to abandon exception abuse, that’s good.

    But the real meat of the argument is that it is wrong.

    The example given is a good one: not finding a product (or any other type of lookup) is not an exceptional condition, it is one of many legitimate outcomes of a lookup, and should be handled as such.

    Treating it as an exception introduces a subjective bias into the outcome, based on some human viewpoint about what it is ‘normal’ or ‘better’.

    The real problem with abusing execptions like this is not runtime performance, but sloppy thinking.

  2. Ivan says:

    Hi.

    1. The return value of the ProductExists() method should not be a null and surely not -1. It should be a boolean false.

    2. You should not use exceptions for controlling the normal flow of the application even if they are 100 times faster than a return in the next generation of the CLR. Exceptions are for another thing than controlling the successful flow of your programs. They are for handling errors.

  3. Timo Stamm says:

    Sorry, but I decidedly disagree.

    Exception handling isn’t necessarily significantly slower than standard control structures. This is just an implementation issue, which might change. I measured the times for a JVM as well, and while that was about 2 years ago, I still remember that the difference wasn’t high enough to justify altering my designs.

    You really should not base the decision to use exceptions or standard control flow mechanisms on performance. Exceptions are called "exceptions" because they are intended for exceptional control flow, and you should use them for these cases. For example, they can be very useful to signal an invalid state through several layers of application code. Using return values is a very impractical alternative. Just choose the right tool for the job, and don’t impede your judgement with useless microbenchmarks.

    Donald Knuth wrote "We should forget about small efficiencies, about 97% of the time. Premature optimization is the root of all evil." In my experience, he was damn right about that.

  4. MSDN Archive says:

    Excellent points guys. Thanks for the comments.