含まれていない場合に例外をキャッチするよりも、辞書にキーが含まれているかどうかをチェックする方が速いのはなぜですか?


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

これらの2つの関数のパフォーマンスに違いがあるかどうか気になったのは、最初の関数は2番目の関数よりも遅い必要があるためです。一度だけですが、WOW、それは実際には反対です:

1 000 000値のループ(100 000が存在し、900 000が存在しない):

最初の関数:306ミリ秒

2番目の関数:20483ミリ秒

何故ですか?

編集:この質問の下のコメントでわかるように、2番目の関数のパフォーマンスは、実際には存在しないキーが0の場合、最初の関数よりもわずかに優れています。ただし、存在しないキーが少なくとも1つ以上存在すると、2番目のキーのパフォーマンスは急速に低下します。


39
なぜ、最初のものはすべき遅くなりますか?実際には、一見したところ、それはより高速であるはずだと思いますContainsKeyが、期待されていO(1)ます...
PatrykĆwiek'19年


8
@Petr例外のスローにO(1)は、ディクショナリでの検索よりも多くの命令が含まれます...特に、2つのO(1)操作を実行することは依然として漸近的であるためO(1)です。
PatrykĆwiek2013

9
以下の適切な回答で述べたように、例外のスローはコストがかかります。彼らの名前はこれを示唆しています:それらは例外的な状況のために予約されることを意図しています。存在しないキーについて辞書を100万回クエリするループを実行している場合、例外的な状況ではなくなります。ディクショナリにキーを照会していて、キーが存在しないことが比較的一般的なケースである場合は、最初にチェックすることは理にかなっています。
Jason R

6
100万の例外をスローする場合と100万の例外をスローする場合のコストを比較しただけであることを忘れないでください。ただし、この2つの方法では、既存の値にアクセスするコストも異なります。欠落しているキーが非常にまれである場合、キーがない場合のコストは高くなりますが、例外メソッドはすべてより高速になります。
アレクシス

回答:


404

一方で、スタックをほどく必要があるため、例外をスローすることは本質的にコストがかかります。
他方、そのキーによってディクショナリ内の値にアクセスすることは、高速なO(1)操作であるため安価です。

ところで:これを行う正しい方法は使用することです TryGetValue

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

これは、2回ではなく1回だけ辞書にアクセスします。キーが存在しない
場合に本当に戻りたいnull場合は、上記のコードをさらに簡略化できます。

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

これは、キーが存在しない場合にTryGetValue設定itemnullれるため機能しnameます。


4
私は答えに従ってテストを更新しました、そして何らかの理由で、提案された機能はより高速ですが、実際にはそれほど重要ではありません:元の264ミリ秒、提案された258ミリ秒
Petr

52
@Petr:はい、重要ではありません。辞書へのアクセスは非常に高速であるため、1回または2回実行しても問題にはなりません。これらの250ミリ秒のほとんどは、テストループ自体に費やされている可能性が高いです。
Daniel Hilgarth 2013

4
これらの状況が一般的であるかどうかに関係なく、パフォーマンスコストを考慮せずに、例外のスローが存在しないファイルやnullポインターのような状況を処理するためのより良いまたはよりクリーンな方法であるという印象を受ける場合があるため、これは知っておくと役立ちます。
LarsH 2013

4
@LarsHそれはあなたが何をしているかにも依存します。このような単純なマイクロベンチマークは、ループが開始されると例外に対して非常に大きなペナルティを示しますが、反復ごとに例外をスローするファイルまたはデータベースアクティビティがループに含まれるようになると、パフォーマンスにはほとんど影響しません。1番目と2番目のテーブルの比較: codeproject.com/Articles/11265/...
ダンであるいじることで火明かり

8
@LarsHまた、ファイル(またはその他の外部リソース)にアクセスしようとすると、チェックと実際のアクセス試行の間で状態が変わる場合があることに注意してください。これらの場合、例外を使用するのが正しい方法です。詳細については、この質問に対するスティーブンCの回答を参照してください。
yoniLavi 2013

6

辞書は、特に超高速のキー検索を行うように設計されています。それらはハッシュテーブルとして実装され、エントリが多いほど、他のメソッドと比較して高速になります。例外エンジンは、エラーを処理するための多くの機能を提供する大規模なオブジェクトのセットであるため、メソッドが設計どおりに実行できなかった場合にのみ使用することが想定されています。私はライブラリクラス全体を一度ビルドし、すべてを一度try catchブロックで囲み、600以上の例外の1つ1つに個別の行を含むデバッグ出力を確認してびっくりしました!


1
言語の実装者が最適化への取り組みをどこに費やすかを決定するとき、ハッシュテーブルはボトルネックになる可能性のある内部ループで頻繁に使用されるため、優先されます。例外は、まれな(いわば「例外的な」)ケースで使用される頻度がはるかに少ないと予想されるため、通常、パフォーマンスにとって重要とは見なされません。
Barmar 2013

「それらはハッシュテーブルとして実装されており、エントリが多いほど、他のメソッドに比べて速くなります。」バケツがいっぱいになると、それは確かに本当ではありませんか?!?!
AnthonyLambert 2013

1
@AnthonyLambert彼が言おうとしているのは、ハッシュテーブルの検索ではO(1)時間の複雑さがあるのに対し、バイナリ検索ツリー検索ではO(log(n)); 要素の数が漸近的に増加するとツリーは遅くなりますが、ハッシュテーブルはそうではありません。したがって、ハッシュテーブルの速度の利点は、要素の数が増えるにつれて増加しますが、速度は非常に遅くなります。
Doval 2013年

@AnthonyLambert通常の使用では、Dictionaryのハッシュテーブルでの衝突はほとんどありません。ハッシュテーブルを使用していてバケットがいっぱいになると、エントリが多すぎます(またはバケットが少なすぎます)。その場合は、カスタムハッシュテーブルを使用します。
AndrewS 2014年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.