大きなHashMapを作成したいのですが、put()
のパフォーマンスが十分ではありません。何か案は?
他のデータ構造の提案も歓迎しますが、Javaマップの検索機能が必要です。
map.get(key)
私の場合、2600万エントリのマップを作成します。標準のJava HashMapを使用すると、2〜3百万件の挿入後、putレートが耐えられないほど遅くなります。
また、キーに異なるハッシュコード分布を使用すると役立つかどうかを知っていますか?
私のハッシュコード方法:
byte[] a = new byte[2];
byte[] b = new byte[3];
...
public int hashCode() {
int hash = 503;
hash = hash * 5381 + (a[0] + a[1]);
hash = hash * 5381 + (b[0] + b[1] + b[2]);
return hash;
}
等しいオブジェクトが同じハッシュコードを持つことを保証するために、加算の結合プロパティを使用しています。配列は0〜51の範囲の値を持つバイトです。値はいずれかの配列で1回だけ使用されます。 a配列に同じ値が含まれていれば(どちらの順序でも)オブジェクトは等しく、b配列にも同じことが言えます。したがって、a = {0,1} b = {45,12,33}とa = {1,0} b = {33,45,12}は等しくなります。
編集、いくつかのメモ:
ハッシュマップまたはその他のデータ構造を使用して2600万のエントリを保存することを批判している人もいます。なぜこれが奇妙に見えるのかわかりません。私には古典的なデータ構造とアルゴリズムの問題のように見えます。 2,600万のアイテムがあり、それらをデータ構造にすばやく挿入して検索できるようにしたいと考えています。データ構造とアルゴリズムを教えてください。
デフォルトのJava HashMapの初期容量を2,600万に設定減少パフォーマンス。
一部の人々は、データベースを使用することを提案していますが、他の状況では間違いなく賢いオプションです。しかし、私は本当にデータ構造とアルゴリズムの質問をしています、完全なデータベースは過剰なものであり、優れたデータ構造ソリューションよりもはるかに遅いでしょう(すべてのデータベースは単なるソフトウェアですが、通信とおそらくディスクオーバーヘッドがあるためです)。
多くの人が指摘したように、hashCode()
メソッドは非難するものでした。 2,600万の個別オブジェクトに対して約20,000コードしか生成していませんでした。これは、ハッシュバケットあたり平均1,300個のオブジェクト=非常に非常に悪いです。ただし、2つの配列をベース52の数値に変換すると、すべてのオブジェクトに対して一意のハッシュコードを取得することが保証されます。
public int hashCode() {
// assume that both a and b are sorted
return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);
}
public static int powerOf52(byte b, int power) {
int result = b;
for (int i = 0; i < power; i++) {
result *= 52;
}
return result;
}
配列は、このメソッドが等しいオブジェクトが同じハッシュコードを持つというhashCode()
コントラクトを満たすようにソートされます。古い方法を使用した場合、100,000のブロック(100,000〜2,000,000のブロック)での1秒あたりの平均プット数は次のとおりです。
168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083
新しい方法を使用すると、次のことができます。
337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25
はるかに良い。古い方法はすぐに終了しましたが、新しい方法は良好なスループットを維持しました。
hashCode()
メソッドで気づいたことの1つは、配列a[]
およびb[]
の要素の順序は重要ではないということです。したがって、(a[]={1,2,3}, b[]={99,100})
は(a[]={3,1,2}, b[]={100,99})
と同じ値にハッシュされます。実際には、すべてのキーk1
およびk2
で、sum(k1.a)==sum(k2.a)
およびsum(k1.b)=sum(k2.b)
は衝突を引き起こします。配列の各位置に重みを割り当てることをお勧めします。
hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);
ここで、c0
、c1
およびc3
は個別定数です(必要に応じて、b
に異なる定数を使用できます)。それは物事をもう少し均一にする必要があります。
パスカルを詳しく説明するには:HashMapの仕組みを理解していますか?ハッシュテーブルにいくつかのスロットがあります。各キーのハッシュ値が検出され、テーブルのエントリにマップされます。 2つのハッシュ値が同じエントリにマップされる場合(「ハッシュ衝突」)、HashMapはリンクリストを作成します。
ハッシュの衝突は、ハッシュマップのパフォーマンスを低下させる可能性があります。極端な場合、すべてのキーが同じハッシュコードを持っている場合、または異なるハッシュコードを持っているがすべて同じスロットにマップする場合、ハッシュマップはリンクリストになります。
パフォーマンスの問題が発生している場合、最初に確認することは次のとおりです。ランダムに見えるハッシュコードの分布が得られますか?そうでない場合は、より良いハッシュ関数が必要です。さて、この場合の「より良い」とは、「特定のデータセットにとってより良い」という意味です。同様に、文字列を使用していて、ハッシュ値の文字列の長さを取得したとします。 (JavaのString.hashCodeの動作方法ではなく、単純な例を作成しています。)文字列の長さが1〜10,000で大きく異なり、その範囲全体にかなり均等に分散している場合、これは非常に良いことです。ハッシュ関数。ただし、文字列がすべて1文字または2文字の場合、これは非常に悪いハッシュ関数になります。
編集:追加する必要があります:新しいエントリを追加するたびに、HashMapはこれが重複しているかどうかを確認します。ハッシュ衝突が発生した場合、着信スロットをそのスロットにマッピングされたすべてのキーと比較する必要があります。したがって、すべてが単一のスロットにハッシュされる最悪の場合、2番目のキーは最初のキーと比較され、3番目のキーは#1および#2と比較され、4番目のキーは#1、#2、および#3と比較されますなど。キー#1ミリオンに到達するまでに、1兆回以上の比較を行いました。
@オスカー:うーん、私はそれがどのように「実際に」ではないかわかりません。それは「明確にする」ようです。ただし、既存のエントリと同じキーを使用して新しいエントリを作成すると、最初のエントリが上書きされるのは事実です。それは私が最後の段落で重複を探すことについて話したときの意味です:キーが同じスロットにハッシュするたびに、HashMapはそれが既存のキーの重複であるか、またはそれらが一致するだけで同じスロットにあるかどうかをチェックする必要がありますハッシュ関数。それがHashMapの「全体のポイント」であることはわかりません。「全体のポイント」とは、キーで要素をすばやく取得できるということです。
しかし、とにかく、それは私が作ろうとしていた「全体のポイント」には影響しません。テーブルの同じスロットにマップする2つのキー(はい、同じキーではなく、異なるキー)がある場合、HashMapはリンクリストを作成します。次に、新しいキーをそれぞれチェックして、実際に既存のキーの複製であるかどうかを確認する必要があるため、この同じスロットにマップする新しいエントリを追加しようとするたびに、リンクされたリストを追跡し、既存の各エントリを調べてこれを確認する必要があります以前に見たキーの複製、または新しいキーの場合。
元の投稿のずっと後に更新
投稿してから6年後にこの回答に賛成票を投じたところ、質問を読み直すことになりました。
質問で与えられたハッシュ関数は、2600万のエントリに対して適切なハッシュではありません。
A [0] + a [1]とb [0] + b [1] + b [2]を加算します。彼は、各バイト範囲の値が0〜51であるため、(51 * 2 + 1)*(51 * 3 + 1)= 15,862個のハッシュ値のみが得られると述べています。 2,600万エントリの場合、これはハッシュ値あたり平均約1639エントリを意味します。それはたくさんの衝突であり、リンクされたリストを通してたくさんの連続した検索を必要とします。
OPは、配列aと配列b内の異なる順序は等しい、つまり[[1,2]、[3,4,5]]。equals([[2,1]、[5,3,4] ])、そして契約を履行するために、彼らは等しいハッシュコードを持たなければなりません。はい。それでも、15,000を超える可能性のある値があります。 2番目に提案されたハッシュ関数ははるかに優れており、より広い範囲を提供します。
他の誰かがコメントしたように、ハッシュ関数が他のデータを変更することは不適切と思われます。オブジェクトを作成するときにオブジェクトを「正規化」するか、配列のコピーからハッシュ関数を機能させる方が理にかなっています。また、関数を使用するたびに定数を計算するためにループを使用するのは非効率的です。ここには4つの値しかないので、
return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;
これにより、コンパイラーはコンパイル時に計算を1回実行します。または、クラスに4つの静的定数が定義されています。
また、ハッシュ関数の最初のドラフトには、出力の範囲に追加するものは何もない、いくつかの計算があります。クラスからの値を考慮する前に、彼は最初にハッシュを= 503に設定し、5301を乗算することに注意してください。そのため、実質的に彼はすべての値に503 * 5381を追加します。これは何を達成しますか?すべてのハッシュ値に定数を追加すると、有用なことを何も達成せずにCPUサイクルが消費されます。ここでのレッスン:ハッシュ関数に複雑さを追加することは目標ではありません。目標は、複雑さのために複雑さを追加するだけでなく、さまざまな値の幅広い範囲を取得することです。
「オン/オフトピック」の灰色の領域に入りますが、HashMapの要素数を減らすため、より多くのハッシュ衝突が良いことであるというオスカーレイエスの提案に関する混乱を排除するために必要です。私はオスカーが言っていることを誤解するかもしれませんが、私だけではないようです:kdgregory、delfuego、Nash0、そして私はすべて同じ(誤)の理解を共有しているようです。
オスカーが同じハッシュコードを持つ同じクラスについて言っていることを理解しているなら、彼は与えられたハッシュコードを持つクラスの1つのインスタンスのみがHashMapに挿入されることを提案しています。たとえば、ハッシュコードが1のSomeClassのインスタンスと、ハッシュコードが1のSomeClassの2番目のインスタンスがある場合、SomeClassの1つのインスタンスのみが挿入されます。
http://Pastebin.com/f20af40b9 のJava Pastebinの例は、上記がオスカーが提案していることを正しく要約していることを示しているようです。
理解または誤解に関係なく、同じクラスの異なるインスタンスが何が起こるかは同じですnot同じハッシュコードを持つ場合、HashMapに一度だけ挿入されます-キーが等しいかどうかが決定されるまで。ハッシュコードコントラクトでは、等しいオブジェクトが同じハッシュコードを持っている必要があります。ただし、等しくないオブジェクトが異なるハッシュコードを持つ必要はありません(これは他の理由で望ましい場合があります)[1]。
Pastebin.com/f20af40b9の例(オスカーは少なくとも2回言及しています)が続きますが、printlinesではなくJUnitアサーションを使用するようにわずかに変更されています。この例は、同じハッシュコードが衝突を引き起こし、クラスが同じ場合に1つのエントリのみが作成されるという提案をサポートするために使用されます(たとえば、この特定のケースでは1つのストリングのみ)。
@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
String s = new String("ese");
String ese = new String("ese");
// same hash right?
assertEquals(s.hashCode(), ese.hashCode());
// same class
assertEquals(s.getClass(), ese.getClass());
// AND equal
assertTrue(s.equals(ese));
Map map = new HashMap();
map.put(s, 1);
map.put(ese, 2);
SomeClass some = new SomeClass();
// still same hash right?
assertEquals(s.hashCode(), ese.hashCode());
assertEquals(s.hashCode(), some.hashCode());
map.put(some, 3);
// what would we get?
assertEquals(2, map.size());
assertEquals(2, map.get("ese"));
assertEquals(3, map.get(some));
assertTrue(s.equals(ese) && s.equals("ese"));
}
class SomeClass {
public int hashCode() {
return 100727;
}
}
ただし、ハッシュコードは完全なストーリーではありません。 Pastebinの例が無視しているのは、s
とese
の両方が等しいという事実です。これらは両方とも文字列「ese」です。したがって、s
またはese
または"ese"
をキーとして使用してマップのコンテンツを挿入または取得することは、s.equals(ese) && s.equals("ese")
であるため、すべて同等です。
2番目のテストは、同じクラスの同一のハッシュコードが、テスト_map.put(ese, 2)
が呼び出されたときにキー->値s -> 1
がese -> 2
によって上書きされる理由であると結論付けるのは誤りであることを示します。テスト2では、s
とese
は同じハッシュコード(assertEquals(s.hashCode(), ese.hashCode());
で検証)を保持し、同じクラスです。ただし、s
およびese
は、このテストではJava MyString
インスタンスではなくString
インスタンスです-このテストに関連する唯一の違いは等しい:上記のテスト1ではString s equals String ese
、テスト2ではMyStrings s does not equal MyString ese
:
@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
MyString s = new MyString("ese");
MyString ese = new MyString("ese");
// same hash right?
assertEquals(s.hashCode(), ese.hashCode());
// same class
assertEquals(s.getClass(), ese.getClass());
// BUT not equal
assertFalse(s.equals(ese));
Map map = new HashMap();
map.put(s, 1);
map.put(ese, 2);
SomeClass some = new SomeClass();
// still same hash right?
assertEquals(s.hashCode(), ese.hashCode());
assertEquals(s.hashCode(), some.hashCode());
map.put(some, 3);
// what would we get?
assertEquals(3, map.size());
assertEquals(1, map.get(s));
assertEquals(2, map.get(ese));
assertEquals(3, map.get(some));
}
/**
* NOTE: equals is not overridden so the default implementation is used
* which means objects are only equal if they're the same instance, whereas
* the actual Java String class compares the value of its contents.
*/
class MyString {
String i;
MyString(String i) {
this.i = i;
}
@Override
public int hashCode() {
return 100727;
}
}
後のコメントに基づいて、オスカーは彼が以前に言ったことを覆すようであり、平等の重要性を認めている。しかし、「同じクラス」ではなく、「等しい」が重要であるという概念はまだ明らかではないようです(強調鉱山):
"実際にはありません。ハッシュが同じであるが、キーが異なる場合にのみリストが作成されます。たとえば、Stringがハッシュコード2345を与え、Integerが同じハッシュコード2345を与える場合、整数がリストはString.equals(Integer)がfalseであるためです。ただし、同じクラス(または少なくとも.equalsがtrueを返す)の場合、同じエントリはたとえば、キーとして使用されるnew String( "one")と `new String(" one ")は、同じエントリを使用します。実際、これは最初のHashMapの完全なポイントです!自分で参照してください:Pastebin.com/ f20af40b9 –オスカーレイエス "
対同等の言及なしで、同一のクラスと同じハッシュコードの重要性を明示的に扱う以前のコメントに対して:
"@ delfuego:参照してください:Pastebin.com/f20af40b9この質問では、同じクラスが使用されています(ちょっと待って、同じクラスが正しく使用されていますか?)これは、同じハッシュがused同じエントリが使用され、エントリの「リスト」はありません。– Oscar Reyes "
または
"実際にこれによりパフォーマンスが向上します。衝突が多いほど、ハッシュテーブルのエントリが少なくなります。実行する作業が少なくなります。ハッシュ(見栄えが良い)でもハッシュテーブル(見栄えが良い)でもありませんパフォーマンスが低下しているオブジェクトの作成です。–オスカーレイエス "
または
"@ kdgregory:はい。ただし、衝突が異なるクラスで発生する場合にのみ、同じクラス(この場合)に対して同じエントリが使用されます。– Oscar Reyes"
繰り返しますが、オスカーが実際に言おうとしていたことを誤解するかもしれません。しかし、彼の最初のコメントは十分な混乱を引き起こしているため、いくつかの明示的なテストですべてをクリアするのが賢明と思われるため、長引く疑問はありません。
[1]-Effective Java、Second Edition Joshua Blochから:
アプリケーションの実行中に同じオブジェクトで2回以上呼び出される場合、hashCodeメソッドは、オブジェクトのequal s比較で使用される情報が変更されない限り、常に同じ整数を返す必要があります。この整数は、あるアプリケーションの実行から同じアプリケーションの別の実行まで一貫性を保つ必要はありません。
Equal s(Obj ect)メソッドに従って2つのオブジェクトが等しい場合、2つのオブジェクトのそれぞれでhashCodeメソッドを呼び出すと、同じ整数の結果が生成される必要があります。
Equal s(Object)メソッドに従って2つのオブジェクトが等しくない場合、2つのオブジェクトのそれぞれでhashCodeメソッドを呼び出すと、異なる整数結果が生成される必要はありません。ただし、プログラマは、等しくないオブジェクトに対して個別の整数結果を生成すると、ハッシュテーブルのパフォーマンスが向上する可能性があることに注意する必要があります。
私は3つのアプローチを提案します:
Javaをより多くのメモリで実行します。たとえば、256メガバイトで実行するにはJava -Xmx256M
を実行します。必要に応じてさらに使用し、大量のRAMを使用します。
別のポスターで提案されているように、計算されたハッシュ値をキャッシュして、各オブジェクトがそのハッシュ値を一度だけ計算するようにします。
より良いハッシュアルゴリズムを使用します。あなたが投稿したものは、a = {1、0}の場合と同じハッシュを返します。
Javaが無料で提供するものを活用してください。
public int hashCode() {
return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}
データの正確な性質に依存しますが、これは既存のhashCodeメソッドよりも衝突する可能性がはるかに低いと確信しています。
私の最初のアイデアは、HashMapを適切に初期化することを確認することです。 HashMapのJavaDocs から:
HashMapのインスタンスには、パフォーマンスに影響を与える2つのパラメーターがあります:初期容量と負荷係数です。容量はハッシュテーブル内のバケットの数であり、初期容量は単にハッシュテーブルが作成された時点の容量です。負荷率とは、ハッシュテーブルの容量が自動的に増加するまでにハッシュテーブルがどれだけいっぱいになるかを示す尺度です。ハッシュテーブルのエントリ数が負荷係数と現在の容量の積を超えると、ハッシュテーブルは再ハッシュされます(つまり、内部データ構造が再構築されます)。これにより、ハッシュテーブルのバケット数は約2倍になります。
したがって、小さすぎるHashMapで開始する場合、サイズ変更が必要になるたびに、allハッシュが再計算されます... 300万から200万の挿入ポイントに到達したときに、あなたが感じていることです。
投稿されたhashCodeの配列がバイトである場合、多くの重複が発生する可能性があります。
a [0] + a [1]は常に0〜512です。bを追加すると、常に0〜768の数値になります。これらを乗算すると、データが完全に分散していると仮定すると、400,000個の一意の組み合わせの上限が得られます各バイトのすべての可能な値の中。データが通常の場合、このメソッドの一意の出力ははるかに少ない可能性があります。
キーにパターンがある場合、マップを小さなマップに分割し、インデックスマップを作成できます。
例:キー:1,2,3、.... n個の100万個のマップ。インデックスマップ:1-1,000,000-> Map1 1,000,000-2,000,000-> Map2
したがって、2つのルックアップを実行しますが、キーセットは1,000,000対28,000,000になります。スティングパターンでも簡単にこれを行うことができます。
キーが完全にランダムな場合、これは機能しません
あなたが言及する2バイト配列があなたのキー全体であり、値が0から51の範囲で一意であり、aおよびb配列内の順序が重要でない場合、私の数学は、わずか約2600万の可能な順列と考えられるすべてのキーの値をマップに入力しようとしている可能性があります。
この場合、HashMapの代わりに配列を使用し、0から25989599のインデックスを付けると、データストアからの値の入力と取得の両方がもちろんはるかに高速になります。
私はここに遅れていますが、大きな地図についていくつかコメントしています:
私は、これらの地図が長寿命であると仮定しています。すなわち、あなたはそれらを投入し、それらはアプリの期間中ずっと動き続けます。私はまた、アプリ自体が長寿命であると仮定しています-ある種のサーバーのように。
Java HashMapの各エントリには、キー、値、およびそれらを結び付けるエントリの3つのオブジェクトが必要です。したがって、マップ内の26Mエントリは、26M * 3 == 78Mオブジェクトを意味します。これは、フルGCに達するまでは問題ありません。その後、世界の一時停止の問題が発生します。 GCは、78Mの各オブジェクトを調べ、それらがすべて生きていると判断します。 78M +個のオブジェクトは、見るだけのオブジェクトです。アプリがたまに長い(おそらく数秒)一時停止を許容できる場合、問題はありません。レイテンシー保証を達成しようとしている場合、大きな問題が発生する可能性があります(もちろん、レイテンシー保証が必要な場合、Javaは選択するプラットフォームではありません:))問題を大きく悪化させます。
この問題に対する優れた解決策は知りません。アイデア:
Javaの巨大なマップで多くの時間を費やした人からのいくつかの考え。
HashMapには初期容量があり、HashMapのパフォーマンスは、基礎となるオブジェクトを生成するhashCodeに非常に大きく依存しています。
両方を微調整してみてください。
私の場合、2600万エントリのマップを作成します。標準のJava HashMapを使用すると、2〜3百万件の挿入後、putレートが耐えられないほど遅くなります。
私の実験から(2009年の学生プロジェクト):
注:「プライムツリー」は、100万から1000万の「連続キー」で最適に機能します。 HashMapのようなキーを使用するには、マイナーな調整が必要です。
それで、#PrimeTreeとは何ですか?要するに、それは二分木のようなツリーデータ構造であり、枝番号は素数です(「2」バイナリの代わりに)。
HSQLDB のようなメモリ内データベースの使用を試みることができます。
SQLite は、メモリ内で使用できるようにします。
最初に、Mapを正しく使用していること、キーに適したhashCode()メソッド、Mapの初期容量、適切なMap実装などを確認する必要があります。
次に、プロファイラーを使用して、実際に何が起こっているのか、どこで実行時間が費やされているのかを確認することをお勧めします。たとえば、hashCode()メソッドは何十億回も実行されていますか?
それでも解決しない場合は、 EHCache または memcached ?はい、キャッシング用の製品ですが、十分な容量を持ち、キャッシュストレージから値を排除しないように構成できます。
別のオプションは、完全なSQL RDBMSよりも軽いデータベースエンジンです。 Berkeley DB のようなものかもしれません。
私は個人的にこれらの製品のパフォーマンスの経験はありませんが、試してみる価値はあります。
計算されたハッシュコードをキーオブジェクトにキャッシュしてみてください。
このようなもの:
public int hashCode() {
if(this.hashCode == null) {
this.hashCode = computeHashCode();
}
return this.hashCode;
}
private int computeHashCode() {
int hash = 503;
hash = hash * 5381 + (a[0] + a[1]);
hash = hash * 5381 + (b[0] + b[1] + b[2]);
return hash;
}
もちろん、hashCodeが初めて計算された後、キーの内容を変更しないように注意する必要があります。
編集:各キーをマップに1回だけ追加する場合、キャッシュにはコード値が含まれているように思えます。他の状況では、これが役立つ場合があります。
別の投稿者は、ハッシュコードの実装は、値を加算する方法が原因で多くの衝突が発生することをすでに指摘しています。デバッガでHashMapオブジェクトを見ると、非常に長いバケットチェーンを持つ200個の異なるハッシュ値を持っていることがわかります。
常に0..51の範囲の値がある場合、これらの値はそれぞれ6ビットで表現します。常に5つの値がある場合、左シフトと追加で30ビットのハッシュコードを作成できます。
int code = a[0];
code = (code << 6) + a[1];
code = (code << 6) + b[0];
code = (code << 6) + b[1];
code = (code << 6) + b[2];
return code;
左シフトは高速ですが、ハッシュコードは均等に分散されません(6ビットは範囲0..63を意味するため)。別の方法は、ハッシュに51を掛けて各値を追加することです。これはまだ完全には分散されず(たとえば、{2,0}と{1,52}は衝突します)、シフトよりも遅くなります。
int code = a[0];
code *= 51 + a[1];
code *= 51 + b[0];
code *= 51 + b[1];
code *= 51 + b[2];
return code;
In 有効なJava:プログラミング言語ガイド(Javaシリーズ)
第3章では、hashCode()を計算するときに従うべき適切なルールを見つけることができます。
特に:
フィールドが配列の場合、各要素が個別のフィールドであるかのように扱います。つまり、これらのルールを再帰的に適用することにより、重要な要素ごとにハッシュコードを計算し、ステップ2.bでこれらの値を組み合わせます。配列フィールドのすべての要素が重要な場合、リリース1.5で追加されたArrays.hashCodeメソッドのいずれかを使用できます。
指摘したように、ハッシュコードの実装は衝突が多すぎるため、それを修正すると、まともなパフォーマンスが得られるはずです。さらに、hashCodeをキャッシュし、equalsを効率的に実装すると役立ちます。
さらに最適化する必要がある場合:
あなたの説明では、(52 * 51/2)*(52 * 51 * 50/6)= 29304600の異なるキーのみがあります(そのうち26000000、つまり約90%が存在します)。したがって、衝突することなくハッシュ関数を設計し、ハッシュマップではなく単純な配列を使用してデータを保持し、メモリ消費を削減し、ルックアップ速度を向上させることができます。
T[] array = new T[Key.maxHashCode];
void put(Key k, T value) {
array[k.hashCode()] = value;
T get(Key k) {
return array[k.hashCode()];
}
(一般に、クラスター化する効率的で衝突のないハッシュ関数を設計することは不可能であるため、HashMapは衝突を許容し、オーバーヘッドが発生します)
a
とb
がソートされていると仮定すると、次のハッシュ関数を使用できます。
public int hashCode() {
assert a[0] < a[1];
int ahash = a[1] * a[1] / 2
+ a[0];
assert b[0] < b[1] && b[1] < b[2];
int bhash = b[2] * b[2] * b[2] / 6
+ b[1] * b[1] / 2
+ b[0];
return bhash * 52 * 52 / 2 + ahash;
}
static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;
これは衝突のないものだと思います。これを証明することは、数学的に傾いた読者のための演習として残されています。
埋め込みデータベースを使用してこれを行うことを検討しましたか。 Berkeley DB を見てください。現在はOracleが所有するオープンソースです。
すべてをキーと値のペアとして保存します。RDBMSではありません。そして、それは高速であることを目指しています。
最初に大きなマップを割り当てます。 2,600万のエントリがあり、そのメモリがある場合は、new HashMap(30000000)
を実行します。
本当に2,600万のキーと値を持つ2,600万のエントリに十分なメモリがありますか?これは私にとって多くの記憶のように聞こえます。ガベージコレクションは200万から300万のマークでまだ正常に実行されていると確信していますか?それがボトルネックだと想像できました。
使用される一般的なハッシュ方法は、大規模なセットにはあまり適していません。上で指摘したように、使用されるハッシュは特に悪いです。より良いのは、BuzHash( http://www.Java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htm ))
次の2つのことを試すことができます。
hashCode
メソッドが、連続したintなど、よりシンプルで効果的なものを返すようにします
次のようにマップを初期化します。
Map map = new HashMap( 30000000, .95f );
これらの2つのアクションは、構造の再ハッシュの量を大幅に削減し、非常に簡単にテストできます。
それでも解決しない場合は、RDBMSなどの別のストレージの使用を検討してください。
編集
初期容量を設定すると、ケースのパフォーマンスが低下するのは奇妙です。
javadocs から参照してください:
初期容量が、エントリの最大数を負荷係数で割った値より大きい場合、再ハッシュ操作は発生しません。
私はマイクロビーチマークを作成しました(これは決して決定的なものではありませんが、少なくともこの点を証明しています)
$cat Huge*Java import Java.util.*; public class Huge { public static void main( String [] args ) { Map map = new HashMap( 30000000 , 0.95f ); for( int i = 0 ; i < 26000000 ; i ++ ) { map.put( i, i ); } } } import Java.util.*; public class Huge2 { public static void main( String [] args ) { Map map = new HashMap(); for( int i = 0 ; i < 26000000 ; i ++ ) { map.put( i, i ); } } } $time Java -Xms2g -Xmx2g Huge real 0m16.207s user 0m14.761s sys 0m1.377s $time Java -Xms2g -Xmx2g Huge2 real 0m21.781s user 0m20.045s sys 0m1.656s $
したがって、初期容量の使用は、リハイジングのために21秒から16秒に低下します。 hashCode
メソッドを「機会の領域」として残します;)
編集
あなたの最後の版に従って。
アプリケーションを実際にプロファイルし、メモリ/ CPUが消費されている場所を確認する必要があると思います。
同じhashCode
を実装するクラスを作成しました
そのハッシュコードは何百万もの衝突を与え、HashMapのエントリは劇的に削減されます。
以前のテストでの21秒、16秒から10秒と8秒に合格しました。その理由は、hashCodeが多数の衝突を引き起こし、あなたが思う26Mのオブジェクトを保存するのではなく、はるかに少ない数(私が言う20k程度)を保存するからです。
問題はハッシュマップではありませんはコードのどこかにあります。
プロファイラーを入手し、どこで見つけるかについてです。アイテムの作成中か、おそらくディスクに書き込んでいるか、ネットワークからデータを受信していると思います。
これがクラスの実装です。
note私はあなたがしたように0-51の範囲を使用しませんでしたが、私の値に-126から127を使用して繰り返しました、それは私がこのテストをしたからです質問を更新する前に
唯一の違いは、クラスの衝突が多くなるため、マップに保存されるアイテムが少なくなることです。
import Java.util.*;
public class Item {
private static byte w = Byte.MIN_VALUE;
private static byte x = Byte.MIN_VALUE;
private static byte y = Byte.MIN_VALUE;
private static byte z = Byte.MIN_VALUE;
// Just to avoid typing :)
private static final byte M = Byte.MAX_VALUE;
private static final byte m = Byte.MIN_VALUE;
private byte [] a = new byte[2];
private byte [] b = new byte[3];
public Item () {
// make a different value for the bytes
increment();
a[0] = z; a[1] = y;
b[0] = x; b[1] = w; b[2] = z;
}
private static void increment() {
z++;
if( z == M ) {
z = m;
y++;
}
if( y == M ) {
y = m;
x++;
}
if( x == M ) {
x = m;
w++;
}
}
public String toString() {
return "" + this.hashCode();
}
public int hashCode() {
int hash = 503;
hash = hash * 5381 + (a[0] + a[1]);
hash = hash * 5381 + (b[0] + b[1] + b[2]);
return hash;
}
// I don't realy care about this right now.
public boolean equals( Object other ) {
return this.hashCode() == other.hashCode();
}
// print how many collisions do we have in 26M items.
public static void main( String [] args ) {
Set set = new HashSet();
int collisions = 0;
for ( int i = 0 ; i < 26000000 ; i++ ) {
if( ! set.add( new Item() ) ) {
collisions++;
}
}
System.out.println( collisions );
}
}
このクラスを使用すると、前のプログラムのキーがあります
map.put( new Item() , i );
私に与えます:
real 0m11.188s
user 0m10.784s
sys 0m0.261s
real 0m9.348s
user 0m9.071s
sys 0m0.161s
少し前にリストとハッシュマップで小さなテストを行いましたが、面白いのはリストを繰り返し処理し、オブジェクトを見つけるのにハッシュマップのget関数を使用するのと同じ時間がミリ秒単位でかかっていたことです...ああ、そのサイズのハッシュマップを操作するときのメモリは大きな問題です。
同期する必要がある場合は、使用してみてください
http://commons.Apache.org/collections/api/org/Apache/commons/collections/FastHashMap.html