Friday, 21 June 2013

Why you should check event delegates for null

The other day I came across some code that was raising an event like this:
    public class Dummy
    {
        public event EventHandler SomethingHappened;
        public void RaiseEvent()
        {
            try
            {
                SomethingHappened(this, EventArgs.Empty);
            }
            catch { }
        }
    }
As opposed to the recognised pattern of
    public class Dummy
    {
        public event EventHandler SomethingHappened;
        public void RaiseEvent()
        {
            if (SomethingHappened != null)
            {
                SomethingHappened(this, EventArgs.Empty);
            }
        }
    }
Chatting to a friend at the pub, I mentioned about this code and the fact that the code was relying on exception handling to control the flow, and he mentioned that it would be a lot slower if there wasn't an event handler defined. I knew it would be slower to create, throw and catch a NullReferenceException than to perform a null check, but I hadn't realised just how much slower.

To prove it, I created a class that contains a single event, and exposed two methods to raise the event - one with a try/catch block and one with a null check
    public class Dummy
    {
        public event EventHandler SomethingHappened;

        public void RaiseWithNullCheck()
        {
            if (SomethingHappened != null)
            {
                SomethingHappened(this, EventArgs.Empty);
            }
        }

        public void RaiseInTryCatch()
        {
            try
            {
                SomethingHappened(this, EventArgs.Empty);
            }
            catch { }
        }
    }
...and a console app to repeatedly run the methods with no event handler defined
        public static void Main(string[] args)
        {
            var dummy = new Dummy();
            var stopwatch = Stopwatch.StartNew();
            var iterations = 10000;

            for (int i = 0; i < iterations; ++i)
            {
                dummy.RaiseWithNullCheck();
            }
            stopwatch.Stop();
            Console.WriteLine("Null check: {0}", stopwatch.ElapsedMilliseconds);

            stopwatch.Restart();
            for (int i = 0; i < iterations; ++i)
            {
                dummy.RaiseInTryCatch();
            }
            stopwatch.Stop();
            Console.WriteLine("Try catch: {0}", stopwatch.ElapsedMilliseconds);

            Console.ReadLine();
        }
This is a screenshot of the output - note the values are milliseconds
That's right. Sub-millisecond for the 10,000 null checks, and over 36 seconds for the 10,000 exceptions.


The remaining question is "what's the difference in timing if there is a handler defined for the event?" Obviously in this case the try/catch will be quicker because there's no overhead of checking for null before invoking the event handler. I had to massage the code a little though, before the difference was noticeable:
        public static void Main(string[] args)
        {
            var dummy = new Dummy();
            // Add an empty event handler
            dummy.SomethingHappened += (s, e) => { };

            var stopwatch = Stopwatch.StartNew();
            var iterations = 10000;

            for (int i = 0; i < iterations; ++i)
            {
                dummy.RaiseWithNullCheck();
            }
            stopwatch.Stop();

            // report in ticks, not milliseconds
            Console.WriteLine("Null check: {0} ticks", stopwatch.ElapsedTicks);

            stopwatch.Restart();
            for (int i = 0; i < iterations; ++i)
            {
                dummy.RaiseInTryCatch();
            }
            stopwatch.Stop();
            Console.WriteLine("Try catch: {0} ticks", stopwatch.ElapsedTicks);

            Console.ReadLine();
        }
Here's a typical output for this one (the timings vary a little between runs). Note that this is in ticks not milliseconds (a tick being 1/10,000 of a millisecond).
Given the tiny numbers when the event handler is defined, there isn't a good reason to rely on a try/catch when a simple null check will do.

No comments:

Post a Comment