Overhead of try/finally in C#?
Asked Answered
R

6

79

We've seen plenty of questions about when and why to use try/catch and try/catch/finally. And I know there's definitely a use case for try/finally (especially since it is the way the using statement is implemented).

We've also seen questions about the overhead of try/catch and exceptions.

The question I linked to, however, doesn't talk about the overhead of having JUST try-finally.

Assuming there are no exceptions from anything that happens in the try block, what's the overhead of making sure that the finally statements get executed on leaving the try block (sometimes by returning from the function)?

Again, I'm asking ONLY about try/finally, no catch, no throwing of exceptions.

Thanks!

EDIT: Okay, I'm going to try to show my use case a little better.

Which should I use, DoWithTryFinally or DoWithoutTryFinally?

public bool DoWithTryFinally()
{
  this.IsBusy = true;

  try
  {
    if (DoLongCheckThatWillNotThrowException())
    {
      this.DebugLogSuccess();
      return true;
    }
    else
    {
      this.ErrorLogFailure();
      return false;
    }
  }
  finally
  {
    this.IsBusy = false;
  }
}

public bool DoWithoutTryFinally()
{
  this.IsBusy = true;

  if (DoLongCheckThatWillNotThrowException())
  {
    this.DebugLogSuccess();

    this.IsBusy = false;
    return true;
  }
  else
  {
    this.ErrorLogFailure();

    this.IsBusy = false;
    return false;
  }
}

This case is overly simplistic because there are only two return points, but imagine if there were four... or ten... or a hundred.

At some point I would want to use try/finally for the following reasons:

  • Keep to DRY principles (especially as the number of exit points gets higher)
  • If it turns out that I'm wrong about my inner function not throwing an exception, then I want to make sure this.Working is set to false.

So hypothetically, given performance concerns, maintainability, and DRY principles, for what number of exit points (especially if I can assume that all inner exceptions are caught) do I want to incur whatever performance penalty is associated with try/finally?

EDIT #2: I changed the name of this.Working to this.IsBusy. Sorry, forgot to mention this is multithreaded (though only one thread will ever actually call the method); other threads will be polling to see if the object is doing its work. The return value is merely success or failure for if the work went as expected.

Redd answered 5/11, 2010 at 14:38 Comment(9)
I don't understand the question fully. You're still marshalling, you're just not catching the exception. The marshalling still has to occur tho, right?Vestibule
I think the rest of us understand the question. It's a good one.Cerate
@Cerate (+1) ~ I thought it was a good one too, I think I figured the hook: I guess that's the same as "what's the cost of insurance if nothing bad ever happens to me?" ... I figure the cost is going to occur if no exception gets thrown, but you might as well pay it anyways.Vestibule
if you have a hundred return points I think you should refactor :-)Racehorse
@drachenstern: Yes, you've basically got it. See my updated version for a better idea of what I'm going for.Redd
@Philipp: Obviously. :-) In my real use case, I'm actually comparing my code against someone else's and we both have about five return points-- two near the end for global success/failure check at the end of everything, but a couple near the middle for simple if checks that should end everything prematurely. I thought it was a good place for try/finally but I'm trying to see if I should worry about performance overhead.Redd
@Platinum Azure ~ In this particular case because you're (already handling exceptions in the internal method then either logging or continuing), then you should not use the try-finally block as it gains you nothing. But if you have the case that it could throw an exception, you're going to need it. Also, your code is a little off in the example. You're returning but using a flag? I think you meant to set the flag internally and return finally?Vestibule
@drachenstern: Sorry, I used bad variable names. The flag is different from the return value; I renamed it to show more closely what I meant. Hope that helps.Redd
@Platinum: If you already have code to compare against, why don't you just measure the difference? For what it's worth, the difference is likely to be insignificant.Found
T
107

Why not look at what you actually get?

Here is a simple chunk of code in C#:

    static void Main(string[] args)
    {
        int i = 0;
        try
        {
            i = 1;
            Console.WriteLine(i);
            return;
        }
        finally
        {
            Console.WriteLine("finally.");
        }
    }

And here is the resulting IL in the debug build:

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init ([0] int32 i)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.0 
    L_0003: nop 
    L_0004: ldc.i4.1 
    L_0005: stloc.0 
    L_0006: ldloc.0 // here's the WriteLine of i 
    L_0007: call void [mscorlib]System.Console::WriteLine(int32)
    L_000c: nop 
    L_000d: leave.s L_001d // this is the flavor of branch that triggers finally
    L_000f: nop 
    L_0010: ldstr "finally."
    L_0015: call void [mscorlib]System.Console::WriteLine(string)
    L_001a: nop 
    L_001b: nop 
    L_001c: endfinally 
    L_001d: nop 
    L_001e: ret 
    .try L_0003 to L_000f finally handler L_000f to L_001d
}

and here's the assembly generated by the JIT when running in debug:

00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  sub         esp,34h 
00000009  mov         esi,ecx 
0000000b  lea         edi,[ebp-38h] 
0000000e  mov         ecx,0Bh 
00000013  xor         eax,eax 
00000015  rep stos    dword ptr es:[edi] 
00000017  mov         ecx,esi 
00000019  xor         eax,eax 
0000001b  mov         dword ptr [ebp-1Ch],eax 
0000001e  mov         dword ptr [ebp-3Ch],ecx 
00000021  cmp         dword ptr ds:[00288D34h],0 
00000028  je          0000002F 
0000002a  call        59439E21 
0000002f  xor         edx,edx 
00000031  mov         dword ptr [ebp-40h],edx 
00000034  nop 
        int i = 0;
00000035  xor         edx,edx 
00000037  mov         dword ptr [ebp-40h],edx 
        try
        {
0000003a  nop 
            i = 1;
0000003b  mov         dword ptr [ebp-40h],1 
            Console.WriteLine(i);
00000042  mov         ecx,dword ptr [ebp-40h] 
00000045  call        58DB2EA0 
0000004a  nop 
            return;
0000004b  nop 
0000004c  mov         dword ptr [ebp-20h],0 
00000053  mov         dword ptr [ebp-1Ch],0FCh 
0000005a  push        4E1584h 
0000005f  jmp         00000061 
        }
        finally
        {
00000061  nop 
            Console.WriteLine("finally.");
00000062  mov         ecx,dword ptr ds:[036E2088h] 
00000068  call        58DB2DB4 
0000006d  nop 
        }
0000006e  nop 
0000006f  pop         eax 
00000070  jmp         eax 
00000072  nop 
    }
00000073  nop 
00000074  lea         esp,[ebp-0Ch] 
00000077  pop         ebx 
00000078  pop         esi 
00000079  pop         edi 
0000007a  pop         ebp 
0000007b  ret 
0000007c  mov         dword ptr [ebp-1Ch],0 
00000083  jmp         00000072 

Now, if I comment out the try and finally and the return, I get nearly identical assembly from the JIT. The differences you'll see are a jump into the finally block and some code to figure out where to go after the finally is executed. So you're talking about TINY differences. In release, the jump into the finally will get optimized out - braces are nop instructions, so this would become a jump to the next instruction, which is also a nop - that's an easy peephole optimization. The pop eax and then jmp eax is similarly cheap.

    {
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  sub         esp,34h 
00000009  mov         esi,ecx 
0000000b  lea         edi,[ebp-38h] 
0000000e  mov         ecx,0Bh 
00000013  xor         eax,eax 
00000015  rep stos    dword ptr es:[edi] 
00000017  mov         ecx,esi 
00000019  xor         eax,eax 
0000001b  mov         dword ptr [ebp-1Ch],eax 
0000001e  mov         dword ptr [ebp-3Ch],ecx 
00000021  cmp         dword ptr ds:[00198D34h],0 
00000028  je          0000002F 
0000002a  call        59549E21 
0000002f  xor         edx,edx 
00000031  mov         dword ptr [ebp-40h],edx 
00000034  nop 
        int i = 0;
00000035  xor         edx,edx 
00000037  mov         dword ptr [ebp-40h],edx 
        //try
        //{
            i = 1;
0000003a  mov         dword ptr [ebp-40h],1 
            Console.WriteLine(i);
00000041  mov         ecx,dword ptr [ebp-40h] 
00000044  call        58EC2EA0 
00000049  nop 
        //    return;
        //}
        //finally
        //{
            Console.WriteLine("finally.");
0000004a  mov         ecx,dword ptr ds:[034C2088h] 
00000050  call        58EC2DB4 
00000055  nop 
        //}
    }
00000056  nop 
00000057  lea         esp,[ebp-0Ch] 
0000005a  pop         ebx 
0000005b  pop         esi 
0000005c  pop         edi 
0000005d  pop         ebp 
0000005e  ret 

So you're talking very, very tiny costs for try/finally. There are very few problem domains where this matters. If you're doing something like memcpy and put a try/finally around each byte being copied and then proceed to copy hundreds of MB of data, I could see that being an issue, but in most usage? Negligible.

Tootsie answered 5/11, 2010 at 15:23 Comment(1)
I doubt it would optimize the micro peep ahead. That's just a nearly useless optimization, unless the instruction fetcher could realize the optimization and skip ahead two calls.Vestibule
B
56

So let's assume there's an overhead. Are you going to stop using finally then? Hopefully not.

IMO performance metrics are only relevant if you can choose between different options. I cannot see how you can get the semantic of finally without using finally.

Balkhash answered 5/11, 2010 at 14:42 Comment(10)
A great additional point; If you need finally, you need it!Respect
Edited my question. I'm specifically asking about using it when I'm not really worried about exceptions... ergo I can choose between different options (per your point in the answer). Please kindly update with your thoughts.Redd
You sometimes can move them further out in a loop. For example the Delphi equivalent of try/catch or try/finally is pretty expensive so I had to do this a few times.Gimlet
To add to my previous point-- while this is a good piece of advice in general, it doesn't answer my question and you can look at the updated version to see why. Thanks :-)Redd
The two methods in your updated question don't really compare as they behave differently in the case of an error. I'm aware that the method is not supposed to throw an exception, but you may still encounter exceptions such as OutOfMemoryException, ThreadAbortException and a couple of others. In that case the methods will behave differently.Balkhash
Okay, that's a point for consideration. I mentioned that. Now what about the other part-- is it worth using try/finally for DRY purposes at a certain point?Redd
It isn't a matter of not using Try/Finally, but rather where to place them. Inside a tightly running loop, or outside? For the most part it may not matter. But if performance is critical, and you can accomplish the same functionality by placing them outside the loop, why wouldn't you want to? This doesn't change schematics, nor remove using try/finally completely. It just produces better thought-out code.Ashtoreth
@BrianRasmussen do you have a problem with a performance question being asked? "I cannot see how you can get the semantic of finally without using finally" - who asked that question? The real question was: "Overhead of try/finally in C#?" Pretty innocent, unless we must assume performance related questions, even on simple but low level issues like this, must be frowned upon.Tyrant
@NicholasPetersen No, I don't have a problem with discussing performance problems at all. My point was that if there is only one way to achieve a given result then discussing the performance of that is only useful to the folks implementing the feature.Balkhash
I checked your profile Brian ... so you wrote a book with the words "High Performance" in it, okay, glad to hear! amazon.com/dp/0735682631/?tag=stackoverfl08-20Tyrant
R
28

try/finally is very lightweight. Actually, so is try/catch/finally as long as no exception is thrown.

I had a quick profile app I did a while ago to test it out; in a tight loop, it really added nothing at all to execution time.

I'd post it again, but it was really simple; just run a tight loop doing something, with a try/catch/finally that does not throw any exceptions inside the loop, and time the results against a version without the try/catch/finally.

Respect answered 5/11, 2010 at 14:42 Comment(1)
Without code/binaries this doesn't make sense. The compiler could just optimize it all into /mono/dev/null.Inebriant
T
14

Let's actually put some benchmark numbers to this. What this benchmark shows is that, indeed, the time of having a try/finally is about as small as the overhead of a call to an empty function (probably better put: "a jump to the next instruction" as the IL expert stated it above).

            static void RunTryFinallyTest()
            {
                int cnt = 10000000;

                Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                Console.WriteLine(TryFinallyBenchmarker(cnt, false));
                Console.WriteLine(TryFinallyBenchmarker(cnt, false));

                Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                Console.WriteLine(TryFinallyBenchmarker(cnt, true));
                Console.WriteLine(TryFinallyBenchmarker(cnt, true));

                Console.ReadKey();
            }

            static double TryFinallyBenchmarker(int count, bool useTryFinally)
            {
                int over1 = count + 1;
                int over2 = count + 2;

                if (!useTryFinally)
                {
                    var sw = Stopwatch.StartNew();
                    for (int i = 0; i < count; i++)
                    {
                        // do something so optimization doesn't ignore whole loop. 
                        if (i == over1) throw new Exception();
                        if (i == over2) throw new Exception();
                    }
                    return sw.Elapsed.TotalMilliseconds;
                }
                else
                {
                    var sw = Stopwatch.StartNew();
                    for (int i = 0; i < count; i++)
                    {
                        // do same things, just second in the finally, make sure finally is 
                        // actually doing something and not optimized out
                        try
                        {
                            if (i == over1) throw new Exception();
                        } finally
                        {
                            if (i == over2) throw new Exception();
                        }
                    }
                    return sw.Elapsed.TotalMilliseconds;
                }
            }

Result: 33,33,32,35,32 63,64,69,66,66 (milliseconds, make sure you have code optimization on)

So about 33 milliseconds overhead for the try/finally in 10 million loops.

Per try/finally then, we are talking 0.033/10000000 =

3.3 nanoseconds or 3.3 billionth of a second overhead of a try/finally.

Tyrant answered 5/4, 2012 at 23:8 Comment(0)
M
6

What Andrew Barber said. The actual TRY/CATCH statements add no/negligible overhead unless an exception is thrown. There's nothing really special about finally. Your code just always jumps to finally after the code in the try+catch statements are done

Maramarabel answered 5/11, 2010 at 15:17 Comment(0)
E
6

In lower levels finally is just as expensive as an else if the condition not met. It is actually a jump in assembler (IL).

Evonneevonymus answered 5/11, 2010 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.