예외가 발생하지 않을 때 try / catch 블록으로 인해 성능이 저하됩니까?


274

Microsoft 직원과의 코드 검토 과정에서 우리는 try{}블록 내부에서 큰 코드 섹션을 발견했습니다 . 그녀와 IT 담당자는 이것이 코드 성능에 영향을 줄 수 있다고 제안했습니다. 실제로 그들은 대부분의 코드가 try / catch 블록 외부에 있어야하며 중요한 섹션 만 확인해야한다고 제안했습니다. Microsoft 직원은 다가오는 백서에서 잘못된 try / catch 블록에 대해 경고한다고 덧붙였습니다.

주변을 둘러보고 최적화영향을 줄 수는 있지만 변수가 범위간에 공유되는 경우에만 적용되는 것으로 보입니다.

코드의 유지 보수에 대해 묻거나 올바른 예외를 처리하지 않습니다 (문제의 코드는 리팩토링이 필요합니다). 또한 흐름 제어에 예외를 사용하는 것에 대해서는 언급하지 않으며 대부분의 경우 분명히 잘못되었습니다. 이것들은 중요한 문제이지만 (일부는 더 중요합니다) 여기서 초점은 아닙니다.

예외가 발생 하지 않을 때 try / catch 블록은 어떻게 성능에 영향을 줍 니까?


147
"성능에 대한 정확성을 희생하는 사람은 그럴 자격이 없습니다."
Joel Coehoorn

16
즉, 성능을 위해 정확성을 항상 희생 할 필요는 없습니다.
Dan Davies Brackett

19
간단한 호기심은 어떻습니까?
사만다 브랜 햄

63
@Joel : 아마도 Kobi는 호기심으로 답을 알고 싶어 할 것입니다. 성능이 나아질 지 아는 것이 반드시 코드에 열중하는 것을 의미하지는 않습니다. 자체 지식을 추구하는 것이 좋은 일이 아닙니까?
LukeH

6
이 변경 여부를 알기에 좋은 알고리즘이 있습니다. 먼저 의미있는 고객 기반 성과 목표를 설정하십시오. 둘째, 코드를 정확하고 명확하게 작성하십시오. 셋째, 목표와 비교하여 테스트하십시오. 넷째, 목표를 달성하면 일찍 일을 시작하고 해변으로갑니다. 다섯째, 목표를 달성하지 못하면 프로파일 러를 사용하여 너무 느린 코드를 찾으십시오. 여섯째, 불필요한 예외 처리기 때문에 해당 코드가 너무 느리면 예외 처리기 만 제거하십시오. 그렇지 않은 경우 실제로 너무 느린 코드를 수정하십시오. 그런 다음 3 단계로 돌아가십시오.
Eric Lippert

답변:


203

확인해 봐.

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

산출:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

밀리 초 단위 :

449
416

새로운 코드 :

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

새로운 결과 :

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334

24
JIT 컴파일이 전자에 영향을 미치지 않았는지 확인하기 위해 역순으로 시도 할 수 있습니까?
JoshJordan

28
이와 같은 프로그램은 예외 처리의 영향을 테스트하기에 좋은 후보 인 것처럼 보이지 않으며, 일반적인 try {} catch {} 블록에서 진행되는 작업의 대부분이 최적화 될 것입니다. 나는 점심을 먹으러 나갔을지도 모른다 ...
LorenVS

30
이것은 디버그 빌드입니다. JIT는이를 최적화하지 않습니다.
벤 M

7
전혀 사실이 아닙니다. 생각해보십시오. 루프에서 몇 번이나 시도를 사용합니까? 대부분의 경우 try.c에서 루프를 사용할 것입니다
Athiwat Chunlakhan

9
정말? "예외가 발생하지 않을 때 try / catch 블록은 어떻게 성능에 영향을 줍니까?"
벤 M

105

시도 / 캐치과 시도 / 캐치하지 않고 모든 통계를 본 후, 호기심보고 저를 강제 뒤의 두 경우에 생성되는 것을 볼 수 있습니다. 코드는 다음과 같습니다.

씨#:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL :

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

씨#:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL :

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

나는 IL의 전문가는 아니지만 로컬 예외 객체가 네 번째 줄에 생성 된 것을 볼 수 있습니다. 그 .locals init ([0] class [mscorlib]System.Exception ex)후 17 줄까지 try / catch가없는 메소드와 거의 동일 IL_0021: leave.s IL_002f합니다. 예외가 발생하면 컨트롤이 줄로 건너 IL_0025: ldloc.0뛰고 그렇지 않으면 레이블로 건너 뛰고 IL_002d: leave.s IL_002f함수가 반환됩니다.

예외가 발생하지 않으면 예외 객체 보유하고 점프 명령 을 보유하기 위해 로컬 변수를 작성하는 것이 오버 헤드라고 안전하게 가정 할 수 있습니다 .


33
IL에는 C #에서와 같은 표기법으로 try / catch 블록이 포함되어 있으므로 실제로 try / catch가 얼마나 많은 오버 헤드를 의미하는지는 보여주지 않습니다! IL이 훨씬 더 추가하지 않는다고해서 컴파일 된 어셈블리 코드에 추가되지 않은 것과 같은 의미는 아닙니다. IL은 모든 .NET 언어의 일반적인 표현입니다. 기계 코드가 아닙니다!
awe

64

아니요. try / finally 블록이 사소한 최적화로 인해 실제로 측정에 영향을 줄 수있는 경우, 우선 .NET을 사용하지 않아야합니다.


10
우리의 목록에있는 다른 항목과 비교할 때이 점은 아주 적어야합니다. 기본 언어 기능이 올바르게 작동하고 신뢰할 수있는 기능 (SQL, 인덱스, 알고리즘)을 최적화해야합니다.
Kobi

3
타이트한 루프 메이트를 생각하십시오. 게임 서버의 소켓 데이터 스트림에서 객체를 읽고 역 직렬화하고 가능한 많이 압착하려고하는 루프를 예로들 수 있습니다. 따라서 바이너리 포맷터 대신 객체 직렬화를위한 MessagePack을 사용하고 바이트 배열 등을 생성하는 대신 ArrayPool <byte>를 사용하십시오. 컴파일러는 일부 최적화를 건너 뛰고 예외 변수는 Gen0 GC로 이동합니다. 내가 말하는 것은 모든 것이 영향을 미치는 "일부"시나리오가 있다는 것입니다.
tcwicks

35

.NET 예외 모델에 대한 포괄적 인 설명.

Rico Mariani의 성능 팁 : 예외 비용 : 던질 때와 던지지 않을 때

첫 번째 종류의 비용은 코드에서 예외 처리를하는 정적 비용입니다. 관리되는 예외는 실제로 여기에서 비교적 잘 수행되므로 정적 비용이 C ++에서보다 훨씬 저렴할 수 있습니다. 왜 이런거야? 정적 비용은 실제로 두 종류의 장소에서 발생합니다. 첫째, try / finally / catch / throw의 실제 사이트는 해당 구문에 대한 코드가 있습니다. 둘째, 관리되지 않는 코드에는 예외가 발생할 경우 파괴해야하는 모든 객체를 추적하는 것과 관련된 은폐 비용이 있습니다. 상당한 정리 논리가 있어야하며 몰래하는 부분은

드미트리 자 슬라브스키 :

Chris Brumme의 메모에 따르면 : 캐치가있을 때 JIT가 일부 최적화를 수행하지 않는다는 사실과 관련된 비용이 있습니다.


1
C ++에 관한 것은 표준 라이브러리의 매우 큰 덩어리가 예외를 throw한다는 것입니다. 그들에 대한 선택은 없습니다. 일종의 예외 정책을 사용하여 객체를 설계해야하며, 일단 스텔스 비용이 더 이상없는 경우.
David Thornley

Rico Mariani의 주장은 네이티브 C ++에서는 완전히 잘못되었습니다. "정적 비용은 C ++에서 말하는 것보다 훨씬 낮을 수 있습니다"-이것은 사실이 아닙니다. 그러나 기사가 작성된 2003 년 예외 메커니즘 디자인이 무엇인지 잘 모르겠습니다. C ++ 시도 / 캐치 블록 수와 위치에 관계없이 예외가 발생 하지 않을전혀 비용들지 않습니다 .
BJovke

1
@BJovke C ++ "제로 비용 예외 처리"는 예외가 발생하지 않을 때 런타임 비용이 들지 않는다는 것을 의미하지만 예외에 대한 소멸자를 호출하는 모든 정리 코드로 인해 여전히 큰 코드 크기 비용이 있습니다. 또한 일반 코드 경로에 예외 별 코드가 생성되지 않지만 예외 가능성으로 인해 여전히 최적화 프로그램이 제한되므로 (예 : 예외가 필요한 경우 필요한 항목) 비용은 실제로 0이 아닙니다. 어딘가에-> 값을 덜 공격적으로 버릴 수 있습니다-> 덜 효율적인 레지스터 할당)
Daniel

24

이 예에서는 Ben M 과 구조가 다릅니다 . 내부 for루프 내부의 오버 헤드가 확장 되어 두 경우를 잘 비교하지 못하게됩니다.

다음은 검사 할 전체 코드 (변수 선언 포함)가 Try / Catch 블록 내에있는 경우 비교하기에 더 정확합니다.

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Ben M 의 원래 테스트 코드를 실행했을 때 디버그 구성과 릴리스 구성에 차이가 있음을 알았습니다.

이 버전에서는 디버그 버전 (실제로는 다른 버전보다)에 차이가 있었지만 릴리스 버전에는 차이가 없었습니다.

결론 :
이 테스트를 통해 Try / Catch 성능에 약간의 영향을 미친다고 말할 수 있습니다.

편집 :
루프 값을 10000000에서 1000000000으로 늘리려 고했는데 Release에서 다시 실행하여 릴리스에서 약간의 차이점을 얻었습니다. 결과는 다음과 같습니다.

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

결과가 결과가 아님을 알 수 있습니다. 경우에 따라 Try / Catch를 사용하는 버전이 실제로 더 빠릅니다!


1
나도 이것을 알아 차리고 때로는 try / catch를 사용하면 더 빠릅니다. 벤의 답변에 대해 언급했습니다. 그러나 유권자 24 명과는 달리 이런 종류의 벤치마킹은 마음에 들지 않지만 좋은 표시라고 생각하지 않습니다. 이 경우 코드가 더 빠르지 만 항상 그렇습니까?
Kobi

5
이것이 기계가 다양한 다른 작업을 동시에 수행하고 있다는 것을 증명하지 않습니까? 경과 시간은 결코 좋은 방법이 아니며, 경과 시간이 아닌 프로세서 시간을 기록하는 프로파일 러를 사용해야합니다.
콜린 데스몬드

2
@ Kobi : 프로그램이 다른 프로그램보다 빠르다는 증거로 게시하려는 경우 이것이 벤치마킹하는 가장 좋은 방법은 아니지만 개발자에게 한 방법이 다른 방법보다 우수하다는 표시를 줄 수 있음에 동의합니다 . 이 경우 차이 (적어도 릴리스 구성의 경우)는 무시할 수 있다고 말할 수 있습니다.
awe

1
당신은 try/catch여기서 타이밍하지 않습니다 . 10M 루프에 대해 12 try / catch 입력 임계 섹션의 타이밍을 지정하고 있습니다. 루프의 노이즈는 시도 / 캐치의 영향을 근절합니다. 대신 try / catch를 타이트 루프 안에 넣고 비교 / 제외하면 try / catch 비용이 발생합니다. (의문의 여지없이, 그러한 코딩은 일반적으로 좋은 습관은 아니지만 구문의 오버 헤드 시간을 원한다면 그렇게하는 것입니다). 오늘날 BenchmarkDotNet은 안정적인 실행 타이밍을위한 도구입니다.
아벨

15

나는 try..catch단단한 루프에서 실제 영향을 테스트했으며 , 정상적인 상황에서 성능 문제가 되기에는 너무 작습니다.

루프가 거의 작동하지 않으면 (내 테스트에서 x++), 예외 처리의 영향을 측정 할 수 있습니다. 예외 처리 루프는 실행하는 데 약 10 배가 더 걸렸습니다.

루프가 실제 작업을 수행하는 경우 (내 테스트에서 Int32.Parse 메서드라고 함) 예외 처리에 영향을 거의 미치지 않아 측정 할 수 없습니다. 루프 순서를 바꾸어 훨씬 더 큰 차이를 얻었습니다 ...


11

캐치 블록은 성능에 미미한 영향을 미치지 만 예외는 상당히 큰 조정이 될 수 있습니다. 동료가 혼란 스러웠을 수 있습니다.


8

try / catch는 성능에 영향을 미칩니다.

그러나 큰 영향은 아닙니다. try / catch의 복잡도는 루프에 배치 된 경우를 제외하고 간단한 할당과 마찬가지로 일반적으로 O (1)입니다. 따라서 현명하게 사용해야합니다.

다음 은 try / catch 성능에 대한 참조입니다 (복잡성을 설명하지는 않지만 내포되어 있음). 더 적은 예외 처리 섹션을 살펴보십시오


3
복잡도는 O (1)이므로 그다지 큰 의미는 없습니다. 예를 들어 try-catch (또는 루프를 언급 함)로 매우 자주 호출되는 코드 섹션을 갖추면 O (1)이 끝에 측정 가능한 숫자까지 더해질 수 있습니다.
Csaba Toth

6

이론적으로 try / catch 블록은 실제로 예외가 발생하지 않는 한 코드 동작에 영향을 미치지 않습니다. 그러나 try / catch 블록의 존재가 큰 영향을 미칠 수있는 드문 상황이 있지만 그 효과가 눈에 띄는 드물지만 거의 모호하지 않은 경우도 있습니다. 그 이유는 다음과 같은 주어진 코드 때문입니다.

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

컴파일러는 statement2가 statement3보다 먼저 실행된다는 사실을 기반으로 statement1을 최적화 할 수 있습니다. 컴파일러가 thing1에 부작용이없고 thing2가 실제로 x를 사용하지 않는다는 것을 인식 할 수 있으면 thing1을 완전히 생략 할 수 있습니다. [이 경우와 같이] thing1이 비싸면 주요 최적화가 될 수 있지만, thing1이 비싸는 경우도 컴파일러가 최적화 할 가능성이 가장 낮습니다. 코드가 변경되었다고 가정하십시오.

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

이제 statement3이 statement2를 실행하지 않고 실행할 수있는 일련의 이벤트가 있습니다. 의 코드에 아무것도 경우에도 thing2예외를 던질 수 없었다, 다른 스레드가 사용할 수 있다는 가능할 것이다 Interlocked.CompareExchange통지에 q지워졌습니다 그것은으로 설정 Thread.ResetAbort, 다음을 수행 할 Thread.Abort()문장 2가 그 값을 작성하기 전에 x. 그런 다음이 catch실행됩니다 Thread.ResetAbort()[위임을 통해 q실행이 statement3을 계속 할 수 있도록]. 이러한 일련의 이벤트는 당연히 불가능할 수 있지만, 그러한 불가능한 이벤트가 발생할 때에도 사양에 따라 작동하는 코드를 생성하려면 컴파일러가 필요합니다.

일반적으로 컴파일러는 복잡한 코드보다 간단한 코드 비트를 생략 할 가능성이 훨씬 높으므로 예외가 발생하지 않는 경우 try / catch가 성능에 큰 영향을 줄 수있는 경우는 거의 없습니다. 여전히 try / catch 블록이 있으면 try / catch의 경우 최적화를 방해하여 코드 실행 속도가 빨라질 수있는 상황이 있습니다.


5

" 예방이 처리하는 것보다 낫지 만 "성능과 효율성의 관점에서 우리는 사전 변이보다 try-catch를 선택할 수 있습니다. 아래 코드를 고려하십시오.

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

결과는 다음과 같습니다.

With Checking: 20367
With Exception: 13998

4

예외가 발생하지 않을 때 try / catch 블록의 작동 방식 및 일부 구현의 오버 헤드가 높고 오버 헤드가없는 방법 에 대한 설명은 try / catch 구현에 대한 설명을 참조하십시오 . 특히 Windows 32 비트 구현은 오버 헤드가 높고 64 비트 구현은 그렇지 않다고 생각합니다.


내가 설명한 것은 예외 구현에 대한 두 가지 다른 접근 방식입니다. 이 접근 방식은 C ++ 및 C #과 관리 / 관리되지 않는 코드에 동일하게 적용됩니다. MS가 C #을 위해 선택한 것은 정확히 알지 못하지만 MS가 제공하는 컴퓨터 수준 응용 프로그램의 예외 처리 아키텍처는 더 빠른 체계를 사용합니다. 64 비트에 대한 C # 구현이 사용하지 않으면 약간 놀랐습니다.
Ira Baxter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.