さて、これを休ませるために、いくつかのシナリオを実行して結果を視覚化するためのテストアプリを作成しました。テストの実行方法は次のとおりです。
- さまざまなコレクションサイズが試されました。10万、10万、10万のエントリです。
- 使用されるキーは、IDによって一意に識別されるクラスのインスタンスです。各テストでは、整数をIDとしてインクリメントする一意のキーを使用します。この
equals
メソッドはIDのみを使用するため、キーマッピングが別のIDを上書きすることはありません。
- キーは、事前設定された番号に対するIDのモジュールの残りで構成されるハッシュコードを取得します。その番号をハッシュ制限と呼びます。これにより、予想されるハッシュ衝突の数を制御することができました。たとえば、コレクションサイズが100の場合、IDが0〜99の範囲のキーがあります。ハッシュ制限が100の場合、すべてのキーに一意のハッシュコードがあります。ハッシュ制限が50の場合、キー0はキー50と同じハッシュコードを持ち、1は51などと同じハッシュコードを持ちます。つまり、キーごとの予想されるハッシュ衝突の数は、コレクションサイズをハッシュで割ったものです。制限。
- コレクションサイズとハッシュ制限の組み合わせごとに、さまざまな設定で初期化されたハッシュマップを使用してテストを実行しました。これらの設定は、負荷係数であり、収集設定の係数として表される初期容量です。たとえば、コレクションサイズが100で、初期容量係数が1.25のテストでは、初期容量が125のハッシュマップが初期化されます。
- 各キーの値は単に新しい
Object
です。
- 各テスト結果は、Resultクラスのインスタンスにカプセル化されます。すべてのテストの最後に、結果は全体的なパフォーマンスの最低から最高の順に並べられます。
- プットとゲットの平均時間は、10プット/ゲットごとに計算されます。
- JITコンパイルの影響を排除するために、すべてのテストの組み合わせが1回実行されます。その後、実際の結果を得るためにテストが実行されます。
クラスは次のとおりです。
package hashmaptest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class HashMapTest {
private static final List<Result> results = new ArrayList<Result>();
public static void main(String[] args) throws IOException {
final int[][] sampleSizesAndHashLimits = new int[][] {
{100, 50, 90, 100},
{1000, 500, 900, 990, 1000},
{100000, 10000, 90000, 99000, 100000}
};
final double[] initialCapacityFactors = new double[] {0.5, 0.75, 1.0, 1.25, 1.5, 2.0};
final float[] loadFactors = new float[] {0.5f, 0.75f, 1.0f, 1.25f};
for(int[] sizeAndLimits : sampleSizesAndHashLimits) {
int size = sizeAndLimits[0];
for(int i = 1; i < sizeAndLimits.length; ++i) {
int limit = sizeAndLimits[i];
for(double initCapacityFactor : initialCapacityFactors) {
for(float loadFactor : loadFactors) {
runTest(limit, size, initCapacityFactor, loadFactor);
}
}
}
}
results.clear();
for(int[] sizeAndLimits : sampleSizesAndHashLimits) {
int size = sizeAndLimits[0];
for(int i = 1; i < sizeAndLimits.length; ++i) {
int limit = sizeAndLimits[i];
for(double initCapacityFactor : initialCapacityFactors) {
for(float loadFactor : loadFactors) {
runTest(limit, size, initCapacityFactor, loadFactor);
}
}
}
}
Collections.sort(results);
for(final Result result : results) {
result.printSummary();
}
}
private static void runTest(final int hashLimit, final int sampleSize,
final double initCapacityFactor, final float loadFactor) {
final int initialCapacity = (int)(sampleSize * initCapacityFactor);
System.out.println("Running test for a sample collection of size " + sampleSize
+ ", an initial capacity of " + initialCapacity + ", a load factor of "
+ loadFactor + " and keys with a hash code limited to " + hashLimit);
System.out.println("====================");
double hashOverload = (((double)sampleSize/hashLimit) - 1.0) * 100.0;
System.out.println("Hash code overload: " + hashOverload + "%");
final List<Key> keys = generateSamples(hashLimit, sampleSize);
final List<Object> values = generateValues(sampleSize);
final HashMap<Key, Object> map = new HashMap<Key, Object>(initialCapacity, loadFactor);
final long startPut = System.nanoTime();
for(int i = 0; i < sampleSize; ++i) {
map.put(keys.get(i), values.get(i));
}
final long endPut = System.nanoTime();
final long putTime = endPut - startPut;
final long averagePutTime = putTime/(sampleSize/10);
System.out.println("Time to map all keys to their values: " + putTime + " ns");
System.out.println("Average put time per 10 entries: " + averagePutTime + " ns");
final long startGet = System.nanoTime();
for(int i = 0; i < sampleSize; ++i) {
map.get(keys.get(i));
}
final long endGet = System.nanoTime();
final long getTime = endGet - startGet;
final long averageGetTime = getTime/(sampleSize/10);
System.out.println("Time to get the value for every key: " + getTime + " ns");
System.out.println("Average get time per 10 entries: " + averageGetTime + " ns");
System.out.println("");
final Result result =
new Result(sampleSize, initialCapacity, loadFactor, hashOverload, averagePutTime, averageGetTime, hashLimit);
results.add(result);
System.gc();
try {
Thread.sleep(200);
} catch(final InterruptedException e) {}
}
private static List<Key> generateSamples(final int hashLimit, final int sampleSize) {
final ArrayList<Key> result = new ArrayList<Key>(sampleSize);
for(int i = 0; i < sampleSize; ++i) {
result.add(new Key(i, hashLimit));
}
return result;
}
private static List<Object> generateValues(final int sampleSize) {
final ArrayList<Object> result = new ArrayList<Object>(sampleSize);
for(int i = 0; i < sampleSize; ++i) {
result.add(new Object());
}
return result;
}
private static class Key {
private final int hashCode;
private final int id;
Key(final int id, final int hashLimit) {
this.id = id;
this.hashCode = id % hashLimit;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(final Object o) {
return ((Key)o).id == this.id;
}
}
static class Result implements Comparable<Result> {
final int sampleSize;
final int initialCapacity;
final float loadFactor;
final double hashOverloadPercentage;
final long averagePutTime;
final long averageGetTime;
final int hashLimit;
Result(final int sampleSize, final int initialCapacity, final float loadFactor,
final double hashOverloadPercentage, final long averagePutTime,
final long averageGetTime, final int hashLimit) {
this.sampleSize = sampleSize;
this.initialCapacity = initialCapacity;
this.loadFactor = loadFactor;
this.hashOverloadPercentage = hashOverloadPercentage;
this.averagePutTime = averagePutTime;
this.averageGetTime = averageGetTime;
this.hashLimit = hashLimit;
}
@Override
public int compareTo(final Result o) {
final long putDiff = o.averagePutTime - this.averagePutTime;
final long getDiff = o.averageGetTime - this.averageGetTime;
return (int)(putDiff + getDiff);
}
void printSummary() {
System.out.println("" + averagePutTime + " ns per 10 puts, "
+ averageGetTime + " ns per 10 gets, for a load factor of "
+ loadFactor + ", initial capacity of " + initialCapacity
+ " for " + sampleSize + " mappings and " + hashOverloadPercentage
+ "% hash code overload.");
}
}
}
これを実行するには時間がかかる場合があります。結果は標準出力で印刷されます。あなたは私が一行コメントアウトしたことに気付くかもしれません。その行は、結果の視覚的表現をpngファイルに出力するビジュアライザーを呼び出します。このためのクラスを以下に示します。実行する場合は、上記のコードの適切な行のコメントを解除してください。警告:ビジュアライザークラスは、Windowsで実行していることを前提としており、C:\ tempにフォルダーとファイルを作成します。別のプラットフォームで実行する場合は、これを調整してください。
package hashmaptest;
import hashmaptest.HashMapTest.Result;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.imageio.ImageIO;
public class ResultVisualizer {
private static final Map<Integer, Map<Integer, Set<Result>>> sampleSizeToHashLimit =
new HashMap<Integer, Map<Integer, Set<Result>>>();
private static final DecimalFormat df = new DecimalFormat("0.00");
static void visualizeResults(final List<Result> results) throws IOException {
final File tempFolder = new File("C:\\temp");
final File baseFolder = makeFolder(tempFolder, "hashmap_tests");
long bestPutTime = -1L;
long worstPutTime = 0L;
long bestGetTime = -1L;
long worstGetTime = 0L;
for(final Result result : results) {
final Integer sampleSize = result.sampleSize;
final Integer hashLimit = result.hashLimit;
final long putTime = result.averagePutTime;
final long getTime = result.averageGetTime;
if(bestPutTime == -1L || putTime < bestPutTime)
bestPutTime = putTime;
if(bestGetTime <= -1.0f || getTime < bestGetTime)
bestGetTime = getTime;
if(putTime > worstPutTime)
worstPutTime = putTime;
if(getTime > worstGetTime)
worstGetTime = getTime;
Map<Integer, Set<Result>> hashLimitToResults =
sampleSizeToHashLimit.get(sampleSize);
if(hashLimitToResults == null) {
hashLimitToResults = new HashMap<Integer, Set<Result>>();
sampleSizeToHashLimit.put(sampleSize, hashLimitToResults);
}
Set<Result> resultSet = hashLimitToResults.get(hashLimit);
if(resultSet == null) {
resultSet = new HashSet<Result>();
hashLimitToResults.put(hashLimit, resultSet);
}
resultSet.add(result);
}
System.out.println("Best average put time: " + bestPutTime + " ns");
System.out.println("Best average get time: " + bestGetTime + " ns");
System.out.println("Worst average put time: " + worstPutTime + " ns");
System.out.println("Worst average get time: " + worstGetTime + " ns");
for(final Integer sampleSize : sampleSizeToHashLimit.keySet()) {
final File sizeFolder = makeFolder(baseFolder, "sample_size_" + sampleSize);
final Map<Integer, Set<Result>> hashLimitToResults =
sampleSizeToHashLimit.get(sampleSize);
for(final Integer hashLimit : hashLimitToResults.keySet()) {
final File limitFolder = makeFolder(sizeFolder, "hash_limit_" + hashLimit);
final Set<Result> resultSet = hashLimitToResults.get(hashLimit);
final Set<Float> loadFactorSet = new HashSet<Float>();
final Set<Integer> initialCapacitySet = new HashSet<Integer>();
for(final Result result : resultSet) {
loadFactorSet.add(result.loadFactor);
initialCapacitySet.add(result.initialCapacity);
}
final List<Float> loadFactors = new ArrayList<Float>(loadFactorSet);
final List<Integer> initialCapacities = new ArrayList<Integer>(initialCapacitySet);
Collections.sort(loadFactors);
Collections.sort(initialCapacities);
final BufferedImage putImage =
renderMap(resultSet, loadFactors, initialCapacities, worstPutTime, bestPutTime, false);
final BufferedImage getImage =
renderMap(resultSet, loadFactors, initialCapacities, worstGetTime, bestGetTime, true);
final String putFileName = "size_" + sampleSize + "_hlimit_" + hashLimit + "_puts.png";
final String getFileName = "size_" + sampleSize + "_hlimit_" + hashLimit + "_gets.png";
writeImage(putImage, limitFolder, putFileName);
writeImage(getImage, limitFolder, getFileName);
}
}
}
private static File makeFolder(final File parent, final String folder) throws IOException {
final File child = new File(parent, folder);
if(!child.exists())
child.mkdir();
return child;
}
private static BufferedImage renderMap(final Set<Result> results, final List<Float> loadFactors,
final List<Integer> initialCapacities, final float worst, final float best,
final boolean get) {
final Color[][] map = new Color[initialCapacities.size()][loadFactors.size()];
for(final Result result : results) {
final int x = initialCapacities.indexOf(result.initialCapacity);
final int y = loadFactors.indexOf(result.loadFactor);
final float time = get ? result.averageGetTime : result.averagePutTime;
final float score = (time - best)/(worst - best);
final Color c = new Color(score, 1.0f - score, 0.0f);
map[x][y] = c;
}
final int imageWidth = initialCapacities.size() * 40 + 50;
final int imageHeight = loadFactors.size() * 40 + 50;
final BufferedImage image =
new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_3BYTE_BGR);
final Graphics2D g = image.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, imageWidth, imageHeight);
for(int x = 0; x < map.length; ++x) {
for(int y = 0; y < map[x].length; ++y) {
g.setColor(map[x][y]);
g.fillRect(50 + x*40, imageHeight - 50 - (y+1)*40, 40, 40);
g.setColor(Color.BLACK);
g.drawLine(25, imageHeight - 50 - (y+1)*40, 50, imageHeight - 50 - (y+1)*40);
final Float loadFactor = loadFactors.get(y);
g.drawString(df.format(loadFactor), 10, imageHeight - 65 - (y)*40);
}
g.setColor(Color.BLACK);
g.drawLine(50 + (x+1)*40, imageHeight - 50, 50 + (x+1)*40, imageHeight - 15);
final int initialCapacity = initialCapacities.get(x);
g.drawString(((initialCapacity%1000 == 0) ? "" + (initialCapacity/1000) + "K" : "" + initialCapacity), 15 + (x+1)*40, imageHeight - 25);
}
g.drawLine(25, imageHeight - 50, imageWidth, imageHeight - 50);
g.drawLine(50, 0, 50, imageHeight - 25);
g.dispose();
return image;
}
private static void writeImage(final BufferedImage image, final File folder,
final String filename) throws IOException {
final File imageFile = new File(folder, filename);
ImageIO.write(image, "png", imageFile);
}
}
視覚化された出力は次のとおりです。
- テストは、最初にコレクションサイズで分割され、次にハッシュ制限で分割されます。
- テストごとに、平均プット時間(10プットあたり)と平均取得時間(10ゲットあたり)に関する出力画像があります。画像は、初期容量と負荷率の組み合わせごとの色を示す2次元の「ヒートマップ」です。
- 画像の色は、飽和した緑から飽和した赤まで、最良の結果から最悪の結果までの正規化されたスケールでの平均時間に基づいています。つまり、最高の時間は完全に緑になり、最悪の時間は完全に赤になります。2つの異なる時間測定値が同じ色になることはありません。
- カラーマップはプットとゲットで別々に計算されますが、それぞれのカテゴリーのすべてのテストを網羅しています。
- ビジュアライゼーションは、x軸に初期容量、y軸に負荷率を示しています。
さらに面倒なことはせずに、結果を見てみましょう。プットの結果から始めましょう。
結果を出す
コレクションサイズ:100。ハッシュ制限:50。これは、各ハッシュコードが2回発生し、他のすべてのキーがハッシュマップで衝突することを意味します。
まあ、それはあまり良いスタートではありません。コレクションサイズの25%を超える初期容量の大きなホットスポットがあり、負荷係数は1であることがわかります。左下隅のパフォーマンスはあまり良くありません。
コレクションサイズ:100。ハッシュ制限:90。10個に1個のキーに重複するハッシュコードがあります。
これはもう少し現実的なシナリオであり、完全なハッシュ関数はありませんが、それでも10%の過負荷です。ホットスポットはなくなりましたが、初期容量が低く、負荷率が低いという組み合わせは明らかに機能しません。
コレクションサイズ:100。ハッシュ制限:100。各キーは独自のハッシュコードです。十分なバケットがある場合、衝突は予想されません。
負荷率1で初期容量100は問題ないようです。驚くべきことに、低い負荷率で高い初期容量が必ずしも良いとは限りません。
コレクションのサイズ:1000。ハッシュ制限:500。1000エントリで、ここではさらに深刻になっています。最初のテストと同様に、2対1のハッシュオーバーロードがあります。
左下隅はまだうまくいっていません。しかし、低い初期カウント/高負荷係数と高い初期カウント/低負荷係数の組み合わせの間には対称性があるようです。
コレクションサイズ:1000。ハッシュ制限:900。これは、10分の1のハッシュコードが2回発生することを意味します。衝突に関する合理的なシナリオ。
初期容量が低すぎて負荷率が1を超えるという、ありそうもない組み合わせで非常に面白いことが起こっています。これは直感に反します。それ以外の場合は、まだかなり対称的です。
コレクションのサイズ:1000。ハッシュ制限:990。衝突がいくつかありますが、ごくわずかです。この点でかなり現実的です。
ここには素晴らしい対称性があります。左下隅はまだ最適ではありませんが、1000初期容量/1.0負荷率と1250初期容量/0.75負荷率の組み合わせは同じレベルです。
コレクションサイズ:1000。ハッシュ制限:1000。重複するハッシュコードはありませんが、サンプルサイズは1000になりました。
ここで言うことはあまりありません。より高い初期容量と0.75の負荷係数の組み合わせは、1000の初期容量と負荷係数1の組み合わせよりもわずかに優れているようです。
コレクションのサイズ:100_000。ハッシュ制限:10_000。了解しました。サンプルサイズが10万で、キーごとに100個のハッシュコードが重複しているため、深刻になっています。
うわぁ!私たちはより低いスペクトルを見つけたと思います。ここでは、負荷係数が1のコレクションサイズとまったく同じ初期容量が非常にうまく機能していますが、それ以外は店全体にあります。
コレクションのサイズ:100_000。ハッシュ制限:90_000。前のテストよりも少し現実的ですが、ここではハッシュコードに10%のオーバーロードがあります。
左下隅はまだ望ましくありません。初期容量が大きいほど最適です。
コレクションのサイズ:100_000。ハッシュ制限:99_000。良いシナリオ、これ。1%のハッシュコードオーバーロードを伴う大規模なコレクション。
正確なコレクションサイズを負荷係数1の初期容量として使用すると、ここで勝ちます!ただし、少し大きいinit容量は非常にうまく機能します。
コレクションのサイズ:100_000。ハッシュ制限:100_000。大きなもの。完璧なハッシュ関数を備えた最大のコレクション。
ここにいくつかの驚くべきもの。負荷率1で50%の追加スペースがある初期容量が勝ちます。
了解しました。プットは以上です。次に、getsを確認します。以下のマップはすべてベスト/ワーストの取得時間に関連していることを忘れないでください。プット時間は考慮されなくなりました。
結果を得る
コレクションサイズ:100。ハッシュ制限:50。これは、各ハッシュコードが2回発生し、他のすべてのキーがハッシュマップで衝突すると予想されたことを意味します。
え…なに?
コレクションサイズ:100。ハッシュ制限:90。10個に1個のキーに重複するハッシュコードがあります。
おっとネリー!これは、質問者の質問と相関する可能性が最も高いシナリオであり、負荷係数1で初期容量100を使用することは、ここで最悪のことの1つです。私はこれを偽造しなかったことを誓います。
コレクションサイズ:100。ハッシュ制限:100。各キーは独自のハッシュコードです。衝突は予想されません。
これはもう少し平和に見えます。全体的にほぼ同じ結果。
コレクションサイズ:1000。ハッシュ制限:500。最初のテストと同様に、2対1のハッシュオーバーロードがありますが、現在はさらに多くのエントリがあります。
ここでは、どの設定でも適切な結果が得られるようです。
コレクションサイズ:1000。ハッシュ制限:900。これは、10分の1のハッシュコードが2回発生することを意味します。衝突に関する合理的なシナリオ。
そして、このセットアップのプットと同じように、奇妙な場所で異常が発生します。
コレクションのサイズ:1000。ハッシュ制限:990。衝突がいくつかありますが、ごくわずかです。この点でかなり現実的です。
どこでもまともなパフォーマンス。高い初期容量と低い負荷率の組み合わせを除けば。2つのハッシュマップのサイズ変更が予想される可能性があるため、プットにはこれを期待します。しかし、なぜ取得するのですか?
コレクションサイズ:1000。ハッシュ制限:1000。重複するハッシュコードはありませんが、サンプルサイズは1000になりました。
まったく見事な視覚化。これは何があってもうまくいくようです。
コレクションのサイズ:100_000。ハッシュ制限:10_000。多くのハッシュコードが重複している状態で、再び100Kに入ります。
悪い点は非常に局所的ですが、それはきれいに見えません。ここでのパフォーマンスは、設定間の特定の相乗効果に大きく依存しているようです。
コレクションのサイズ:100_000。ハッシュ制限:90_000。前のテストよりも少し現実的ですが、ここではハッシュコードに10%のオーバーロードがあります。
目を細めると右上隅を指す矢印が表示されますが、かなりの違いがあります。
コレクションのサイズ:100_000。ハッシュ制限:99_000。良いシナリオ、これ。1%のハッシュコードオーバーロードを伴う大規模なコレクション。
とても混沌としている。ここで多くの構造を見つけるのは難しいです。
コレクションのサイズ:100_000。ハッシュ制限:100_000。大きなもの。完璧なハッシュ関数を備えた最大のコレクション。
他の誰かがこれがAtariグラフィックのように見え始めていると思いますか?これは、正確にコレクションサイズの-25%または+ 50%の初期容量を優先するようです。
さて、それは今結論の時間です...
- プットタイムについて:マップエントリの予想数よりも少ない初期容量は避けたいと思うでしょう。正確な数が事前にわかっている場合は、その数またはそれより少し上の数が最適であるように思われます。高い負荷率は、ハッシュマップのサイズ変更が早いため、初期容量の低下を相殺する可能性があります。初期容量が高い場合は、それほど重要ではないようです。
- 取得時間について:ここでは結果が少し混沌としている。結論を出すことはあまりありません。ハッシュコードのオーバーラップ、初期容量、負荷率の間の微妙な比率に大きく依存しているようです。おそらく悪いセットアップがうまく機能し、良いセットアップがひどく機能します。
- Javaのパフォーマンスについての仮定に関しては、私は明らかにがらくたでいっぱいです。真実は、の実装に合わせて設定を完全に調整していない限り
HashMap
、結果はいたるところにあるということです。これから取り除くべきことが1つあるとすれば、デフォルトの初期サイズである16は、最小のマップ以外では少し馬鹿げているということです。したがって、サイズの順序について何らかのアイデアがある場合は、初期サイズを設定するコンストラクターを使用してください。なるだろう。
- ここではナノ秒単位で測定しています。10プットあたりの最高の平均時間は1179nsで、私のマシンでは最悪の5105nsでした。10回の取得あたりの最高の平均時間は547nsで、最悪の3484nsでした。これは6倍の違いかもしれませんが、話しているのは1ミリ秒未満です。元のポスターが考えていたものよりもはるかに大きいコレクションについて。
まあ、それだけです。私のコードに、ここに投稿したすべてのものを無効にするような恐ろしい見落としがないことを願っています。これは楽しかったし、結局のところ、小さな最適化との大きな違いを期待するよりも、Javaに頼って仕事をする方がよいことを学びました。避けてはいけないことがあるというわけではありませんが、forループで長い文字列を作成し、間違ったデータ構造を使用してO(n ^ 3)アルゴリズムを作成することについて主に話します。