String内のJavaのhashCode()が31を乗数として使用するのはなぜですか?


480

Javaドキュメントによれば、オブジェクトのハッシュコードStringは次のように計算されます。

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

int算術を使用します。ここs[i]で、 は文字列のi番目の文字、文字列nの長さ、および^指数を示します。

31が乗算器として使用されるのはなぜですか?

乗数は比較的大きな素数でなければならないことを理解しています。では、なぜ29や37、あるいは97でもないのでしょうか。


1
stackoverflow.com/questions/1835976/…も比較してください。独自のhashCode関数を作成する場合、31は悪い選択だと思います。
Hans-PeterStörr、

6
それが29、37、または97だったら、「なぜ31ではないのですか?」
ローン侯爵

2
@EJPノーの選択の背後にある理由を知ることが重要です。数が黒魔術の結果でない限り。
Dushyant Sabharwal 2017

ここではそれについて@ピーター・lawreyにより、ブログの記事があります:vanilla-java.github.io/2018/08/12/...、ここで:vanilla-java.github.io/2018/08/15/...
クリストフRoussy

@DushyantSabharwal私のポイントは、それはしている可能性があることですして多くの実用的な違いをすることなく、29または37または97、または41、または他の多くの値。私たちは、1976年に37を使用していた
ローン侯爵

回答:


405

Joshua BlochのEffective Java(十分にお勧めできない本で、stackoverflowについての継続的な言及のおかげで私が購入した本)によると、

奇数の素数であるため、値31が選択されました。偶数で乗算がオーバーフローした場合、2による乗算はシフトと同じであるため、情報は失われます。素数を使用する利点はそれほど明確ではありませんが、伝統的なものです。31の優れた特性は、乗算をシフトと減算で置き換えて、パフォーマンスを向上できることです31 * i == (i << 5) - i。最近のVMでは、この種の最適化が自動的に行われます。

(第3章、アイテム9:等号をオーバーライドするときは常にハッシュコードをオーバーライドする、48ページ)


346
2つを除いて、すべての素数は奇数です。
キップ、

38
それが奇数の素数であったために選択されたとBlochが言っているとは思わないが、それは素数であったため(かつシフト/減算に簡単に最適化できるため、かつ)奇数であったため。
matt b

50
31選ばれたcozそれは奇妙な素数ですか??? それdoesntのはどんな意味を成して-私はそれが最善の配布を行いましたので、31が選ばれたと言う-チェック computinglife.wordpress.com/2008/11/20/...
computinglife

65
31の選択はかなり残念です。確かに、古いマシンではいくつかのCPUサイクルを節約できるかもしれませんが、 "@ and#! 。私は- * I == I << 19 524287:また、ビットシフトを可能にする少なくとも524287
ハンス・ペーター・ストー

15
@ジェイソン私の答えを参照してくださいstackoverflow.com/questions/1835976/…。私の要点は、より大きな素数を使用すれば、衝突がはるかに少なくなり、最近は何も失うことがないということです。一般的な非ASCII文字で英語以外の言語を使用すると、問題はさらに悪化します。また、31は、独自のhashCode関数を作成する際の多くのプログラマにとって悪い例です。
Hans-PeterStörr10年

80

グッドリッチとTamassiaはあなたがオーバー50,000英単語を取る場合は、指摘定数31、33、37、39を使用して、(Unixのの二つの変種で提供単語リストの和集合として形成される)、および41は、7回の未満の衝突を生成しますいずれの場合にも。これを知っていれば、多くのJava実装がこれらの定数の1つを選択することは当然のことです。

偶然にも、この質問を見たとき、「多項式ハッシュコード」のセクションを読んでいたところです。

編集:ここに私が上記で言及している〜10mb PDFブックへのリンクがあります。Javaのデータ構造とアルゴリズムのセクション10.2ハッシュテーブル(413ページ)を参照してください。


6
ただし、ASCII範囲外の一般的な文字を含む国際文字セットを使用すると、さらに多くの衝突が発生する可能性があることに注意してください。少なくとも、私は31とドイツ語でこれをチェックしました。したがって、31の選択は壊れていると思います。
Hans-PeterStörr2010年

1
@jJack、回答で提供されたリンクが壊れています。
SK Venkat

この回答の両方のリンクは壊れています。また、最初の段落の引数は、少し不完全です。他の奇数は、このベンチマークにリストした5つとどのように比較しますか?
Mark Amery

58

(主に)古いプロセッサでは、31を掛けることは比較的安価です。たとえば、ARMでは1つの命令のみです。

RSB       r1, r0, r0, ASL #5    ; r1 := - r0 + (r0<<5)

他のほとんどのプロセッサは、個別のシフトおよび減算命令を必要とします。しかし、あなたの乗数が遅い場合、これはまだ勝利です。最近のプロセッサは高速の乗算器を備えている傾向があるため、32が正しい側にある限り、それほど大きな違いはありません。

これは優れたハッシュアルゴリズムではありませんが、十分に優れ、1.0コードよりも優れています(1.0仕様よりもはるかに優れています)。


7
おかしなことに、31を使用した乗算は、デスクトップマシン上で実際に、たとえば92821を使用した乗算よりも少し遅いです。コンパイラは、シフトと加算に「最適化」しようとしていると思います。:-)
Hans-PeterStörr10年

1
+/- 255の範囲のすべての値で同等に高速ではなかったARMを使用したことはないと思います。2の累乗から1を引いた値を使用すると、2つの値への一致する変更がハッシュコードを2の累乗で変更するという残念な影響があります。-31の値が良かったのですが、-83(64 + 16 + 2 + 1)のようなものがもっと良かったのではないかと思います(ビットをいくらかよくブレンドする)。
スーパーキャット2014年

@supercatマイナスには納得できません。ゼロに向かっているようです。/ String.hashCodeIIは8ビットの乗算器を導入し、場合によっては算術/論理演算とシフト演算の組み合わせで2サイクルに増加するStrongARMよりも前の日付です。
トム・ホーティン-タックライン2014年

1
@ TomHawtin-tackline:31を使用すると、4つの値のハッシュは29791 * a + 961 * b + 31 * c + dになります。-31を使用すると、-29791 * a + 961 * b-31 * c + dになります。4つのアイテムが独立している場合、違いは重要ではないと私は思いますが、隣接するアイテムのペアが一致する場合、結果のハッシュコードは、ペアになっていないすべてのアイテムに32の倍数を加えたものになります(ペアになっているアイテムから)。文字列の場合はそれほど重要ではないかもしれませんが、集計をハッシュするための汎用的なメソッドを記述している場合、隣接するアイテムが一致する状況は不釣り合いに一般的です。
スーパーキャット2014年

3
@supercatおもしろいことに、のハッシュコードは、完全に異なる意味を持つため、順序付けされていないペアでMap.Entryあるkey.hashCode() ^ value.hashCode()にもかかわらず、仕様によって修正されています。はい、それは、またはなどが予測可能にゼロであることを意味します。したがって、マップを他のマップのキーとして使用しないでください...keyvalueMap.of(42, 42).hashCode()Map.of("foo", "foo", "bar", "bar").hashCode()
Holger

33

乗算すると、ビットは左にシフトされます。これにより、ハッシュコードの使用可能なスペースがより多く使用され、衝突が減少します。

2の累乗を使用しないことにより、下位の右端のビットも入力され、ハッシュに入る次のデータと混合されます。

n * 31はと同等(n << 5) - nです。


29

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でさえもその傾向があるかもしれません)。


2
徹底的な調査と公平な答え!
Vishal K 2016

22

実際、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はシェルドンクーパーのお気に入りの番号です。


5
あなたはパスカルプログラマーですか?:=とは何ですか?
Mainguy

11
@Mainguyこれは実際にはALGOL構文であり、疑似コードでかなり頻繁に使用されます。
DarknessFishへの接近

4
ARMアセンブリでは、31による乗算は単一の命令で実行できます
phuclv 2015


TPOP(1999)1は、早期のJava(P.57)について読むことができます:「...問題は、我々は(の乗数を示している一から一同等でハッシュを交換することによって解決された37 ...)」
ミク

19

Neil Coffey 、31がバイアスのアイアンアウトで使用される理由を説明しています

基本的に31を使用すると、ハッシュ関数のセットビット確率分布がより均一になります。


12

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は合成なので、少し緊張します。

ジョシュ


5

Blochはこれについて詳しく説明していませんが、私がいつも聞いたり信じたりしている根拠は、これが基本的な代数であるということです。ハッシュは、乗算とモジュロ演算に要約されます。つまり、あなたがそれを助けることができるならば、あなたは共通の要素を持つ数を決して使いたくないということを意味します。言い換えると、比較的素数の方が回答が均等に分散されるということです。

ハッシュを使用して構成する数値は通常、次のとおりです。

  • 入力したデータ型の係数(2 ^ 32または2 ^ 64)
  • ハッシュテーブルのバケット数の係数(可変。Javaでは以前は素数でしたが、現在は2 ^ n)
  • ミキシング関数でマジックナンバーを乗算またはシフトする
  • 入力値

実際にはこれらの値のいくつかしか制御できないため、少し余分な注意が必要です。


4

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で、次にそれ自体を減算するため、安価なリソースが必要です。


3

確かではありませんが、彼らが素数のいくつかのサンプルをテストし、31が可能な文字列のいくつかのサンプルに対して最良の分布を与えることがわかったと思います。


1

これは、31には優れた特性があるためです。その乗算は、標準の乗算よりも速いビット単位のシフトで置き換えることができます。

31 * i == (i << 5) - i
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.