code

C #에서 try / finally 오버 헤드?

codestyles 2020. 10. 28. 08:07
반응형

C #에서 try / finally 오버 헤드?


try/ catchtry/ catch/ 를 사용하는시기와 이유에 대한 많은 질문을 보았습니다 finally. 그리고 확실히 try/에 대한 사용 사례가 있다는 것을 알고 있습니다 finally(특히 using문이 구현 되는 방식이기 때문에 ).

try / catch 및 예외의 오버 헤드에 대한 질문도 보았습니다 .

그러나 내가 연결 한 질문은 단지 시험을 치르는 데 드는 오버 헤드에 대해서는 언급하지 않습니다.

try블록 에서 발생하는 예외가 없다고 가정 할 때, 블록 finally을 떠날 때 문이 실행 되도록하는 오버 헤드는 무엇입니까 try(때로는 함수에서 반환 됨)?

다시 말하지만, 나는 try/ finally, no catch, 예외 던지기에 대해서만 묻습니다 .

감사!

편집 : 좋아, 내 사용 사례를 좀 더 잘 보여 주려고합니다.

어떤 내가 사용한다 DoWithTryFinallyDoWithoutTryFinally?

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;
  }
}

이 경우는 리턴 포인트가 2 개뿐이기 때문에 지나치게 단순하지만, 4 개 또는 10 개 또는 100 개가 있다고 상상해보십시오.

어느 시점 에서 다음과 같은 이유로 try/ 를 사용하고 싶습니다 finally.

  • DRY 원칙을 준수하십시오 (특히 출구 지점 수가 증가함에 따라).
  • 내 내부 함수가 예외를 던지지 않는 것에 대해 잘못된 것으로 밝혀지면 this.Working이로 설정되어 있는지 확인하고 싶습니다 false.

따라서 가설 적으로 성능 문제, 유지 관리 및 DRY 원칙을 고려할 때 어떤 수의 출구 지점 (특히 모든 내부 예외가 포착되었다고 가정 할 수있는 경우 )에 대해 try/ 와 관련된 성능 불이익을 발생시키고 싶 finally습니까?

편집 # 2 : 이름 this.Workingthis.IsBusy. 죄송합니다. 이것이 다중 스레드라는 것을 잊었습니다 (실제로 하나의 스레드 만 메서드를 호출 할 수 있음). 다른 스레드는 개체가 작업을 수행하는지 확인하기 위해 폴링합니다. 반환 값은 작업이 예상대로 진행된 경우 단순히 성공 또는 실패입니다.


실제로 얻은 것을 보지 않겠습니까?

다음은 C #의 간단한 코드 청크입니다.

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

그리고 다음은 디버그 빌드의 결과 IL입니다.

.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
}

다음은 디버그에서 실행할 때 JIT에 의해 생성 된 어셈블리입니다.

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 

이제 시도와 최종 결과 및 반환을 주석 처리하면 JIT에서 거의 동일한 어셈블리를 얻습니다. 당신이 보게 될 차이점은 finally 블록으로의 점프와 finally가 실행 된 후 어디로 가야할지 알아내는 코드입니다. 그래서 당신은 아주 작은 차이에 대해 이야기하고 있습니다. 릴리스에서 finally 로의 점프는 최적화되어 있습니다. 중괄호는 nop 명령이므로 다음 명령으로의 점프가 될 것입니다. 이것은 또한 nop입니다. 이것은 쉬운 틈새 최적화입니다. pop eax와 jmp eax는 비슷하게 저렴합니다.

    {
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.


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.


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.


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.


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


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

참고URL : https://stackoverflow.com/questions/4106891/overhead-of-try-finally-in-c

반응형