Javaドキュメントによれば、オブジェクトのハッシュコードString
は次のように計算されます。
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
int
算術を使用します。ここs[i]
で、 は文字列のi番目の文字、文字列n
の長さ、および^
指数を示します。
31が乗算器として使用されるのはなぜですか?
乗数は比較的大きな素数でなければならないことを理解しています。では、なぜ29や37、あるいは97でもないのでしょうか。
Javaドキュメントによれば、オブジェクトのハッシュコードString
は次のように計算されます。
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
int
算術を使用します。ここs[i]
で、 は文字列のi番目の文字、文字列n
の長さ、および^
指数を示します。
31が乗算器として使用されるのはなぜですか?
乗数は比較的大きな素数でなければならないことを理解しています。では、なぜ29や37、あるいは97でもないのでしょうか。
回答:
Joshua BlochのEffective Java(十分にお勧めできない本で、stackoverflowについての継続的な言及のおかげで私が購入した本)によると、
奇数の素数であるため、値31が選択されました。偶数で乗算がオーバーフローした場合、2による乗算はシフトと同じであるため、情報は失われます。素数を使用する利点はそれほど明確ではありませんが、伝統的なものです。31の優れた特性は、乗算をシフトと減算で置き換えて、パフォーマンスを向上できることです
31 * i == (i << 5) - i
。最近のVMでは、この種の最適化が自動的に行われます。
(第3章、アイテム9:等号をオーバーライドするときは常にハッシュコードをオーバーライドする、48ページ)
グッドリッチとTamassiaはあなたがオーバー50,000英単語を取る場合は、指摘定数31、33、37、39を使用して、(Unixのの二つの変種で提供単語リストの和集合として形成される)、および41は、7回の未満の衝突を生成しますいずれの場合にも。これを知っていれば、多くのJava実装がこれらの定数の1つを選択することは当然のことです。
偶然にも、この質問を見たとき、「多項式ハッシュコード」のセクションを読んでいたところです。
編集:ここに私が上記で言及している〜10mb PDFブックへのリンクがあります。Javaのデータ構造とアルゴリズムのセクション10.2ハッシュテーブル(413ページ)を参照してください。
(主に)古いプロセッサでは、31を掛けることは比較的安価です。たとえば、ARMでは1つの命令のみです。
RSB r1, r0, r0, ASL #5 ; r1 := - r0 + (r0<<5)
他のほとんどのプロセッサは、個別のシフトおよび減算命令を必要とします。しかし、あなたの乗数が遅い場合、これはまだ勝利です。最近のプロセッサは高速の乗算器を備えている傾向があるため、32が正しい側にある限り、それほど大きな違いはありません。
これは優れたハッシュアルゴリズムではありませんが、十分に優れ、1.0コードよりも優れています(1.0仕様よりもはるかに優れています)。
String.hashCode
IIは8ビットの乗算器を導入し、場合によっては算術/論理演算とシフト演算の組み合わせで2サイクルに増加するStrongARMよりも前の日付です。
Map.Entry
あるkey.hashCode() ^ value.hashCode()
にもかかわらず、仕様によって修正されています。はい、それは、またはなどが予測可能にゼロであることを意味します。したがって、マップを他のマップのキーとして使用しないでください...key
value
Map.of(42, 42).hashCode()
Map.of("foo", "foo", "bar", "bar").hashCode()
Blochの元の推論は、http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622の「コメント」で読むことができます。彼は、ハッシュテーブルで結果として得られる「平均チェーンサイズ」に関して、さまざまなハッシュ関数のパフォーマンスを調査しました。P(31)
彼がK&Rの本で見つけた当時の一般的な機能の1つでした(しかし、カーニハンとリッチーでさえ、それがどこから来たのか思い出せませんでした)。結局のところ、彼は基本的に1つを選択P(31)
する必要があり、十分に機能するように見えたため、彼はそれを採用しました。にもかかわらずP(33)
、本当に悪いことではなかったと33は素数ではありませんので、33による乗算は均等に高速計算(わずか5分のシフトと加算)に、彼は31を選んだされています。
残りの4つのうち、RISCマシンで計算するのが最も安いため、おそらくP(31)を選択します(31は2のべき乗の差であるため)。P(33)も同様に安価に計算できますが、パフォーマンスはわずかに悪く、33は合成なので、少し緊張します。
したがって、ここでの回答の多くが示唆するように、推論は合理的ではありませんでした。しかし、私たちは皆、直感的な決定の後に合理的な理由を思いつくのは得意です(そして、Blochでさえもその傾向があるかもしれません)。
実際、37はかなりうまくいくでしょう。z:= 37 * xは次のように計算できますy := x + 8 * x; z := x + 4 * y
。どちらの手順も1つのLEA x86命令に対応しているため、これは非常に高速です。
実際、さらに大きな素数73との乗算は、を設定することで同じ速度で実行できますy := x + 8 * x; z := x + 8 * y
。
より高密度のコードにつながるため、73または37(31の代わりに)を使用する方が良い場合があります。2つのLEA命令は、31による乗算の移動+シフト+減算の7バイトに対して、6バイトしかかかりません。ここで使用されている3つの引数を持つLEA命令は、IntelのSandyブリッジアーキテクチャでは遅くなり、レイテンシが3サイクル長くなりました。
また、73はシェルドンクーパーのお気に入りの番号です。
Joshua Blochがその特定の(新しい)実装が選択された理由を説明するJDK-4045622からString.hashCode()
以下の表は、3つのデータセットについて、上記のさまざまなハッシュ関数のパフォーマンスをまとめたものです。
1)Merriam-Websterの2nd Int'l Unabridged Dictionary(311,141文字列、平均長10文字)のエントリを持つすべての単語とフレーズ。
2)/ bin / 、/ usr / bin /、/ usr / lib / 、/ usr / ucb /のすべての文字列 および/ usr / openwin / bin / *のすべての文字列(66,304文字列、平均長21文字)。
3)昨夜数時間実行されたWebクローラーによって収集されたURLのリスト(28,372文字列、平均長49文字)。
表に示されているパフォーマンスメトリックは、ハッシュテーブル内のすべての要素の「平均チェーンサイズ」です(つまり、要素を検索するためのキー比較数の期待値)。
Webster's Code Strings URLs --------- ------------ ---- Current Java Fn. 1.2509 1.2738 13.2560 P(37) [Java] 1.2508 1.2481 1.2454 P(65599) [Aho et al] 1.2490 1.2510 1.2450 P(31) [K+R] 1.2500 1.2488 1.2425 P(33) [Torek] 1.2500 1.2500 1.2453 Vo's Fn 1.2487 1.2471 1.2462 WAIS Fn 1.2497 1.2519 1.2452 Weinberger's Fn(MatPak) 6.5169 7.2142 30.6864 Weinberger's Fn(24) 1.3222 1.2791 1.9732 Weinberger's Fn(28) 1.2530 1.2506 1.2439
この表を見ると、現在のJava関数と2つのバージョンのWeinberger関数を除くすべての関数が、優れた、ほとんど区別がつかないパフォーマンスを提供していることがわかります。このパフォーマンスは本質的に「理論上の理想」であると強く推測します。これは、ハッシュ関数の代わりに真の乱数ジェネレータを使用した場合に得られるものです。
WAIS関数の仕様には乱数のページが含まれているため、WAIS関数は除外します。そのパフォーマンスは、はるかに単純な関数のどれよりも優れています。残りの6つの関数はどれも優れた選択肢のようですが、1つ選択する必要があります。マイナーではありますが、VoのバリアントとWeinbergerの関数は、複雑さが増しているため除外します。残りの4つのうち、RISCマシンで計算するのが最も安いため、おそらくP(31)を選択します(31は2のべき乗の差であるため)。P(33)も同様に安価に計算できますが、パフォーマンスはわずかに悪く、33は合成なので、少し緊張します。
ジョシュ
Blochはこれについて詳しく説明していませんが、私がいつも聞いたり信じたりしている根拠は、これが基本的な代数であるということです。ハッシュは、乗算とモジュロ演算に要約されます。つまり、あなたがそれを助けることができるならば、あなたは共通の要素を持つ数を決して使いたくないということを意味します。言い換えると、比較的素数の方が回答が均等に分散されるということです。
ハッシュを使用して構成する数値は通常、次のとおりです。
実際にはこれらの値のいくつかしか制御できないため、少し余分な注意が必要です。
JDKの最新バージョンでは、31がまだ使用されています。https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/String.html#hashCode()
ハッシュ文字列の目的は
^
ハッシュコード計算ドキュメントで演算子を見てみましょう。一意に役立ちます)31は8ビット(= 1バイト)レジスタに入れることができる最大値、1バイトレジスタに入れることができる最大の素数、奇数
乗算31は<< 5で、次にそれ自体を減算するため、安価なリソースが必要です。