언제 예외를 던지거나하지 말아야하는지에 대한 토론을 원하지 않습니다. 간단한 문제를 해결하고 싶습니다. 99 %의 시간은 예외를 던지지 않는다는 주장이 느리게 진행되는 반면 다른 쪽은 벤치 마크 테스트를 통해 속도가 문제가 아니라고 주장합니다. 한 쪽 또는 다른 쪽과 관련된 수많은 블로그, 기사 및 게시물을 읽었습니다. 그래서 어느 것입니까?
언제 예외를 던지거나하지 말아야하는지에 대한 토론을 원하지 않습니다. 간단한 문제를 해결하고 싶습니다. 99 %의 시간은 예외를 던지지 않는다는 주장이 느리게 진행되는 반면 다른 쪽은 벤치 마크 테스트를 통해 속도가 문제가 아니라고 주장합니다. 한 쪽 또는 다른 쪽과 관련된 수많은 블로그, 기사 및 게시물을 읽었습니다. 그래서 어느 것입니까?
답변:
나는 "느리게하지 않는"편에있다.보다 정확하게는 "정상적으로 사용하는 것을 피할 가치가있을 정도로 느리지 않다"고 말했다. 나는 이것에 관한 두 개의 짧은 기사 를 썼습니다 . 벤치 마크 측면에 대한 비판이 있습니다. 대부분 "실제로 더 많은 스택이 필요하므로 캐시를 날려 버릴 것입니다."-오류 코드를 사용하여 스택을 처리하는 방법 도 있습니다. 캐시를 날려 버려서 특히 좋은 주장으로 보지 않습니다.
명확하게하기 위해-논리적이지 않은 예외 사용은 지원하지 않습니다. 예를 들어, int.TryParse
사용자의 데이터를 변환하는 데 전적으로 적합합니다. 기계가 생성 한 파일을 읽을 때 적절합니다. 여기서 실패는 "파일이 의도 한 형식이 아닙니다. 다른 파일이 무엇인지 알지 못하므로이 파일을 처리하려고하지 않습니다." "
"합리적인 상황에서만"예외를 사용할 때 예외로 인해 성능이 크게 저하 된 응용 프로그램을 본 적이 없습니다. 기본적으로 중요한 정확성 문제가 없으면 예외가 자주 발생하지 않으며, 심각한 문제가있는 경우 성능이 가장 큰 문제는 아닙니다.
Chris Brumme을 구현 한 사람이 이에 대한 결정적인 대답이 있습니다. 그는 주제에 관한 훌륭한 블로그 기사를 썼습니다 (경고-매우 긴) )
행정상 요약 : 그들은 느리다. Win32 SEH 예외로 구현되므로 일부는 링 0 CPU 경계를 통과합니다! 분명히 현실 세계에서는 많은 다른 작업을 수행하므로 이상한 예외는 전혀 눈에 띄지 않지만 프로그램 흐름에 사용하면 앱이 망치질 것으로 예상됩니다. 이것은 우리에게 장애를 일으키는 MS 마케팅 머신의 또 다른 예입니다. 나는 한 Microsoftie가 오버 헤드가 전혀 발생하지 않은 방식을 알려주는 것을 기억합니다.
Chris는 적절한 인용문을 제공합니다.
실제로 CLR은 엔진의 관리되지 않는 부분에서도 내부적으로 예외를 사용합니다. 그러나 예외와 관련하여 심각한 장기 성능 문제가 있으며 이는 결정에 반영되어야합니다.
나는 그들이 던져 질 때만 느리다고 말할 때 사람들이 무엇에 대해 이야기하고 있는지 전혀 모른다.
편집 : 예외가 발생하지 않으면 새로운 Exception () 또는 이와 유사한 작업을 수행하고 있음을 의미합니다. 그렇지 않으면 예외로 인해 스레드가 일시 중단되고 스택이 진행됩니다. 소규모 상황에서는 문제가 없지만 트래픽이 많은 웹 사이트에서는 워크 플로 또는 실행 경로 메커니즘으로 예외를 사용하면 성능 문제가 발생할 수 있습니다. 예외 자체는 나쁘지 않으며 예외적 인 조건을 표현하는 데 유용합니다.
.NET 앱의 예외 워크 플로는 첫 번째 및 두 번째 기회 예외를 사용합니다. 모든 예외에 대해 예외를 잡아서 처리하더라도 예외 개체는 계속 생성되며 프레임 워크는 여전히 스택을 걸어서 핸들러를 찾습니다. 더 오래 걸리는 코스를 잡아서 다시 던지면-첫 번째 예외가 발생하고 다시 잡아서 다시 던져서 또 다른 첫 번째 예외가 발생하여 핸들러를 찾지 못하게됩니다. 두 번째 기회 예외.
예외는 힙에있는 객체이기도하므로 많은 예외가 발생하면 성능 및 메모리 문제가 모두 발생합니다.
또한 ACE 팀이 작성한 "성능 테스트 Microsoft .NET 웹 응용 프로그램"의 사본에 따르면 :
"예외 처리는 비용이 많이 든다. 올바른 예외 처리기를 검색하기 위해 호출 스택을 통해 CLR이 반복되는 동안 관련 스레드의 실행이 일시 중단되고 발견되면 예외 처리기와 일부 최종 블록이 모두 실행될 기회를 가져야한다 정기적 인 처리를 수행하기 전에
이 분야에서 저 자신의 경험을 통해 예외를 줄이면 성능이 크게 향상되었습니다. 물론 성능 테스트시 고려해야 할 다른 사항이 있습니다 (예 : 디스크 I / O가 발생했거나 쿼리가 몇 초 내에 수행되는 경우). 그러나 예외를 찾아서 제거하는 것이 전략의 중요한 부분이어야합니다.
내가 이해하는 논쟁은 예외를 던지는 것이 나쁘다는 것이 아닙니다. 대신, 일반적인 조건부 구문 대신 일반적인 응용 프로그램 논리를 제어하는 첫 번째 방법으로 throw / catch 구문을 사용합니다.
일반적인 응용 프로그램 논리에서는 종종 같은 동작이 수천 / 백만 번 반복되는 루핑을 수행합니다. 이 경우 매우 간단한 프로파일 링 (Stopwatch 클래스 참조)을 사용하면 간단한 if 문 대신 예외를 던지면 실제로 느릴 수 있음을 알 수 있습니다.
사실 저는 Microsoft의 .NET 팀이 .NET 2.0의 TryXXXXX 메소드를 많은 기본 FCL 유형에 도입했다고 구체적으로 읽었습니다. 고객이 애플리케이션 성능이 너무 느리다고 불평했기 때문입니다.
많은 경우에 고객이 루프에서 값의 유형 변환을 시도하고 각 시도가 실패했기 때문입니다. 변환 예외가 발생하고 예외 처리기에 의해 포착 된 다음 예외를 삼켜 루프를 계속했습니다.
이러한 상황에서 가능한 성능 문제를 피하기 위해 특히이 상황에서 TryXXX 방법을 사용하는 것이 좋습니다.
내가 틀렸을 수도 있지만, 읽은 "벤치 마크"의 정확성에 대해 확신이없는 것 같습니다. 간단한 해결책 : 직접 사용해보십시오.
이 토론에 내 자신의 최근 경험을 추가하기 위해 위에서 언급 한 대부분의 내용에 따라 디버거를 실행하지 않아도 반복적으로 수행 할 때 예외가 매우 느리게 발생한다는 것을 알았습니다. 방금 5 줄의 코드를 변경하여 작성중인 큰 프로그램의 성능을 60 % 향상 시켰습니다. 예외를 던지는 대신 리턴 코드 모델로 전환했습니다. 문제의 코드가 수천 번 실행되었으며 변경하기 전에 수천 개의 예외가 발생할 수 있습니다. 따라서 위의 진술에 동의합니다. "예상 된"상황에서 응용 프로그램 흐름을 제어하는 방법이 아니라 중요한 것이 실제로 잘못 될 때 예외를 throw합니다.
예외와 관련하여 성능 문제가 없었습니다. 예외를 많이 사용합니다. 가능하면 리턴 코드를 사용하지 않습니다. 그것들은 나쁜 습관이며, 제 생각에는 스파게티 코드 냄새가납니다.
나는 그것이 예외를 어떻게 사용하는지에 달려 있다고 생각한다 : 리턴 코드 (스택 캐치 및 다시 던지기의 각 메소드 호출)와 같은 예외를 사용하면 각 캐치 / 던지기마다 오버 헤드가 있기 때문에 느려질 것입니다.
그러나 스택의 맨 아래에 던지고 맨 위에 걸면 (전체 리턴 코드 체인을 하나의 스로우 / 캐치로 대체) 모든 비용이 많이 드는 작업이 한 번 수행됩니다.
하루가 끝나면 유효한 언어 기능입니다.
내 요점을 증명하기 위해
이 링크에서 코드를 실행하십시오 (답이 너무 큽니다).
내 컴퓨터의 결과 :
marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM
타임 스탬프는 처음에 리턴 코드와 예외 사이에서 끝에 출력됩니다. 두 경우 모두 같은 시간이 걸립니다. 최적화를 사용하여 컴파일해야합니다.
그러나 모노는 .net 독립형 모드보다 10 배 더 빠르게 예외를 처리하고 .net 독립형 모드는 .net 디버거 모드보다 60 배 더 빠르게 예외를 처리합니다. (테스트 머신은 동일한 CPU 모델을 가짐)
int c = 1000000;
int s = Environment.TickCount;
for (int i = 0; i < c; i++)
{
try { throw new Exception(); }
catch { }
}
int d = Environment.TickCount - s;
Console.WriteLine(d + "ms / " + c + " exceptions");
Windows CLR에서 깊이 8 호출 체인의 경우 예외를 throw하면 반환 값을 확인하고 전파하는 것보다 750 배 느립니다. (아래 벤치 마크 참조)
이 예외에 대한 높은 비용은 Windows CLR이 Windows 구조적 예외 처리 라는 것과 통합되기 때문 입니다. 이를 통해 다른 런타임과 언어에서 예외를 올바르게 포착하고 처리 할 수 있습니다. 그러나 매우 느립니다.
모든 플랫폼에서 Mono 런타임의 예외는 SEH와 통합되지 않기 때문에 훨씬 빠릅니다. 그러나 SEH와 같은 것을 사용하지 않기 때문에 여러 런타임에서 예외를 전달할 때 기능 손실이 있습니다.
다음은 Windows CLR에 대한 예외 및 반환 값 벤치 마크의 간략한 결과입니다.
baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms
retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms
retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms
retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms
retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms
retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms
exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms
exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208 ms
exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639 ms
exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms
exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms
그리고 여기 코드가 있습니다 ..
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
public class TestIt {
int value;
public class TestException : Exception { }
public int getValue() {
return value;
}
public void reset() {
value = 0;
}
public bool baseline_null(bool shouldfail, int recurse_depth) {
if (recurse_depth <= 0) {
return shouldfail;
} else {
return baseline_null(shouldfail,recurse_depth-1);
}
}
public bool retval_error(bool shouldfail, int recurse_depth) {
if (recurse_depth <= 0) {
if (shouldfail) {
return false;
} else {
return true;
}
} else {
bool nested_error = retval_error(shouldfail,recurse_depth-1);
if (nested_error) {
return true;
} else {
return false;
}
}
}
public void exception_error(bool shouldfail, int recurse_depth) {
if (recurse_depth <= 0) {
if (shouldfail) {
throw new TestException();
}
} else {
exception_error(shouldfail,recurse_depth-1);
}
}
public static void Main(String[] args) {
int i;
long l;
TestIt t = new TestIt();
int failures;
int ITERATION_COUNT = 1000000;
// (0) baseline null workload
for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {
int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);
failures = 0;
DateTime start_time = DateTime.Now;
t.reset();
for (i = 1; i < ITERATION_COUNT; i++) {
bool shoulderror = (i % EXCEPTION_MOD) == 0;
t.baseline_null(shoulderror,recurse_depth);
}
double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
Console.WriteLine(
String.Format(
"baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
recurse_depth, exception_freq, failures,elapsed_time));
}
}
// (1) retval_error
for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {
int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);
failures = 0;
DateTime start_time = DateTime.Now;
t.reset();
for (i = 1; i < ITERATION_COUNT; i++) {
bool shoulderror = (i % EXCEPTION_MOD) == 0;
if (!t.retval_error(shoulderror,recurse_depth)) {
failures++;
}
}
double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
Console.WriteLine(
String.Format(
"retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
recurse_depth, exception_freq, failures,elapsed_time));
}
}
// (2) exception_error
for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {
int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);
failures = 0;
DateTime start_time = DateTime.Now;
t.reset();
for (i = 1; i < ITERATION_COUNT; i++) {
bool shoulderror = (i % EXCEPTION_MOD) == 0;
try {
t.exception_error(shoulderror,recurse_depth);
} catch (TestException e) {
failures++;
}
}
double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
Console.WriteLine(
String.Format(
"exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
recurse_depth, exception_freq, failures,elapsed_time)); }
}
}
}
}
예외 포착과 관련된 성능에 대한 간단한 참고 사항입니다.
실행 경로가 'try'블록에 들어가면 마술은 발생하지 않습니다. 'try'명령이 없으며 try 블록을 시작하거나 종료하는 것과 관련된 비용이 없습니다. try 블록에 대한 정보는 메소드의 메타 데이터에 저장되며이 메타 데이터는 예외가 발생할 때마다 런타임에 사용됩니다. 실행 엔진은 스택을 따라 내려가 try 블록에 포함 된 첫 번째 호출을 찾습니다. 예외 처리와 관련된 모든 오버 헤드는 예외가 발생할 때만 발생합니다.
다른 사람들이 사용할 클래스 / 함수를 작성할 때 예외가 적절한 경우 말하기가 어렵습니다. BCL에는 오류를 반환하는 대신 예외를 throw하기 때문에 도발하고 핀 호출을 해야하는 유용한 부분이 있습니다. 어떤 경우에는 해결 할 수 있지만 System.Management 및 Performance Counters와 같은 다른 경우에는 BCL에서 예외가 자주 발생하는 루프를 수행 해야하는 사용법이 있습니다.
라이브러리를 작성 중이고 함수가 루프에서 사용될 수 있고 많은 반복 가능성이있는 원격 가능성이있는 경우 Try .. 패턴 또는 다른 방법을 사용하여 예외 옆에 오류를 표시하십시오. 그럼에도 불구하고 공유 환경의 많은 프로세스에서 함수를 사용하는 경우 함수가 얼마나 많이 호출되는지 말하기가 어렵습니다.
내 코드에서 예외는 예외가 발생하여 스택 추적을보고 무엇이 잘못되었는지 확인한 다음 수정해야 할 때 예외가 발생합니다. 따라서 예외 대신 Try .. 패턴을 기반으로 오류 처리를 사용하기 위해 BCL의 일부를 다시 작성했습니다.