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

  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.
  • Anonymous
    February 14, 2008
    The comment has been removed
  • Anonymous
    May 24, 2008
    Excellent points guys. Thanks for the comments.