Friday, October 11, 2013

Try Catch Finally Performance

My brother asked me an interesting question yesterday about how best to handle exceptions in his C# code.  (I think) I answered his question and then we moved to the topic of performance degradation due to the over-usage of try/catch|finally blocks.  I read a blog post by Peter Ritchie a few weeks ago about how the compiler treats code (specifically variables) within the construct, but I wanted to do some more digging.  If you're interested in what Peter had to say you can find his posts here and here.

My main line of thought was that those blog posts are pretty old and specifically discuss the x86 compiler.  Peter provides a pretty trivial bit of sample code to demonstrate his point, but by virtue of being trivial it isn't all that helpful.  I found a pretty good discussion about try/catch|finally performance here on StackOverflow, but again I felt like the example provided wasn't really indicative of a real world scenario.  My biggest issues with both sets of code is that they weren't performing any operations that might actually throw an exception.

What I mean is, we can talk all day about try/catch|finally performance and give those types of examples, but in the end that's not how we'd really use a try/catch|finally block.  I'll be honest with you: my sample isn't much better, but I think it is better.

Since my brother's question was specifically about how to handle an exception when File.Move is the bit being tried, I decided to go with that.  I wrote a simple console application that moves a file from one directory to another.  There are five separate methods I used to perform the same operation.

Note: method is used here to describe a "way" of doing something, not an actual method as the term is used in C# and other languages

In the first method I put everything inside of a try block.  Variable declaration, initialization, file moving and counter incrementing.  In the second method I put only my file moving and counter incrementing code in the try block.  In the third method I put my file moving code in the try block and the counter incrementing code in the finally block.  In the fourth method I put my file moving code in the try block and moved the counter incrementing outside of the entire try/catch|finally construct.  In the fifth method I did not use a try/catch|finally block at all.

Note: if you look at the source code below you will see that there is a sixth File.Move operation; that is there solely to move the file back to the starting folder so the loop can repeat properly


   1:  var stopwatch1 = new Stopwatch();
   2:  var stopwatch2 = new Stopwatch();
   3:  var stopwatch3 = new Stopwatch();
   4:  var stopwatch4 = new Stopwatch();
   5:  var stopwatch5 = new Stopwatch();
   6:   
   7:  var counterForNoReason = 0;
   8:   
   9:  for (var i = 0; i < 100000; i++)
  10:  {
  11:      stopwatch1.Start();
  12:      try
  13:      {
  14:          var originalPath = @"C:\Temp\";
  15:          var newPath = @"C:\Temp\TryCatchPerformanceFolder\";
  16:          var fileName = @"TryCatchPerformanceFile.txt";
  17:   
  18:          File.Move(originalPath + fileName, newPath + fileName);
  19:   
  20:          counterForNoReason++;
  21:      }
  22:      catch (Exception e)
  23:      {
  24:          Console.WriteLine(e.Message);
  25:      }
  26:      stopwatch1.Stop();
  27:   
  28:      stopwatch2.Start();
  29:   
  30:      var originalPath2 = @"C:\Temp\TryCatchPerformanceFolder\";
  31:      var newPath2 = @"C:\Temp\";
  32:      var fileName2 = @"TryCatchPerformanceFile.txt";
  34:      try
  35:      {
  36:          File.Move(originalPath2 + fileName2, newPath2 + fileName2);
  37:          counterForNoReason++;
  38:      }
  39:      catch (Exception e)
  40:      {
  41:          Console.WriteLine(e.Message);
  42:      }
  43:   
  44:      stopwatch2.Stop();
  45:   
  46:      stopwatch3.Start();
  47:   
  48:      var originalPath3 = @"C:\Temp\";
  49:      var newPath3 = @"C:\Temp\TryCatchPerformanceFolder\";
  50:      var fileName3 = @"TryCatchPerformanceFile.txt";
  51:   
  52:      try
  53:      {
  54:          File.Move(originalPath3 + fileName3, newPath3 + fileName3);
  55:      }
  56:      catch (Exception e)
  57:      {
  58:          Console.WriteLine(e.Message);
  59:      }
  60:      finally
  61:      {
  62:          counterForNoReason++;
  63:      }
  64:   
  65:      stopwatch3.Stop();
  66:   
  67:      stopwatch4.Start();
  68:   
  69:      var originalPath4 = @"C:\Temp\TryCatchPerformanceFolder\";
  70:      var newPath4 = @"C:\Temp\";
  71:      var fileName4 = @"TryCatchPerformanceFile.txt";
  72:   
  73:      try
  74:      {
  75:          File.Move(originalPath4 + fileName4, newPath4 + fileName4);
  76:      }
  77:      catch (Exception e)
  78:      {
  79:          Console.WriteLine(e.Message);
  80:      }
  81:   
  82:      counterForNoReason++;
  83:   
  84:      stopwatch4.Stop();
  85:   
  86:      stopwatch5.Start();
  87:   
  88:      var originalPath5 = @"C:\Temp\";
  89:      var newPath5 = @"C:\Temp\TryCatchPerformanceFolder\";
  90:      var fileName5 = @"TryCatchPerformanceFile.txt";
  91:   
  92:      File.Move(originalPath5 + fileName5, newPath5 + fileName5);
  93:   
  94:      stopwatch5.Stop();
  95:      counterForNoReason++;
  96:   
  97:      // move the file back to where it started so the loop can repeat
  98:      File.Move(originalPath4 + fileName4, newPath4 + fileName4);
  99:  }
 100:   
 101:  Console.WriteLine(@"Try\Catch done wrong: {0}", stopwatch1.ElapsedMilliseconds);
 102:  Console.WriteLine(@"Try\Catch done right: {0}", stopwatch2.ElapsedMilliseconds);
 103:  Console.WriteLine(@"Try\Catch done right with finally: {0}", stopwatch3.ElapsedMilliseconds);
 104:  Console.WriteLine(@"No Try\Catch at all (dangerous): {0}", stopwatch4.ElapsedMilliseconds);

So how did it all turn out?  Well, it turns out that the fastest way is to live dangerously.  Skipping the try/catch|finally construct entirely was the fastest way to achieve the File.Move operation.  Since that's not reasonable, I'll ignore those results (that was really just the control anyway).  I'll let the screenshot of the results tell the rest of the story:




So there you have it.  Definitive proof (yeah, sure) that the try/catch|finally block must be used responsibly.  Only include code that can fail inside of the try block or you'll risk sinking your program under an insurmountable load and kill performance.