Do try/catch blocks hurt performance when exceptions are not thrown?

Viewed 102033

During a code review with a Microsoft employee we came across a large section of code inside a try{} block. She and an IT representative suggested this can have effects on performance of the code. In fact, they suggested most of the code should be outside of try/catch blocks, and that only important sections should be checked. The Microsoft employee added and said an upcoming white paper warns against incorrect try/catch blocks.

I've looked around and found it can affect optimizations, but it seems to only apply when a variable is shared between scopes.

I'm not asking about maintainability of the code, or even handling the right exceptions (the code in question needs re-factoring, no doubt). I'm also not referring to using exceptions for flow control, this is clearly wrong in most cases. Those are important issues (some are more important), but not the focus here.

How do try/catch blocks affect performance when exceptions are not thrown?

13 Answers

Though "Prevention is better than handling", in the perspective of performance and efficiency we could chose the try-catch over the pre-varication. Consider the below code:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

Here is the result:

With Checking:  20367
With Exception: 13998

Yes, try/catch will "hurt" performance (everything is relative). Not much in terms of wasted CPU cycles, but there are other important aspects to consider:

  • Code size
  • Method inlining

Benchmark

First, let's check the speed using some sophisticated tools (i.e. BenchmarkDotNet). Compiled as Release (AnyCPU), run on x64 machine. I would say there is no difference, even though the test will indeed tell us that NoTryCatch() is a tiny, tiny bit faster:

|            Method |   N |     Mean |     Error |    StdDev |
|------------------ |---- |---------:|----------:|----------:|
|        NoTryCatch | 0.5 | 3.770 ns | 0.0492 ns | 0.0411 ns |
|      WithTryCatch | 0.5 | 4.060 ns | 0.0410 ns | 0.0384 ns |
| WithTryCatchThrow | 0.5 | 3.924 ns | 0.0994 ns | 0.0881 ns |

Analysis

Some additional notes.

|            Method | Code size | Inlineable |
|------------------ |---------- |-----------:|
|        NoTryCatch |        12 |        yes |
|      WithTryCatch |        18 |          ? |
| WithTryCatchThrow |        18 |         no |

Code size NoTryCatch() yields 12 bytes in code whereas a try/catch adds another 6 bytes. Also, whenever writing a try/catch you will most likely have one or more throw new Exception("Message", ex) statements, further "bloating" the code.

The most important thing here though is code inlining. In .NET the mere existence of the throw keyword implies that the method will never be inlined by the compiler (implying slower code, but also less footprint). I recently tested this fact thoroughly, so it still seems valid in .NET Core. Not sure if try/catch follows the same rule. TODO: Verify!

Complete test code

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace TryCatchPerformance
{
    public class TryCatch
    {
        [Params(0.5)]
        public double N { get; set; }

        [Benchmark]
        public void NoTryCatch() => Math.Sin(N);

        [Benchmark]
        public void WithTryCatch()
        {
            try
            {
                Math.Sin(N);
            }
            catch
            {
            }
        }

        [Benchmark]
        public void WithTryCatchThrow()
        {
            try
            {
                Math.Sin(N);
            }
            catch (Exception ex)
            {
                throw;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<TryCatch>();
        }
    }
}

I tested a deep try-catch.

        static void TryCatch(int level, int max)
        {
            try
            {
                if (level < max) TryCatch(level + 1, max);
            }
            catch
            { }
        }
        static void NoTryCatch(int level, int max)
        {
            if (level < max) NoTryCatch(level + 1, max);
        }
        static void Main(string[] args)
        {
            var s = new Stopwatch();
            const int max = 10000;
            s.Start();
            TryCatch(0, max);
            s.Stop();
            Console.WriteLine("try-catch " + s.Elapsed);
            s.Restart();
            NoTryCatch(0, max);
            s.Stop();
            Console.WriteLine("no try-catch " + s.Elapsed);
        }

The result:

try-catch 00:00:00.0008528
no try-catch 00:00:00.0002422
Related