私は辞書をルックアップテーブルとして使用していましたが、リストがアプリケーションに適しているかどうか疑問に思い始めました。ルックアップテーブルのエントリの量はそれほど多くありませんでした。リストがボンネットの下でC配列を使用することを知っているので、ほんの数項目のリストの検索は辞書よりも優れていると結論付けました(配列内のいくつかの要素にアクセスする方がハッシュを計算するよりも高速です)。
私は代替案をプロファイリングすることにしましたが、結果は驚きました。リスト検索は、単一の要素でのみ改善されました!次の図を参照してください(log-logプロット):
だからここに質問が来ます:なぜリストのルックアップはそれほどパフォーマンスが悪いのですか?
余談ですが、私の注意を呼んだものは、約1000エントリ後の辞書検索時間の少しの「不連続性」でした。 dict検索時間だけをプロットして表示しました。
ps1知っているO(n) vs O(1)配列とハッシュテーブルの償却時間ですが、通常、配列で反復する要素の数が少ない場合は、ハッシュテーブルを使用するよりも優れています。
p.s.2辞書とリストのルックアップ時間を比較するために使用したコードは次のとおりです:
import timeit
lengths = [2 ** i for i in xrange(15)]
list_time = []
dict_time = []
for l in lengths:
list_time.append(timeit.timeit('%i in d' % (l/2), 'd=range(%i)' % l))
dict_time.append(timeit.timeit('%i in d' % (l/2),
'd=dict.fromkeys(range(%i))' % l))
print l, list_time[-1], dict_time[-1]
p.s.3 Python 2.7.13の使用
リストがボンネットの下でC配列を使用することを知っているので、ほんの数項目のリストの検索は辞書よりも優れていると結論付けました(配列内のいくつかの要素にアクセスする方がハッシュを計算するよりも高速です)。
いくつかの配列要素へのアクセスは確かに安価ですが、==
の計算はPythonで驚くほど重いです。 2番目のグラフにそのスパイクが表示されますか?そこにある2つの整数に対して==
を計算するコストです。
リストの検索では、dict検索よりもはるかに多くの==
を計算する必要があります。
一方、ハッシュの計算は多くのオブジェクトにとって非常に重い操作かもしれませんが、ここに関係するすべてのintについては、それらは自分自身にハッシュするだけです。 (-1は-2にハッシュし、大きな整数(技術的にはlong
s)はより小さい整数にハッシュしますが、ここでは当てはまりません。)
Pythonでは、特にキーがintの連続した範囲である場合、辞書検索はそれほど悪くありません。ここのすべてのintは自分自身にハッシュし、Pythonはチェーンの代わりにカスタムのオープンアドレス指定スキームを使用するため、すべてのキーはリストを使用した場合とほぼ同じようにメモリ内で連続します(つまり、キーへのポインターは、PyDictEntry
sの連続した範囲になります)。ルックアップ手順は高速であり、テストケースでは、最初のプローブで常に正しいキーを押します。
さて、グラフ2のスパイクに戻ります。2番目のグラフの1024エントリでのルックアップ時間のスパイクは、すべての小さいサイズで、探していた整数がすべて<= 256であったため、すべての範囲に収まりました。 CPythonの小さな整数キャッシュ。 Pythonの参照実装は、-5〜256のすべての整数の標準整数オブジェクトを保持します。これらの整数について、Pythonはクイックポインター比較を使用して、(驚くほど重い)計算プロセス==
を通過することを回避できました。より大きい整数の場合、in
への引数は、辞書内の一致する整数と同じオブジェクトではなくなり、Pythonは==
プロセス全体を処理する必要がありました。
簡単な答えは、リストは線形検索を使用し、辞書は償却O(1)検索を使用します。
さらに、dict検索では、1)ハッシュ値が一致しない場合、または2)IDが一致する場合に、同等性テストをスキップできます。リストは、同一性を意味する等価性最適化からのみ恩恵を受けます。
2008年に、このテーマに関する講演を行いました。ここでは、すべての詳細を確認できます。 https://www.youtube.com/watch?v=hYUsssClE94
リストを検索するための大まかなロジックは次のとおりです。
for element in s:
if element is target:
# fast check for identity implies equality
return True
if element == target:
# slower check for actual equality
return True
return False
口述の場合、ロジックは大体次のとおりです。
h = hash(target)
for i in probe_sequence(h, len(table)):
element = key_table[i]
if element is UNUSED:
raise KeyError(target)
if element is target:
# fast path for identity implies equality
return value_table[i]
if h != h_table[i]:
# unequal hashes implies unequal keys
continue
if element == target:
# slower check for actual equality
return value_table[i]
ディクショナリハッシュテーブルは通常、3分の1から3分の2の間であるため、サイズに関係なく、衝突はほとんどありません(上記のループの周りのトリップはほとんどありません)。また、ハッシュ値のチェックにより、不必要に遅い等値チェックが行われなくなります(等値チェックが無駄になる可能性は2 ** 64分の1)。
タイミングが整数に焦点を合わせている場合、他にもいくつかの効果があります。 intのハッシュはintそのものなので、ハッシュは非常に高速です。また、連続した整数を格納している場合は、まったく衝突しない傾向があることを意味します。
「配列内のいくつかの要素にアクセスする方が、ハッシュを計算するよりも高速です」と言います。
文字列の単純なハッシュルールは、単に合計(最後にモジュロ)である場合があります。これは、特にプレフィックスに長い一致がある場合に、文字比較と良好に比較できるブランチレス操作です。