Overhead of try/finally in C#?

后端 未结 6 1226
有刺的猬
有刺的猬 2020-12-07 23:47

We\'ve seen plenty of questions about when and why to use try/catch and try/catch/finally. And I know there\

相关标签:
6条回答
  • 2020-12-08 00:18

    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.

    0 讨论(0)
  • 2020-12-08 00:18

    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.

    0 讨论(0)
  • 2020-12-08 00:25

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

    0 讨论(0)
  • 2020-12-08 00:30

    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.

    0 讨论(0)
  • 2020-12-08 00:31

    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

    0 讨论(0)
  • 2020-12-08 00:32

    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.

    0 讨论(0)
提交回复
热议问题