Java ArrayListと比較したHashMapのメモリオーバーヘッドは何ですか?
更新:
同じオブジェクトの大きなパック(6ミリオン以上)の特定の値を検索する速度を改善したいと思います。
したがって、ArrayListを使用する代わりに1つまたは複数のHashMapを使用することを考えています。しかし、HashMapのオーバーヘッドはどのくらいなのか疑問に思っています。
私が理解する限り、キーは保存されず、キーのハッシュのみが保存されるため、オブジェクトのハッシュのサイズ+ポインター1つのようにする必要があります。
しかし、どのハッシュ関数が使用されていますか? Objectが提供するもの または別のものですか?
HashMapとArrayListを比較している場合、バイナリ検索やカスタムハッシュテーブルなど、ArrayListの何らかの検索/インデックス作成を行っていると思います...?線形検索を使用すると、.get(key)から600万エントリが実行不可能になるためです。
その仮定を使用して、いくつかの実証テストを行い、「ArrayListをバイナリ検索で使用すると、同じ量のRAMで2.5倍の小さなオブジェクトを格納できる」という結論に達しました。 HashMapと比較したカスタムハッシュマップの実装」。私のテストは、3つのフィールドのみを含む小さなオブジェクトに基づいており、そのうち1つはキーであり、キーは整数です。32ビットjdk 1.6を使用しました。 「2.5」の。
注意すべき重要な点は次のとおりです。
(a)参照に必要なスペースや「負荷係数」ではなく、オブジェクト作成に必要なオーバーヘッドです。キーがプリミティブ型、または2つ以上のプリミティブまたは参照値の組み合わせである場合、各キーには8バイトのオーバーヘッドを運ぶ独自のオブジェクトが必要です。
(b)私の経験では、通常、値の一部としてキーが必要です(たとえば、顧客IDでインデックス付けされた顧客レコードを格納するには、顧客オブジェクトの一部として顧客IDが必要です)。これは、HashMapがキーと値への参照を別々に保存することはIMOにとって多少無駄だということです。
警告:
HashMapキーに使用される最も一般的なタイプは文字列です。オブジェクト作成のオーバーヘッドはここでは適用されないため、差は小さくなります。
-Xmx256M JVMのHashMapへの3148004と比較して、ArrayListに挿入された8880502エントリである2.8の数値を取得しましたが、ArrayListの負荷率は80%で、オブジェクトは非常に小さく、12バイトと8バイトのオブジェクトオーバーヘッドがありました。
私の図と実装では、キーが値に含まれている必要があります。そうしないと、オブジェクト作成のオーバーヘッドで同じ問題が発生し、HashMapの別の実装になります。
私のコード:
public class Payload {
int key,b,c;
Payload(int _key) { key = _key; }
}
import org.junit.Test;
import Java.util.HashMap;
import Java.util.Map;
public class Overhead {
@Test
public void useHashMap()
{
int i=0;
try {
Map<Integer, Payload> map = new HashMap<Integer, Payload>();
for (i=0; i < 4000000; i++) {
int key = (int)(Math.random() * Integer.MAX_VALUE);
map.put(key, new Payload(key));
}
}
catch (OutOfMemoryError e) {
System.out.println("Got up to: " + i);
}
}
@Test
public void useArrayList()
{
int i=0;
try {
ArrayListMap map = new ArrayListMap();
for (i=0; i < 9000000; i++) {
int key = (int)(Math.random() * Integer.MAX_VALUE);
map.put(key, new Payload(key));
}
}
catch (OutOfMemoryError e) {
System.out.println("Got up to: " + i);
}
}
}
import Java.util.ArrayList;
public class ArrayListMap {
private ArrayList<Payload> map = new ArrayList<Payload>();
private int[] primes = new int[128];
static boolean isPrime(int n)
{
for (int i=(int)Math.sqrt(n); i >= 2; i--) {
if (n % i == 0)
return false;
}
return true;
}
ArrayListMap()
{
for (int i=0; i < 11000000; i++) // this is clumsy, I admit
map.add(null);
int n=31;
for (int i=0; i < 128; i++) {
while (! isPrime(n))
n+=2;
primes[i] = n;
n += 2;
}
System.out.println("Capacity = " + map.size());
}
public void put(int key, Payload value)
{
int hash = key % map.size();
int hash2 = primes[key % primes.length];
if (hash < 0)
hash += map.size();
do {
if (map.get(hash) == null) {
map.set(hash, value);
return;
}
hash += hash2;
if (hash >= map.size())
hash -= map.size();
} while (true);
}
public Payload get(int key)
{
int hash = key % map.size();
int hash2 = primes[key % primes.length];
if (hash < 0)
hash += map.size();
do {
Payload payload = map.get(hash);
if (payload == null)
return null;
if (payload.key == key)
return payload;
hash += hash2;
if (hash >= map.size())
hash -= map.size();
} while (true);
}
}
最も簡単なことは、ソースを見て、そのように解決することです。ただし、実際にはリンゴとオレンジを比較しています。リストとマップは概念的にはまったく異なります。メモリ使用量に基づいてそれらを選択することはまれです。
この質問の背景は何ですか?
いずれかに格納されるのはポインターのみです。アーキテクチャに応じて、ポインターは32ビットまたは64ビット(またはそれ以上またはそれ以下)でなければなりません。
10の配列リストは、少なくとも10個の「ポインター」を割り当てる傾向があります(また、一時的なオーバーヘッドもいくつか)。
マップは、一度に2つの値を格納するため、その2倍(20ポインター)を割り当てる必要があります。それに加えて、「ハッシュ」を保存する必要があります。これはマップよりも大きくする必要があり、75%のロードでは、約13の32ビット値(ハッシュ)である必要があります。
したがって、オフハンドの回答が必要な場合、比率は約1:3.25程度である必要がありますが、大量のオブジェクトを格納している場合を除き、ポインタストレージのみを話しているのです。瞬時に参照する(HashMap)vs反復する(配列)は、メモリサイズよりもはるかに重要です。
また、配列はコレクションの正確なサイズに合わせることができます。 HashMapsは、サイズを指定することもできますが、そのサイズを超えて「成長する」場合、より大きな配列を再割り当てし、その一部を使用しないため、そこにも少し無駄があります。
あなたにも答えはありませんが、グーグルで簡単に検索すると、Javaに役立つ機能が見つかりました。
Runtime.getRuntime()。freeMemory();
したがって、HashMapとArrayListに同じデータを入力することを提案します。空きメモリを記録し、最初のオブジェクトを削除し、メモリを記録し、2番目のオブジェクトを削除し、メモリを記録し、差を計算します...、利益!!!
おそらくこれを大量のデータで行う必要があります。つまり、1000で始まり、10000、100000、1000000です。
編集: amischiefrのおかげで修正されました。
編集:投稿を編集して申し訳ありませんが、これを使用する場合はこれが非常に重要です(そしてコメントのために少しだけです)。 freeMemoryは、思っているようには機能しません。まず、その値はガベージコレクションによって変更されます。第二に、Javaがより多くのメモリを割り当てると、値が変更されます。freeMemory呼び出しだけを使用しても有用なデータが得られません。
これを試して:
public static void displayMemory() {
Runtime r=Runtime.getRuntime();
r.gc();
r.gc(); // YES, you NEED 2!
System.out.println("Memory Used="+(r.totalMemory()-r.freeMemory()));
}
または、使用したメモリを返して保存し、それを後の値と比較できます。いずれにしても、2つのgcsを覚えて、totalMemory()から減算します。
再度、投稿を編集して申し訳ありません!
ハッシュマップは負荷係数(通常は75%満杯)を維持しようとします。ハッシュマップはまばらに満たされた配列リストと考えることができます。サイズを直接比較した場合の問題は、データのサイズに合わせてマップのこの負荷係数が大きくなることです。一方、ArrayListは、内部配列サイズを2倍にすることで、ニーズに合わせて拡張されます。比較的小さなサイズの場合は同等ですが、より多くのデータをマップにパックすると、ハッシュのパフォーマンスを維持するために多くの空の参照が必要になります。
いずれの場合も、追加を開始する前に、予想されるデータサイズをプライミングすることをお勧めします。これにより、実装の初期設定が改善され、両方の場合で全体の消費量が少なくなる可能性があります。
更新:
更新された問題のチェックアウトに基づいて Glazedリスト 。これは、あなたが説明したものと同様の操作を行うために、Googleの一部の人々によって書かれた小さなツールです。また、非常に迅速です。クラスタリング、フィルタリング、検索などを許可します。
HashMapは、値への参照とキーへの参照を保持します。
ArrayListは、値への参照を保持するだけです。
そのため、キーが値と同じメモリを使用すると仮定すると、HashMapは50%以上のメモリを使用します(厳密に言えば、参照を保持するだけなので、そのメモリを使用するHashMapではありません)
一方、HashMapは基本操作(getおよびput)に対して一定のパフォーマンスを提供します。したがって、より多くのメモリを使用する可能性がありますが、HashMapを使用すると要素の取得がはるかに高速になりますArrayListよりも。
だから、あなたが次にすべきことは、より多くのメモリを使用する人を気にしないが、何のために良い。
プログラムに正しいデータ構造を使用すると、ライブラリの実装方法よりも多くのCPU /メモリを節約できます。
[〜#〜] edit [〜#〜]
Grant Welchの回答の後、2,000,000個の整数を測定することにしました。
これは出力です
$
$javac MemoryUsage.Java
Note: MemoryUsage.Java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$Java -Xms128m -Xmx128m MemoryUsage
Using ArrayListMemoryUsage@8558d2 size: 0
Total memory: 133.234.688
Initial free: 132.718.608
Final free: 77.965.488
Used: 54.753.120
Memory Used 41.364.824
ArrayListMemoryUsage@8558d2 size: 2000000
$
$Java -Xms128m -Xmx128m MemoryUsage H
Using HashMapMemoryUsage@8558d2 size: 0
Total memory: 133.234.688
Initial free: 124.329.984
Final free: 4.109.600
Used: 120.220.384
Memory Used 129.108.608
HashMapMemoryUsage@8558d2 size: 2000000
基本的に、「仕事に最適なツール」を使用する必要があります。キーと値のペアが必要な場合(HashMap
を使用できる場合)と値のリストだけが必要な場合(ArrayList
)それから、私の意見では、「どちらがより多くのメモリを使用するか」という質問は意味がありません。
しかし、質問に答えるには、HashMap
がキー/値のペアを格納し、ArrayList
が値のみを格納するため、HashMapにキーだけを追加すると、より多くのメモリが必要になると想定します。もちろん、同じ値で比較していると仮定しますtype(たとえば、両方の値が文字列である場合)。
ここで間違った質問がされていると思います。
600万のエントリを含むList
内のオブジェクトを検索できる速度を改善したい場合は、どのくらい高速かを調べる必要がありますこれらのデータ型の取得操作が実行されます。
いつものように、これらのクラスのJavadocは、それらが提供するパフォーマンスのタイプを非常に明確に述べています。
HashMap :
この実装は、ハッシュ関数がバケット間で要素を適切に分散すると仮定して、基本操作(getおよびput)に一定時間のパフォーマンスを提供します。
これは、HashMap.get(key)がO(1)
であることを意味します。
サイズ、isEmpty、get、set、iterator、およびlistIterator操作は、一定の時間で実行されます。追加操作は償却された一定時間で実行されます。つまり、n個の要素を追加するにはO(n)時間が必要です。他の操作はすべて線形時間で実行されます。
つまり、ArrayList
の操作のほとんどはO(1)
ですが、特定の値に一致するオブジェクトを見つけるために使用する操作ではない可能性があります。
ArrayList
内のすべての要素を反復処理して等しいかどうかをテストしている場合、またはcontains()
を使用している場合、これは操作がO(n)
時間(またはそれ以下)で実行されていることを意味します)。
O(1)
またはO(n)
表記に慣れていない場合、これは操作にかかる時間を示しています。この場合、一定時間のパフォーマンスが得られるなら、それを使いたいと思うでしょう。 HashMap.get()
がO(1)
の場合、これは検索操作にほぼ同じ時間がかかることを意味しますとにかくマップ内のエントリ数。
ArrayList.contains()
のようなものがO(n)
であるという事実は、リストのサイズが大きくなると、時間がかかることを意味します。そのため、600万エントリのArrayList
を反復処理することはまったく効果的ではありません。
正確な数はわかりませんが、HashMapsはずっと重いです。 2つを比較すると、ArrayListの内部表現は自明ですが、HashMapはメモリ消費を膨らませることができるエントリオブジェクト(エントリ)を保持します。
それほど大きくはありませんが、大きくなっています。これを視覚化する優れた方法は、すべてのヒープ割り当てを確認できる YourKit などの動的プロファイラーを使用することです。とてもいいです。
この投稿 は、Javaのオブジェクトサイズに関する多くの情報を提供しています。
この site は、一般的に使用される(あまり一般的ではない)いくつかのデータ構造のメモリ消費量をリストします。そこから、HashMap
がArrayList
の約5倍のスペースを取ることがわかります。マップは、エントリごとに1つの追加オブジェクトも割り当てます。
予測可能な反復順序が必要で、LinkedHashMap
を使用する場合、メモリ消費はさらに大きくなります。
Memory Measurer を使用して、独自のメモリ測定を行うことができます。
ただし、注意すべき重要な事実が2つあります。
ArrayList
およびHashMap
を含む)は、現在必要なスペースよりも多くのスペースを割り当てます。そうしないと、コストのかかるサイズ変更操作を頻繁に実行する必要があるためです。したがって、要素ごとのメモリ消費は、コレクション内の要素の数に依存します。たとえば、デフォルト設定のArrayList
は、0〜10個の要素に同じメモリを使用します。Jon Skeetが指摘したように、これらはまったく異なる構造です。マップ(HashMapなど)は、ある値から別の値へのマッピングです。つまり、Key-> Valueのような関係で、値にマップするキーがあります。キーはハッシュされ、すばやく検索できるように配列に配置されます。
一方、リストは順序を持つ要素のコレクションです-ArrayListはたまたまバックエンドストレージメカニズムとして配列を使用しますが、それは無関係です。インデックス付きの各要素は、リスト内の単一の要素です。
編集:あなたのコメントに基づいて、私は次の情報を追加しました:
キーはハッシュマップに保存されます。これは、ハッシュが2つの異なる要素に対して一意であることが保証されていないためです。したがって、ハッシュ衝突の場合、キーを保存する必要があります。要素が要素のセットに存在するかどうかだけを確認する場合は、Setを使用します(これの標準実装はHashSetです)。順序は重要ですが、クイックルックアップが必要な場合は、LinkedHashSetを使用します。要素が挿入された順序が維持されるためです。ルックアップ時間は両方でO(1)ですが、LinkedHashSetでは挿入時間がわずかに長くなります。実際に1つの値から別の値にマッピングする場合にのみマップを使用します。一意のオブジェクトのセット、セットを使用し、オブジェクトを順序付けている場合はリストを使用します。
2つのArrayListと1つのハッシュマップを検討している場合、それは不確定です。どちらも部分的に完全なデータ構造です。 VectorとHashtableを比較している場合、Hashtablesはより多くのスペースを割り当てるのに対して、Vectorは使用するスペースのみを割り当てるため、おそらくメモリ効率が高くなります。
キーと値のペアが必要で、メモリを大量に消費する作業をしていない場合は、ハッシュマップを使用してください。