Java HashMapパフォーマンスの最適化/代替


102

大きなHashMapを作成したいのですが、put()パフォーマンスが十分ではありません。何か案は?

他のデータ構造の提案も歓迎しますが、Javaマップの検索機能が必要です。

map.get(key)

私の場合、2,600万エントリのマップを作成します。標準のJava HashMapを使用すると、200万から300万の挿入後、書き込み速度が耐えられないほど遅くなります。

また、キーに異なるハッシュコードの分布を使用することが役立つかどうか誰かが知っていますか?

私のハッシュコード方法:

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;
}

同等のオブジェクトが同じハッシュコードを持つようにするために、additionの連想プロパティを使用しています。配列は0から51の範囲の値を持つバイトです。値はどちらの配列でも1回だけ使用されます。a配列に同じ値(どちらの順序でも)が含まれ、b配列にも同じ場合、オブジェクトは等しくなります。したがって、a = {0,1} b = {45,12,33}とa = {1,0} b = {33,45,12}は等しくなります。

編集、いくつかのメモ:

  • ハッシュマップやその他のデータ構造を使用して2,600万のエントリを保存することを非難する人もいます。なぜこれが奇妙に見えるのかわかりません。私には、古典的なデータ構造とアルゴリズムの問​​題のように見えます。私は2600万個のアイテムを持っているので、それらをすばやくデータ構造に挿入してデータ構造から検索できるようにしたいと思います。データ構造とアルゴリズムを教えてください。

  • デフォルトのJava HashMapの初期容量を2,600万に設定すると、パフォーマンスが低下します。

  • 一部の人々は、データベースの使用を提案しましたが、他のいくつかの状況では、それは間違いなくスマートなオプションです。しかし、私は本当にデータ構造とアルゴリズムの質問をしています。完全なデータベースはやりすぎであり、優れたデータ構造ソリューションよりもはるかに遅くなります(すべてのデータベースは単なるソフトウェアですが、通信とディスクオーバーヘッドが発生する可能性があります)。


29
HashMapが遅くなる場合は、おそらくハッシュ関数が十分ではありません。
Pascal Cuoq 09年

12
医師は、それが痛い私が行うときに、この
skaffman

12
これは本当に良い質問です。ハッシュアルゴリズムが重要である理由とパフォーマンスにどのような影響を与える可能性があるかを示す良いデモ
oxbow_lakes 2009年

12
aの合計の範囲は0〜102で、bの合計の範囲は0〜153であるため、可能なハッシュ値は15,606だけで、同じhashCodeを持つキーの平均は1,666です。可能なハッシュコードの数がキーの数よりもはるかに多くなるように、ハッシュコードを変更する必要があります。
Peter Lawrey、

6
私はあなたがテキサスホールデムポーカーをモデリングしていると
心から決めました

回答:


56

多くの人々が指摘したように、その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〜2,000,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

はるかに良い。新しい方法が良好なスループットを維持している間、古い方法は非常に速く終了しました。


17
hashCodeメソッドの配列を変更しないことをお勧めします。慣例によりhashCode、オブジェクトの状態は変更されません。おそらく、コンストラクタはそれらをソートするためのより良い場所でしょう。
マイケルマイヤーズ

配列のソートはコンストラクターで行う必要があることに同意します。示されているコードは、hashCodeを設定するようには見えません。コードの計算は、次のように簡単に行うことができますint result = a[0]; result = result * 52 + a[1]; //etc
rsp

コンストラクタで並べ替えてから、mmyersとrspが示唆するようにハッシュコードを計算する方がよいことに同意します。私の場合、私の解決策は許容可能であり、機能するためには配列をソートする必要があるという事実を強調したかったのhashCode()です。
ナッシュ

3
ハッシュコードをキャッシュすることもできます(オブジェクトが変更可能な場合は適切に無効化します)。
NateS 2010

1
java.util.Arrays.hashCode()を使用するだけです。それはより簡単で(自分で記述して保守するコードはありません)、その計算はおそらくより高速で(乗算が少ない)、そのハッシュコードの分布はおそらくより均一になります。
jcsahnwaldt Reinstate Monica、2018

18

あなたのhashCode()メソッドで私が気づく1つのことは、配列内の要素の順序であり、重要a[]b[]はないということです。したがって(a[]={1,2,3}, b[]={99,100})、と同じ値にハッシュされ(a[]={3,1,2}, b[]={100,99})ます。実際にはすべてのキーk1k2場所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]);

どこ、c0c1c3している個別の定数(あなたのために別の定数を使用することができb、必要に応じて)。それはもう少し均等になるはずです。


また、同じ要素を異なる順序で持つ配列が同じハッシュコードを返すというプロパティが必要なため、機能しません。
ナッシュ

5
その場合、52C2 + 52C3のハッシュコード(計算機によると23426)があり、ハッシュマップはジョブにとって非常に間違ったツールです。
kdgregory 2009年

実際には、これによりパフォーマンスが向上します。衝突数が多いほど、ハッシュテーブル式のエントリは少なくなります。実行する作業が少なくなります。ハッシュ(見栄えが良い)でも、ハッシュテーブル(うまく機能する)でもないのは、パフォーマンスが低下しているオブジェクトの作成にあると思います。
OscarRyz 2009年

7
@Oscar- ハッシュチェーンの線形検索を実行する必要があるため、衝突が増えると、実行する作業が増えます。equals()ごとに26,000,000個の異なる値、およびhashCode()ごとに26,000個の異なる値がある場合、バケットチェーンにはそれぞれ1,000個のオブジェクトがあります。
kdgregory 2009年

@ Nash0:あなたは、これらに同じhashCodeを持たせたいが、同時に等しくないようにしたいと言っているようです(equals()メソッドで定義)。なぜあなたはそれをしたいですか?
MAK、

17

Pascalについて詳しく説明します。HashMapの仕組みを理解していますか?ハッシュテーブルにいくつかのスロットがあります。各キーのハッシュ値が見つかり、テーブルのエントリにマッピングされます。2つのハッシュ値が同じエントリにマップされる場合(「ハッシュ衝突」)、HashMapはリンクリストを作成します。

ハッシュの衝突は、ハッシュマップのパフォーマンスを低下させる可能性があります。極端なケースでは、すべてのキーが同じハッシュコードを持っている場合、または異なるハッシュコードを持っているがすべて同じスロットにマッピングされている場合、ハッシュマップはリンクリストに変わります。

パフォーマンスの問題が発生している場合、最初に確認することは次のとおりです。ランダムに見えるハッシュコードの分布を取得していますか?そうでない場合は、より優れたハッシュ関数が必要です。この場合の「より良い」とは、「特定のデータセットにとってより良い」ことを意味します。同様に、文字列を処理していて、ハッシュ値に文字列の長さを使用したとします。(JavaのString.hashCodeがどのように機能するかではなく、単純な例を作成しています。)文字列の長さが1から10,000まで幅広く変化し、その範囲全体にかなり均等に分散されている場合、これは非常に良いことです。ハッシュ関数。しかし、文字列がすべて1または2文字である場合、これは非常に悪いハッシュ関数になります。

編集:追加する必要があります:新しいエントリを追加するたびに、HashMapはこれが重複しているかどうかをチェックします。ハッシュの衝突がある場合は、着信キーをそのスロットにマップされたすべてのキーと比較する必要があります。したがって、すべてが1つのスロットにハッシュされる最悪の場合、2番目のキーは最初のキーと比較され、3番目のキーは#1および#2と比較され、4番目のキーは#1、#2、および#3と比較されます、など。キー100万に到達するまでに、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;

これにより、コンパイラはコンパイル時に一度計算を実行します。または、クラスで4つの静的定数が定義されています。

また、ハッシュ関数の最初のドラフトには、出力の範囲に何も追加しない計算がいくつかあります。クラスの値を考慮する前に、最初にハッシュ= 503を5381倍に設定することに注意してください。つまり...実際には、すべての値に503 * 5381を追加します。これは何を達成しますか?すべてのハッシュ値に定数を追加すると、有用なことを何も実行せずにCPUサイクルが燃焼するだけです。ここでのレッスン:ハッシュ関数に複雑さを追加することは目標ではありません。目標は、複雑さのために複雑さを追加するだけでなく、さまざまな異なる値を取得することです。


3
うん、悪いハッシュ関数はこの種の振る舞いをもたらすでしょう。+1
ヘニング

あんまり。リストは、ハッシュが同じで、キーが異なる場合にのみ作成されます。ので、文字列の所与のハッシュコード2345と整数が同じハッシュコード2345を与えた場合、例えば、その整数は、リストに挿入されています。ただし、同じクラスがある(または少なくともtrueを返す)場合は、同じエントリが使用されます。たとえば、キーとして使用される `new String(" one ")は、同じエントリを使用します。実際、これはHashMap の完全なポイントです。自分自身を参照してください:pastebin.com/f20af40b9String.equals( Integer )false.equalsnew String("one")
OscarRyz

3
@オスカー:元の投稿に追加された返信をご覧ください。
ジェイ

私は、これは非常に古いスレッドですけど、それはハッシュコードに関連し、ここで用語「衝突」のための参照です:リンク。同じキーを持つ別の値を入力してハッシュマップの値を置き換える場合、それは衝突と呼ばれません
Tahir Akhtar

@Tahirその通りです。おそらく私の投稿の言葉遣いは不十分だったのでしょう。説明をありがとう。
ジェイ

7

私の最初のアイデアは、HashMapを適切に初期化していることを確認することです。HashMapJavaDocsから:

HashMapのインスタンスには、そのパフォーマンスに影響を与える2つのパラメーターがあります。初期容量と負荷係数です。容量はハッシュテーブル内のバケット数であり、初期容量はハッシュテーブルが作成されたときの容量です。負荷係数は、ハッシュテーブルの容量が自動的に増加する前に、ハッシュテーブルがどの程度いっぱいになるかを示す尺度です。ハッシュテーブルのエントリ数が負荷係数と現在の容量の積を超えると、ハッシュテーブルが再ハッシュされる(つまり、内部データ構造が再構築される)ため、ハッシュテーブルのバケット数は約2倍になります。

したがって、小さすぎるHashMapから始めた場合、サイズを変更する必要があるたびに、すべてのハッシュが再計算されます。これは、200万から300万の挿入ポイントに到達したときに感じていることかもしれません。


今までに再計算されたとは思いません。テーブルのサイズが大きくなり、ハッシュが保持されます。
ヘニング

ハッシュマップはビットごとに、すべてのエントリに対して次の処理を実行します。newIndex= storedHash&newLength;
ヘニング

4
ハニング:おそらくデルフエゴ側の言葉遣いが不十分ですが、要点は有効です。はい、hashCode()の出力が再計算されないという意味で、ハッシュ値は再計算されません。ただし、テーブルのサイズが大きくなると、すべてのキーをテーブルに再挿入する必要があります。つまり、テーブルで新しいスロット番号を取得するには、ハッシュ値を再ハッシュする必要があります。
ジェイ

ジェイ、うん-言葉遣いが悪く、あなたが言ったこと。:)
delfuego

1
@delfuegoと@ nash0:はい、初期容量を要素の数と同じに設定すると、数百万の衝突が発生し、その容量を少量しか使用しないため、パフォーマンスが低下します。使用可能なすべてのエントリを使用する場合でも、同じ容量を設定すると、負荷が原因でより多くのスペースが要求されるため、最悪の状態になります!使用する必要がありますinitialcapactity = maxentries/loadcapacity(たとえば、30M、2,600万エントリの場合は0.95)。ただし、使用しているすべての衝突が約20k以下であるため、これは当てはまりません
OscarRyz 2009年

7

私は三叉のアプローチを提案します:

  1. メモリを増やしてJavaを実行しますjava -Xmx256M。たとえば、256メガバイトで実行します。必要に応じてさらに使用すると、RAMが大量に使用されます。

  2. 別の投稿者の提案に従って、計算されたハッシュ値をキャッシュするので、各オブジェクトはそのハッシュ値を1回だけ計算します。

  3. より良いハッシュアルゴリズムを使用します。あなたが投稿したものは、a = {1、0}の場合と同じハッシュを返し、それ以外はすべて等しいです。

Javaが無料で提供するものを利用してください。

public int hashCode() {
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}

これは、データの正確な性質に依存しますが、既存のhashCodeメソッドよりも衝突する可能性がはるかに少ないと確信しています。


RAMは、この種のマップや配列にとっては小さすぎるかもしれないので、すでにメモリ制限の問題を疑っています。
ReneS 2009年

7

「オン/オフトピック」の灰色の領域に入りますが、ハッシュコリジョンを増やすことはHashMap内の要素の数を減らすので良いことであるというOscar Reyesの提案に関する混乱を避けるために必要です。私はオスカーが言っていることを誤解しているかもしれませんが、私だけではないようです:kdgregory、delfuego、Nash0、そして私はすべて同じ(誤)理解を共有しているようです。

オスカーが同じハッシュコードの同じクラスについて言っていることを理解している場合、彼は、特定のハッシュコードを持つクラスの1つのインスタンスのみがHashMapに挿入されることを提案しています。たとえば、ハッシュコードが1のSomeClassのインスタンスと、ハッシュコードが1のSomeClassの2番目のインスタンスがある場合、SomeClassのインスタンスが1つだけ挿入されます。

http://pastebin.com/f20af40b9にある Javaペーストビンの例は、上記がオスカーの提案を正しく要約していることを示しているようです。

かかわらず、任意の理解や誤解のか、何が起こるかは、同じクラスの異なるインスタンスはないですありません、キーが等しいか否か判断だないまで-彼らは同じハッシュコードを持っている場合に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;
    }
}

ただし、ハッシュコードは完全な話ではありません。ペーストビンの例で無視されているのは、両方seseが等しいという事実です。どちらも文字列「ese」です。したがって、sor eseまたはor "ese"をキーとして使用したマップのコンテンツの挿入または取得は、すべて同等です。s.equals(ese) && s.equals("ese")

2番目のテストは、同じクラスの同一のハッシュコードが、テスト1でが呼び出さs -> 1れたese -> 2ときにkey- >値が上書きされる理由であると結論付けることが誤っていることを示していますmap.put(ese, 2)。テスト2では、seseまだ同じハッシュコードを持つ(によって確認されるようにassertEquals(s.hashCode(), ese.hashCode());)、彼らは同じクラスです。しかし、seseしているMyString。この試験でインスタンスしないのJava Stringインスタンス-この試験に等しいことに関連する唯一の違いを有する:String s equals String ese上記試験いずれかで、一方、MyStrings s does not equal MyString ese試験2において:

@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;
    }
}

後のコメントに基づいて、オスカーは彼が以前に言ったものを逆転させるようで、平等の重要性を認めます。ただし、等しいという概念は重要であり、「同じクラス」ではなく、明確ではないようです(強調は私のものです)。

「実際にはそうではありません。リストはハッシュが同じで、キーが異なる場合にのみ作成されます。たとえば、文字列がハッシュコード2345を与え、整数が同じハッシュコード2345を与える場合、整数はリストに挿入されます。 equals(Integer)はfalseです。ただし、同じクラスがある場合(または少なくとも.equalsがtrueを返す場合)、同じエントリが使用されます。たとえば、new String( "one")と `new String(" one ")は、キーは、同じエントリを使用します。実際には、これが最初のHashMapの完全なポイントです!自分で確認してください:pastebin.com/f20af40b9 – Oscar Reyes "

同等の記述がない、同一のクラスと同じハッシュコードの重要性を明示的に扱う以前のコメントとの比較:

「@delfuego:自分で確認してください:pastebin.com/f20af40b9したがって、この質問では同じクラスが使用されています(ちょっと待って、同じクラスが正しく使用されていますか?)これは、同じハッシュが同じエントリで使用されていることを意味しますが使用され、エントリの「リスト」がありません。– Oscar Reyes "

または

「実際には、これによりパフォーマンスが向上します。衝突が多いほどeqハッシュテーブルのエントリが少ないeq。実行する作業が少なくなります。ハッシュ(見栄えが良い)でも、ハッシュテーブル(うまく機能する)でもありません。オブジェクトにあると思いますパフォーマンスが低下している作品–オスカー・レイエス」

または

"@kdgregory:はい、ただし、異なるクラスで衝突が発生した場合のみ、同じクラス(この場合)に同じエントリが使用されます。–オスカーレイエス"

繰り返しになりますが、オスカーが実際に言おうとしていたことを誤解しているかもしれません。しかし、彼の最初のコメントは十分な混乱を引き起こし、いくつかの明示的なテストですべてをクリアすることが賢明であるように思われるので、疑いの余地はありません。


[1] -Joshua BlochによるEffective Java、Second Editionから:

  • アプリケーションの実行中に同じオブジェクトで2回以上呼び出される場合、オブジェクトのequals比較で使用される情報が変更されていなければ、hashCodeメソッドは常に同じ整数を返す必要があります。この整数は、アプリケーションのある実行から別の実行への一貫性を保つ必要はありません。

  • equal s(Obj ect)メソッドに従って2つのオブジェクトが等しい場合、2つのオブジェクトのそれぞれでhashCodeメソッドを呼び出すと、同じ整数の結果が生成される必要があります。

  • equal s(Object)メソッドに従って2つのオブジェクトが等しくない場合、2つのオブジェクトのそれぞれでhashCodeメソッドを呼び出すと、異なる整数の結果が生成される必要はありません。ただし、プログラマは、等しくないオブジェクトに対して異なる整数の結果を生成すると、ハッシュテーブルのパフォーマンスが向上する可能性があることに注意する必要があります。


5

投稿されたhashCodeの配列がバイトである場合、多くの場合、多くの重複が発生します。

a [0] + a [1]は常に0から512の間です。bを追加すると、常に0から768の間の数になります。これらを乗算すると、データが完全に分散していると仮定して、40万の一意の組み合わせの上限が得られます。各バイトのすべての可能な値の中。データがまったく規則的である場合、このメソッドの固有の出力がはるかに少ない可能性があります。


4

HashMapには初期容量があり、HashMapのパフォーマンスは、基礎となるオブジェクトを生成するhashCodeに大きく依存します。

両方を微調整してみてください。


4

キーにパターンがある場合は、マップをより小さなマップに分割し、インデックスマップを作成できます。

例:キー:1、2、3、... nそれぞれ100万の28個のマップ。インデックスマップ:1-1,000,000-> Map1 1,000,000-2,000,000-> Map2

したがって、2つのルックアップを実行しますが、キーセットは1,000,000対28,000,000になります。スティングパターンでもこれを簡単に行うことができます。

キーが完全にランダムである場合、これは機能しません


1
キーがランダムであっても、(key.hashCode()%28)を使用して、そのキーと値を格納するマップを選択できます。
JuhaSyrjälä、2009年

4

あなたが言及する2つのバイト配列がキー全体である場合、値は0〜51の範囲にあり、一意であり、a配列とb配列内の順序は重要ではありません。考えられるすべてのキーの値をマップに入力しようとしている可能性があります。

この場合、HashMapの代わりに配列を使用して0から25989599にインデックスを付けると、データストアからの値の入力と取得の両方がはるかに高速になります。


これは非常に良いアイデアであり、実際には、12億個の要素を持つ別のデータストレージの問題に対してもそうしています。この場合、私は簡単な方法を取り、事前に作成されたデータ構造を使用したいと思っていました:)
nash '19 / 11/09

4

私はここに遅れましたが、大きな地図についていくつかコメントがあります:

  1. 他の投稿で詳細に説明したように、優れたhashCode()を使用すると、マップ内の26Mエントリは大した問題ではありません。
  2. ただし、ここで潜在的に隠されている問題は、巨大マップのGCへの影響です。

私はこれらのマップが長命であると仮定しています。つまり、ユーザーがそれらを入力すると、アプリの実行中はそのまま残ります。また、アプリ自体が、ある種のサーバーのように長寿命であると想定しています。

Java HashMapの各エントリには3つのオブジェクトが必要です。キー、値、およびそれらを結び付けるエントリです。したがって、マップの26Mエントリは、26M * 3 == 78Mのオブジェクトを意味します。これは、フルGCに到達するまで問題ありません。次に、一時停止の問題があります。GCは78Mの各オブジェクトを調べ、それらがすべて生きていると判断します。78M以上のオブジェクトは、見るべきオブジェクトの数が多いだけです。アプリが不定期に長い(おそらく数秒)一時停止を許容できる場合、問題はありません。レイテンシの保証を達成しようとしている場合、大きな問題が発生する可能性があります(もちろん、レイテンシの保証が必要な場合、Javaはプラットフォームを選択するのではありません:))マップの値がすぐにチャーンする場合、頻繁にフルコレクションが発生する可能性があります。これは問題を大きく悪化させます。

この問題の優れた解決策を知りません。アイデア:

  • 完全なGCを「ほぼ」防止するために、GCとヒープサイズを調整することが可能な場合があります。
  • マップコンテンツが頻繁にチャーンする場合は、JavolutionのFastMapを試すことができます。これは、エントリオブジェクトをプールできるため、完全な収集の頻度を下げることができます。
  • 独自のマップ実装を作成し、byte []で明示的なメモリ管理を行うことができます(つまり、数百万のオブジェクトを1バイトにシリアル化することで、CPUをより予測可能なレイテンシと交換します[]-うーん!)
  • この部分ではJavaを使用しないでください-ソケットを介して予測可能な何らかのインメモリDBと話します
  • 新しいG1コレクターが役立つことを願っています(主にチャーンが多い場合に適用されます)

Javaで巨大なマップを使用して多くの時間を費やした人からのいくつかの考え。



3

私の場合、2,600万エントリのマップを作成します。標準のJava HashMapを使用すると、200万から300万の挿入後、書き込み速度が耐えられないほど遅くなります。

私の実験から(2009年の学生プロジェクト):

  • 1から100.000までの100.000ノードに対してRed Black Treeを構築しました。785.68秒(13分)かかりました。そして、100万ノードのRBTreeを構築できませんでした(HashMapでの結果のように)。
  • 「Prime Tree」を使用して、私のアルゴリズムのデータ構造。21.29秒(RAM:1.97Gb)以内に1000万ノードのツリー/マップを構築できました。検索Key-ValueコストはO(1)です。

注:「Prime Tree」は、100万から1000万の「連続キー」で最適に機能します。HashMapのようなキーを操作するには、いくつかのマイナー調整が必要です。


それでは、#PrimeTreeとは何ですか?要するに、これはBinary Treeのようなツリーデータ構造であり、ブランチ番号は( "2" -binaryではなく)素数です。


リンクや実装を教えてください。
ベンジ



1

これを行うために組み込みデータベースの使用を検討しましたか?Berkeley DBを見てください。それは現在オラクルが所有しているオープンソースです。

すべてをKey-> Valueペアとして格納します。これはRDBMSではありません。そしてそれは速いことを目指しています。


2
シリアル化/ IOオーバーヘッドのため、Berkeley DBはこの数のエントリに対して十分高速ではありません。ハッシュマップよりも速くなることは決してなく、OPは永続性を気にしません。あなたの提案は良いものではありません。
oxbow_lakes 2009年

1

最初に、他の多くの回答が説明するように、Mapを正しく使用していること、キーの適切なhashCode()メソッド、Mapの初期容量、正しいMapの実装などを確認する必要があります。

次に、プロファイラーを使用して、実際に何が行われていて、実行時間がどこで費やされているかを確認することをお勧めします。たとえば、hashCode()メソッドは何十億回も実行されていますか?

それでも問題が解決しない場合は、EHCachememcachedなどを使用してみませんか?はい、これらはキャッシング用の製品ですが、十分な容量があり、キャッシュストレージから値が削除されないように構成できます。

別のオプションは、完全なSQL RDBMSよりも軽量なデータベースエンジンです。Berkeley DBのようなものかもしれません。

個人的にこれらの製品のパフォーマンスの経験はありませんが、試してみる価値はあります。


1

計算されたハッシュコードをキーオブジェクトにキャッシュしてみることができます。

このようなもの:

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が初めて計算された後にキーの内容を変更しないように注意する必要があります。

編集:マップに各キーを一度だけ追加する場合、キャッシュにコード値があることは価値がないようです。他の状況では、これが役立つ場合があります。


以下で指摘するように、サイズが変更されても、HashMap内のオブジェクトのハッシュコードの再計算は行われないため、何も得られません。
デルフエゴ2009年

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;

@kdgregory:「衝突が多いほど作業が増えることを意味する」と私は別のところで回答しました:)
OscarRyz 2009年

1

指摘したように、ハッシュコードの実装では衝突が多すぎます。これを修正すると、適切なパフォーマンスが得られます。さらに、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;  

これは衝突がないと思います。これを証明することは、数学的に傾いた読者のための練習問題として残されています。


1

効果的なJavaの:プログラミング言語ガイド(Javaのシリーズ)

第3章では、hashCode()を計算する際に従うべき適切なルールを見つけることができます。

特に:

フィールドが配列の場合は、各要素が個別のフィールドであるかのように扱います。つまり、これらのルールを再帰的に適用して重要な要素ごとにハッシュコードを計算し、ステップ2.bでこれらの値を結合します。配列フィールドのすべての要素が重要な場合は、リリース1.5で追加されたArrays.hashCodeメソッドの1つを使用できます。


0

最初に大きな地図を割り当てます。エントリが2,600万個あり、そのためのメモリがある場合は、を実行しnew HashMap(30000000)ます。

2600万のキーと値を持つ2600万のエントリに十分なメモリがありますか?これは私にとって多くの記憶のように思えます。ガベージコレクションが200万から300万のマークでまだ正常に機能していることを確認しますか?それはボトルネックだと想像できました。


2
ああ、もう一つ。ハッシュコードは、マップ内の単一の位置にある大きなリンクリストを回避するために、均等に分散する必要があります。
ReneS 2009年

0

あなたは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方法を「機会の領域」として残します;)

編集する

HashMapではありません

あなたの最後の版によると。

アプリケーションを実際にプロファイリングし、メモリ/ CPUが消費されている場所を確認する必要があると思います。

私はあなたの同じものを実装するクラスを作成しました hashCode

そのハッシュコードは何百万もの衝突を引き起こし、HashMapのエントリは劇的に減少します。

私は前回のテストで21秒、16秒から10秒と8秒に合格しています。その理由は、hashCodeが多数の衝突を引き起こし、あなたが考える26Mのオブジェクトを格納するのではなく、はるかに低い数(約20kと私が言う)を格納しているためです。

問題はハッシュマップではなく、コードのどこかにあります。

プロファイラーを入手し、場所を確認するときが来ました。アイテムの作成か、ディスクに書き込んでいるか、ネットワークからデータを受信して​​いる可能性があります。

これがあなたのクラスの私の実装です。

私はあなたがしたように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

3
オスカーは、他の場所で(コメントへの応答として)指摘したように、衝突が多いほど良いと想定しているようです。それはあまり良くありません。衝突とは、特定のハッシュのスロットが、単一のエントリを含むことからエントリのリストを含むことになることを意味します。スロットにアクセスするたびに、このリストを検索/トラバースする必要があります。
デルフエゴ2009年

@delfuego:実際には、異なるクラスを使用して衝突が発生した場合にのみ発生しますが、同じクラスに対して同じエントリが使用されます;)
OscarRyz 2009年

2
@オスカー-MAKの回答であなたへの私の返事を見てください。HashMapは各ハッシュバケットでリンクされたエントリのリストを維持し、すべての要素でequals()を呼び出してそのリストをウォークします。オブジェクトのクラスはそれとは何の関係もありません(equals()の短絡を除く)。
kdgregory 2009年

1
@Oscar-あなたの答えを読んで、ハッシュコードが同じである場合、equals()がtrueを返すと想定しているようです。これは、equals / hashcodeコントラクトの一部ではありません。誤解している場合は、このコメントを無視してください。
kdgregory 2009年

1
オスカーの取り組みに感謝しますが、同じハッシュコードを持つキーオブジェクトと同等であるキーオブジェクトを混同していると思います。また、キーとしてイコール文字列を使用しているコードリンクの1つで、Javaの文字列は不変であることを覚えておいてください。今日私達は両方ともハッシュについて多くを学んだと思います:)
ナッシュ


0

私はしばらく前にリストとハッシュマップを使用して簡単なテストを行いましたが、面白いことに、リストを繰り返し処理し、オブジェクトを見つけるのにミリ秒単位の時間を要しました。ああそうなのですが、そのサイズのハッシュマップを扱う場合、メモリは大きな問題です。


0

使用される一般的なハッシュ方法は、大きなセットにはあまり適していません。また、上記で指摘したように、使用されるハッシュは特に悪いものです。BuzHash(http://www.java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htmのサンプル実装)など、混合率が高く、カバレッジの高いハッシュアルゴリズムを使用することをお勧めします。

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