私はペットプロジェクトの1つとして7カードポーカーハンドエバリュエーターを書いています。その速度を最適化しようとしている間(私は挑戦が好きです)、配列のキールックアップと比較して、辞書のキールックアップのパフォーマンスが非常に遅いことにショックを受けました。
たとえば、52個のchoose 7 = 133,784,560の可能な7枚のカードハンドすべてを列挙するこのサンプルコードを実行しました。
var intDict = new Dictionary<int, int>();
var intList = new List<int>();
for (int i = 0; i < 100000; i ++)
{
intDict.Add(i, i);
intList.Add(i);
}
int result;
var sw = new Stopwatch();
sw.Start();
for (int card1 = 0; card1 < 46; card1++)
for (int card2 = card1 + 1; card2 < 47; card2++)
for (int card3 = card2 + 1; card3 < 48; card3++)
for (int card4 = card3 + 1; card4 < 49; card4++)
for (int card5 = card4 + 1; card5 < 50; card5++)
for (int card6 = card5 + 1; card6 < 51; card6++)
for (int card7 = card6 + 1; card7 < 52; card7++)
result = intDict[32131]; // perform C(52,7) dictionary key lookups
sw.Stop();
Console.WriteLine("time for dictionary lookups: {0} ms", sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
for (int card1 = 0; card1 < 46; card1++)
for (int card2 = card1 + 1; card2 < 47; card2++)
for (int card3 = card2 + 1; card3 < 48; card3++)
for (int card4 = card3 + 1; card4 < 49; card4++)
for (int card5 = card4 + 1; card5 < 50; card5++)
for (int card6 = card5 + 1; card6 < 51; card6++)
for (int card7 = card6 + 1; card7 < 52; card7++)
result = intList[32131]; // perform C(52,7) array index lookups
sw.Stop();
Console.WriteLine("time for array index lookups: {0} ms", sw.ElapsedMilliseconds);
出力:
time for dictionary lookups: 2532 ms
time for array index lookups: 313 ms
このタイプの動作は予想されますか(パフォーマンスが8分の1に低下します)? IIRC、辞書には平均してO(1)ルックアップがありますが、配列には最悪の場合O(1)ルックアップがあるので、私は期待しています配列の検索は速くなりますが、これだけではありません!
私は現在、ポーカーハンドのランキングを辞書に保存しています。これが辞書検索と同じくらい速いとしたら、私のアプローチを再考し、代わりに配列を使用する必要があると思います。
Big-O表記は、サイズ(など)に関して複雑さがどのように増大するかを示すだけであり、関連する一定の要因を示すものではないことを忘れないでください。そのため、キーが十分に少ない場合、キーの線形searchでさえ辞書検索よりも高速になることがあります。この場合でも、配列を使用して検索を行うことはありません。単なるインデックス付け操作です。
ストレートインデックスルックアップの場合、配列は基本的に理想的です-それは単なるケースです
pointer_into_array = base_pointer + offset * size
(そして、ポインター逆参照。)
辞書検索の実行は比較的複雑です。キーが多数ある場合、キーによる線形検索と比較すると非常に高速ですが、直線配列検索よりもはるかに複雑です。キーのハッシュを計算し、どのバケットに入れるかを計算し、おそらく重複ハッシュ(または重複バケット)を処理して、等価性をチェックする必要があります。
いつものように、ジョブに適したデータ構造を選択してください。配列にインデックスを付けるだけで本当にうまくいく場合は(またはList<T>
)そうです、それは目がくらむほど速くなります。
このタイプの動作は予想されますか(パフォーマンスが8分の1に低下します)?
何故なの?各配列のルックアップは、ほぼ瞬時に/簡単に実行できますが、辞書のルックアップでは、少なくとも追加のサブルーチン呼び出しが必要になる場合があります。
両方がO(1)であるという点は、各コレクションに50倍のアイテムがある場合でも、パフォーマンスの低下はそれが何であれ(8)の要因にすぎないことを意味します。
何かが千年かかる可能性がありますが、それでもO(1)です。
逆アセンブリウィンドウでこのコードをシングルステップ実行すると、違いがすぐにわかるようになります。
ディクショナリ構造は、キースペースが非常に大きく、安定した順序にマップできない場合に最も役立ちます。キーを比較的狭い範囲の単純な整数に変換できる場合、配列よりもパフォーマンスが優れたデータ構造を見つけるのは困難です。
実装上の注意; .NETでは、辞書は基本的にハッシュ可能です。キーが一意の値の大きなスペースにハッシュされるようにすることで、キールックアップのパフォーマンスをいくらか向上させることができます。あなたの場合は、単純な整数をキーとして使用しているように見えます(これは、独自の値にハッシュされていると思います)。これが最善の方法かもしれません。
配列のルックアップは、実行できる最速のことです。基本的に、配列の先頭から検索したい要素へのポインタ演算が1ビットです。一方、ハッシュを実行して正しいバケットを見つけることに関係する必要があるため、辞書の検索は多少遅くなる可能性があります。予想される実行時間もO(1)-アルゴリズム定数が大きいため、遅くなります。
Big-O表記へようこそ。常に一定の要因が関係していることを考慮する必要があります。
もちろん、1つのDict-Lookupを実行すると、配列ルックアップよりもはるかにコストがかかります。
Big-Oは、アルゴリズムのスケーリング方法のみを示します。ルックアップの量を2倍にして、数値がどのように変化するかを確認します。どちらも約2倍の時間がかかるはずです。
ディクショナリはO(1) から要素を取得するコストですが、これはディクショナリがハッシュテーブルとして実装されているためです。したがって、最初にハッシュ値を計算して、返す要素を知る必要があります。多くの場合、ハッシュテーブルはそれほど効率的ではありませんが、大規模なデータセットや、一意のハッシュ値が多数あるデータセットには適しています。
リスト(リンクリストではなく配列をdercribeするために使用されるごみの単語であることは別として!)は、返される要素を直接計算することによって値を返すため、より高速になります。