사전에 키가 포함되어 있는지 확인하는 것이 왜 더 빠르지 않은지 예외를 잡는 것보다 더 빠른 이유는 무엇입니까?


234

코드를 상상해보십시오.

public class obj
{
    // elided
}

public static Dictionary<string, obj> dict = new Dictionary<string, obj>();

방법 1

public static obj FromDict1(string name)
{
    if (dict.ContainsKey(name))
    {
        return dict[name];
    }
    return null;
}

방법 2

public static obj FromDict2(string name)
{
    try
    {
        return dict[name];
    }
    catch (KeyNotFoundException)
    {
        return null;
    }
}

두 함수의 성능에 차이가 있는지 궁금합니다. 첫 번째 함수는 두 번째 함수보다 느려 야합니다. 한 번만 와우, 그것은 실제로 반대입니다.

1 만개의 값에 대한 루프 (기존의 10 만개와 존재하지 않는 9 만개의 경우) :

첫 번째 기능 : 306 밀리 초

두 번째 기능 : 20483 밀리 초

왜 그런 겁니까?

편집 :이 질문 아래 주석에서 알 수 있듯이 두 번째 기능의 성능은 실제로 기존 키가 0이 아닌 경우 첫 번째 기능보다 약간 우수합니다. 그러나 기존 키가 아닌 하나 이상의 키가 있으면 두 번째 키의 성능이 급격히 떨어집니다.


39
왜 첫 번째 해야 느려질 수? 사실, 첫 눈에, 나는 그것을 빨리해야한다라고 말하고 싶지만, ContainsKey기대된다 O(1)...
Patryk Ćwiek


8
@Petr O(1)딕셔너리 룩업 보다 예외 던지기와 관련하여 훨씬 더 많은 명령어가 있습니다 . 특히 두 가지 O(1)작업을 수행하는 것이 여전히 무증상 이기 때문 O(1)입니다.
Patryk Ćwiek

9
아래의 좋은 대답에서 언급했듯이 예외를 던지면 비용이 많이 듭니다. 그들의 이름은 이것을 암시합니다 : 그들은 예외 상황을 위해 예약되어 있습니다. 존재하지 않는 키에 대해 백만 번 사전을 쿼리하는 루프를 실행하는 경우 예외적 인 상황이 아닙니다. 키에 대한 사전을 쿼리 할 때 키가 존재하지 않는 비교적 일반적인 경우에는 먼저 확인하는 것이 좋습니다.
Jason R

6
백만 개의 부재 값을 확인하는 비용과 백만 개의 예외를 처리하는 비용 만 비교 한 것을 잊지 마십시오. 그러나 두 가지 방법은 기존 가치 에 액세스하는 비용도 다릅니다 . 누락 된 키가 충분히 드물면 키가 없을 의 높은 비용에도 불구하고 예외 방법이 전체적으로 더 빠릅니다 .
Alexis

답변:


404

한편, 예외를 던지는 것은 본질적으로 스택이 풀려야하기 때문에 본질적으로 비싸다 .
반면에, 키로 사전의 값에 액세스하는 것은 값이 빠르다. 왜냐하면 O (1) 연산이 빠르기 때문이다.

BTW :이를 수행하는 올바른 방법은 TryGetValue

obj item;
if(!dict.TryGetValue(name, out item))
    return null;
return item;

이것은 두 번이 아닌 한 번만 사전에 액세스합니다. 키가 존재하지 않으면
실제로 반환 null하려면 위 코드를 더 단순화 할 수 있습니다.

obj item;
dict.TryGetValue(name, out item);
return item;

이 때문에, 작동 TryGetValue세트 itemnull아무 키는 경우 name가 없습니다.


4
답변에 따라 테스트를 업데이트했으며 어떤 이유로 든 제안 된 기능이 더 빠르지 만 실제로는별로 중요하지 않습니다. 264ms 원본, 258ms 제 안됨
Petr

52
@ Petr : 예, 사전에 액세스하는 것이 매우 빠르기 때문에 중요하지 않습니다. 한두 번만해도 실제로 중요하지 않습니다. 250ms의 대부분은 테스트 루프 자체에 사용됩니다.
Daniel Hilgarth

4
때로는 예외 발생이 존재하지 않는 파일이나 널 포인터와 같은 상황을 처리하는 더 나은 방법으로 성능 비용을 고려하지 않고 상황에 상관없이 예외 처리가 더 좋고 깔끔한 방법이라는 인상을 받기 때문에 이는 잘 알고 있습니다.
LarsH

4
@LarsH 그것은 또한 당신이하고있는 일에 달려 있습니다. 이와 같은 간단한 마이크로 벤치 마크는 파일 또는 데이터베이스 활동을 포함하여 루프가 시작되면 모든 반복에서 예외를 던지는 성능에 거의 영향을 미치지 않습니다. 첫 번째 테이블과 두 번째 테이블 비교 : codeproject.com/Articles/11265/…
Dan Is Fiddling By Firelight

8
@LarsH 또한 파일 (또는 다른 외부 리소스)에 액세스하려고하면 검사와 실제 액세스 시도 사이의 상태가 변경 될 수 있습니다. 이러한 경우 예외를 사용하는 것이 올바른 방법입니다. 추가 정보를 얻으 려면이 질문에 대한 Stephen C의 답변을 참조하십시오 .
yoniLavi

6

사전은 특히 초고속 키 조회를 수행하도록 설계되었습니다. 그것들은 해시 테이블로 구현되며 항목이 많을수록 다른 방법에 비해 빠릅니다. 예외 엔진을 사용하는 것은 메소드가 설계 한 작업을 수행하지 못한 경우에만 수행되어야합니다. 이는 메소드가 오류 처리를위한 많은 기능을 제공하는 큰 객체 세트이기 때문입니다. try catch 블록으로 둘러싸인 모든 항목으로 한 번 전체 라이브러리 클래스를 작성했으며 600 개가 넘는 예외 중 하나에 대한 별도의 행이 포함 된 디버그 출력을 보도록 놀라게되었습니다!


1
언어 구현자가 최적화에 노력을 기울일 위치를 결정할 때 해시 테이블은 종종 병목 현상이 발생할 수있는 내부 루프에서 자주 사용되므로 우선 순위를 갖습니다. 예외는 예외적 인 ( "예외적 인"말을하는) 경우에 훨씬 덜 자주 사용될 것으로 예상되므로 일반적으로 성능에 중요한 것으로 간주되지 않습니다.
Barmar

"해시 테이블로 구현되며 다른 방법에 비해 항목이 많을수록 빠릅니다." 버킷이 가득 차면 반드시 그렇지 않습니다!
AnthonyLambert

1
@AnthonyLambert 그가 말하려는 것은 해시 테이블을 검색하면 O (1) 시간 복잡성이 있고 이진 검색 트리 검색은 O (log (n))을 갖습니다. 해시 테이블은 그렇지 않은 반면 요소 수가 무제한으로 증가함에 따라 트리 속도가 느려집니다. 따라서 해시 테이블의 속도 이점은 요소 수에 따라 증가하지만 느리게 증가합니다.
Doval

@AnthonyLambert 정상적인 사용에서 Dictionary의 해시 테이블에는 충돌이 거의 없습니다. 해시 테이블을 사용 중이고 버킷이 가득 찬 경우 항목이 너무 많거나 버킷이 너무 적습니다. 이 경우 사용자 지정 해시 테이블을 사용할 차례입니다.
AndrewS
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.