固定サイズのHashMapの最適な容量と負荷率はどれくらいですか?


85

特定のケースに最適な容量と負荷率を見つけようとしています。その要点はわかったと思いますが、それでも私より知識のある方からの確認はありがたいです。:)

HashMapが100個のオブジェクトを含むようにいっぱいになり、ほとんどの時間を100個のオブジェクトを持つことに費やすことがわかっている場合、最適な値は初期容量100と負荷係数1だと思いますか?または、容量101が必要ですか、それとも他に問題がありますか?

編集:わかりました、私は数時間を取っておき、いくつかのテストを行いました。結果は次のとおりです。

  • 不思議なことに、容量、容量+ 1、容量+ 2、容量-1、さらには容量-10でも、まったく同じ結果が得られます。少なくとも容量1と容量10の方が悪い結果になると思います。
  • (デフォルト値の16を使用するのではなく)初期容量を使用すると、put()が大幅に改善され、最大30%高速になります。
  • 負荷係数1を使用すると、少数のオブジェクトで同等のパフォーマンスが得られ、多数のオブジェクト(> 100000)でパフォーマンスが向上します。ただし、これはオブジェクトの数に比例して改善されるわけではありません。結果に影響を与える追加の要因があると思います。
  • get()のパフォーマンスは、オブジェクト/容量の数によって少し異なりますが、ケースごとにわずかに異なる場合がありますが、通常、初期容量や負荷率の影響を受けません。

EDIT2:私の側にもいくつかのチャートを追加します。これは、HashMapを初期化して最大容量まで埋めた場合の、負荷率0.75と1の違いを示しています。yスケールはミリ秒単位の時間(小さいほど良い)であり、xスケールはサイズ(オブジェクトの数)です。サイズは直線的に変化するため、必要な時間も直線的に増加します。

だから、私が得たものを見てみましょう。次の2つのグラフは、負荷率の違いを示しています。最初のグラフは、HashMapが容量いっぱいになったときに何が起こるかを示しています。サイズ変更により、負荷率0.75のパフォーマンスが低下します。しかし、それは一貫して悪化しているわけではなく、あらゆる種類のバンプとホップがあります-これにはGCが大きな役割を果たしていると思います。負荷率1.25は1と同じように機能するため、グラフには含まれていません。

完全に満たされました

このグラフは、サイズ変更により0.75が悪化したことを示しています。HashMapを半分の容量まで満たすと、0.75は悪くはなく、ただ...違います(そして、使用するメモリが少なくなり、反復パフォーマンスが明らかに向上するはずです)。

半分いっぱい

もう1つお見せしたいことがあります。これは、3つの負荷係数すべてと異なるHashMapサイズのパフォーマンスを取得することです。負荷係数1の1つのスパイクを除いて、わずかな変動で一貫して一定です。それが何であるかを本当に知りたいです(おそらくGCですが、誰が知っていますか)。

スパイクに行く

そして、興味のある人のためのコードは次のとおりです。

import java.util.HashMap;
import java.util.Map;

public class HashMapTest {

  // capacity - numbers high as 10000000 require -mx1536m -ms1536m JVM parameters
  public static final int CAPACITY = 10000000;
  public static final int ITERATIONS = 10000;

  // set to false to print put performance, or to true to print get performance
  boolean doIterations = false;

  private Map<Integer, String> cache;

  public void fillCache(int capacity) {
    long t = System.currentTimeMillis();
    for (int i = 0; i <= capacity; i++)
      cache.put(i, "Value number " + i);

    if (!doIterations) {
      System.out.print(System.currentTimeMillis() - t);
      System.out.print("\t");
    }
  }

  public void iterate(int capacity) {
    long t = System.currentTimeMillis();

    for (int i = 0; i <= ITERATIONS; i++) {
      long x = Math.round(Math.random() * capacity);
      String result = cache.get((int) x);
    }

    if (doIterations) {
      System.out.print(System.currentTimeMillis() - t);
      System.out.print("\t");
    }
  }

  public void test(float loadFactor, int divider) {
    for (int i = 10000; i <= CAPACITY; i+= 10000) {
      cache = new HashMap<Integer, String>(i, loadFactor);
      fillCache(i / divider);
      if (doIterations)
        iterate(i / divider);
    }
    System.out.println();
  }

  public static void main(String[] args) {
    HashMapTest test = new HashMapTest();

    // fill to capacity
    test.test(0.75f, 1);
    test.test(1, 1);
    test.test(1.25f, 1);

    // fill to half capacity
    test.test(0.75f, 2);
    test.test(1, 2);
    test.test(1.25f, 2);
  }

}

1
この場合、デフォルトを変更するとパフォーマンスが向上する(put()の実行が高速になる)という意味で最適です。
domchi 2011

2
@Peter GC =ガベージコレクション。
domchi 2011年

2
それらのチャートはきちんとしています...それらを生成/レンダリングするために何を使用しましたか?
g_H 2011年

1
@G_H何も派手ではない-上記のプログラムとExcelの出力。:)
Domchi 2011

2
次回は、線の代わりに点を使用します。視覚的に比較しやすくなります。
ポール・ドレイパー

回答:


74

さて、これを休ませるために、いくつかのシナリオを実行して結果を視覚化するためのテストアプリを作成しました。テストの実行方法は次のとおりです。

  • さまざまなコレクションサイズが試されました。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 {

        //First entry of each array is the sample collection size, subsequent entries
        //are the hash limits
        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};

        //Doing a warmup run to eliminate JIT influence
        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();

        //Now for the real thing...
        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();
        }

//      ResultVisualizer.visualizeResults(results);

    }

    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 + "%");

        //Generating our sample key collection.
        final List<Key> keys = generateSamples(hashLimit, sampleSize);

        //Generating our value collection
        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);

        //Haha, what kind of noob explicitly calls for garbage collection?
        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) {

            //Equals implies same hashCode if limit is the same
            //Same hashCode doesn't necessarily implies equals

            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) {

        //[x][y] => x is mapped to initial capacity, y is mapped to load factor
        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回発生し、他のすべてのキーがハッシュマップで衝突することを意味します。

size_100_hlimit_50_puts

まあ、それはあまり良いスタートではありません。コレクションサイズの25%を超える初期容量の大きなホットスポットがあり、負荷係数は1であることがわかります。左下隅のパフォーマンスはあまり良くありません。


コレクションサイズ:100。ハッシュ制限:90。10個に1個のキーに重複するハッシュコードがあります。

size_100_hlimit_90_puts

これはもう少し現実的なシナリオであり、完全なハッシュ関数はありませんが、それでも10%の過負荷です。ホットスポットはなくなりましたが、初期容量が低く、負荷率が低いという組み合わせは明らかに機能しません。


コレクションサイズ:100。ハッシュ制限:100。各キーは独自のハッシュコードです。十分なバケットがある場合、衝突は予想されません。

size_100_hlimit_100_puts

負荷率1で初期容量100は問題ないようです。驚くべきことに、低い負荷率で高い初期容量が必ずしも良いとは限りません。


コレクションのサイズ:1000。ハッシュ制限:500。1000エントリで、ここではさらに深刻になっています。最初のテストと同様に、2対1のハッシュオーバーロードがあります。

size_1000_hlimit_500_puts

左下隅はまだうまくいっていません。しかし、低い初期カウント/高負荷係数と高い初期カウント/低負荷係数の組み合わせの間には対称性があるようです。


コレクションサイズ:1000。ハッシュ制限:900。これは、10分の1のハッシュコードが2回発生することを意味します。衝突に関する合理的なシナリオ。

size_1000_hlimit_900_puts

初期容量が低すぎて負荷率が1を超えるという、ありそうもない組み合わせで非常に面白いことが起こっています。これは直感に反します。それ以外の場合は、まだかなり対称的です。


コレクションのサイズ:1000。ハッシュ制限:990。衝突がいくつかありますが、ごくわずかです。この点でかなり現実的です。

size_1000_hlimit_990_puts

ここには素晴らしい対称性があります。左下隅はまだ最適ではありませんが、1000初期容量/1.0負荷率と1250初期容量/0.75負荷率の組み合わせは同じレベルです。


コレクションサイズ:1000。ハッシュ制限:1000。重複するハッシュコードはありませんが、サンプルサイズは1000になりました。

size_1000_hlimit_1000_puts

ここで言うことはあまりありません。より高い初期容量と0.75の負荷係数の組み合わせは、1000の初期容量と負荷係数1の組み合わせよりもわずかに優れているようです。


コレクションのサイズ:100_000。ハッシュ制限:10_000。了解しました。サンプルサイズが10万で、キーごとに100個のハッシュコードが重複しているため、深刻になっています。

size_100000_hlimit_10000_puts

うわぁ!私たちはより低いスペクトルを見つけたと思います。ここでは、負荷係数が1のコレクションサイズとまったく同じ初期容量が非常にうまく機能していますが、それ以外は店全体にあります。


コレクションのサイズ:100_000。ハッシュ制限:90_000。前のテストよりも少し現実的ですが、ここではハッシュコードに10%のオーバーロードがあります。

size_100000_hlimit_90000_puts

左下隅はまだ望ましくありません。初期容量が大きいほど最適です。


コレクションのサイズ:100_000。ハッシュ制限:99_000。良いシナリオ、これ。1%のハッシュコードオーバーロードを伴う大規模なコレクション。

size_100000_hlimit_99000_puts

正確なコレクションサイズを負荷係数1の初期容量として使用すると、ここで勝ちます!ただし、少し大きいinit容量は非常にうまく機能します。


コレクションのサイズ:100_000。ハッシュ制限:100_000。大きなもの。完璧なハッシュ関数を備えた最大のコレクション。

size_100000_hlimit_100000_puts

ここにいくつかの驚くべきもの。負荷率1で50%の追加スペースがある初期容量が勝ちます。


了解しました。プットは以上です。次に、getsを確認します。以下のマップはすべてベスト/ワーストの取得時間に関連していることを忘れないでください。プット時間は考慮されなくなりました。

結果を得る


コレクションサイズ:100。ハッシュ制限:50。これは、各ハッシュコードが2回発生し、他のすべてのキーがハッシュマップで衝突すると予想されたことを意味します。

size_100_hlimit_50_gets

え…なに?


コレクションサイズ:100。ハッシュ制限:90。10個に1個のキーに重複するハッシュコードがあります。

size_100_hlimit_90_gets

おっとネリー!これは、質問者の質問と相関する可能性が最も高いシナリオであり、負荷係数1で初期容量100を使用することは、ここで最悪のことの1つです。私はこれを偽造しなかったことを誓います。


コレクションサイズ:100。ハッシュ制限:100。各キーは独自のハッシュコードです。衝突は予想されません。

size_100_hlimit_100_gets

これはもう少し平和に見えます。全体的にほぼ同じ結果。


コレクションサイズ:1000。ハッシュ制限:500。最初のテストと同様に、2対1のハッシュオーバーロードがありますが、現在はさらに多くのエントリがあります。

size_1000_hlimit_500_gets

ここでは、どの設定でも適切な結果が得られるようです。


コレクションサイズ:1000。ハッシュ制限:900。これは、10分の1のハッシュコードが2回発生することを意味します。衝突に関する合理的なシナリオ。

size_1000_hlimit_900_gets

そして、このセットアップのプットと同じように、奇妙な場所で異常が発生します。


コレクションのサイズ:1000。ハッシュ制限:990。衝突がいくつかありますが、ごくわずかです。この点でかなり現実的です。

size_1000_hlimit_990_gets

どこでもまともなパフォーマンス。高い初期容量と低い負荷率の組み合わせを除けば。2つのハッシュマップのサイズ変更が予想される可能性があるため、プットにはこれを期待します。しかし、なぜ取得するのですか?


コレクションサイズ:1000。ハッシュ制限:1000。重複するハッシュコードはありませんが、サンプルサイズは1000になりました。

size_1000_hlimit_1000_gets

まったく見事な視覚化。これは何があってもうまくいくようです。


コレクションのサイズ:100_000。ハッシュ制限:10_000。多くのハッシュコードが重複している状態で、再び100Kに入ります。

size_100000_hlimit_10000_gets

悪い点は非常に局所的ですが、それはきれいに見えません。ここでのパフォーマンスは、設定間の特定の相乗効果に大きく依存しているようです。


コレクションのサイズ:100_000。ハッシュ制限:90_000。前のテストよりも少し現実的ですが、ここではハッシュコードに10%のオーバーロードがあります。

size_100000_hlimit_90000_gets

目を細めると右上隅を指す矢印が表示されますが、かなりの違いがあります。


コレクションのサイズ:100_000。ハッシュ制限:99_000。良いシナリオ、これ。1%のハッシュコードオーバーロードを伴う大規模なコレクション。

size_100000_hlimit_99000_gets

とても混沌としている。ここで多くの構造を見つけるのは難しいです。


コレクションのサイズ:100_000。ハッシュ制限:100_000。大きなもの。完璧なハッシュ関数を備えた最大のコレクション。

size_100000_hlimit_100000_gets

他の誰かがこれがAtariグラフィックのように見え始めていると思いますか?これは、正確にコレクションサイズの-25%または+ 50%の初期容量を優先するようです。


さて、それは今結論の時間です...

  • プットタイムについて:マップエントリの予想数よりも少ない初期容量は避けたいと思うでしょう。正確な数が事前にわかっている場合は、その数またはそれより少し上の数が最適であるように思われます。高い負荷率は、ハッシュマップのサイズ変更が早いため、初期容量の低下を相殺する可能性があります。初期容量が高い場合は、それほど重要ではないようです。
  • 取得時間について:ここでは結果が少し混沌としている。結論を出すことはあまりありません。ハッシュコードのオーバーラップ、初期容量、負荷率の間の微妙な比率に大きく依存しているようです。おそらく悪いセットアップがうまく機能し、良いセットアップがひどく機能します。
  • Javaのパフォーマンスについての仮定に関しては、私は明らかにがらくたでいっぱいです。真実は、の実装に合わせて設定を完全に調整していない限りHashMap、結果はいたるところにあるということです。これから取り除くべきことが1つあるとすれば、デフォルトの初期サイズである16は、最小のマップ以外では少し馬鹿げているということです。したがって、サイズの順序について何らかのアイデアがある場合は、初期サイズを設定するコンストラクターを使用してください。なるだろう。
  • ここではナノ秒単位で測定しています。10プットあたりの最高の平均時間は1179nsで、私のマシンでは最悪の5105nsでした。10回の取得あたりの最高の平均時間は547nsで、最悪の3484nsでした。これは6倍の違いかもしれませんが、話しているのは1ミリ秒未満です。元のポスターが考えていたものよりもはるかに大きいコレクションについて。

まあ、それだけです。私のコードに、ここに投稿したすべてのものを無効にするような恐ろしい見落としがないことを願っています。これは楽しかったし、結局のところ、小さな最適化との大きな違いを期待するよりも、Javaに頼って仕事をする方がよいことを学びました。避けてはいけないことがあるというわけではありませんが、forループで長い文字列を作成し、間違ったデータ構造を使用してO(n ^ 3)アルゴリズムを作成することについて主に話します。


1
努力してくれてありがとう、素晴らしく見えます!怠惰にならないように、結果にもきれいなグラフをいくつか追加しました。私のテストはあなたのテストよりも少しブルートフォースですが、より大きなマップを使用すると違いがより顕著になることがわかりました。小さな地図では、何をするにしても見逃すことはできません。JVMの最適化とGCのために、パフォーマンスは混沌としがちです。小さなデータセットの中には、その混沌によって強い結論が出されたという理論があります。
domchi 2011

パフォーマンスの取得に関するもう1つのコメント。混沌としているように見えますが、非常に狭い範囲で大きく変動することがわかりましたが、全体としては一定で、地獄のように退屈です。100/90のように、ときどき奇妙なスパイクが発生しました。説明することはできませんが、実際にはおそらく気づかれることはありません。
domchi 2011

G_H、私の答えを見てください。これは非常に古いスレッドですが、おそらくこれを念頭に置いてテストをやり直す必要があります。
durron597 2013年

ねえ、あなたはこれを会議論文としてACMに投稿するべきです:)なんて努力でしょう!
yerlilbilgin 2016年

12

これはかなり素晴らしいスレッドですが、見逃している重要なことが1つあります。あなたが言った:

不思議なことに、容量、容量+ 1、容量+ 2、容量-1、さらには容量-10でも、まったく同じ結果が得られます。少なくとも容量1と容量10の方が悪い結果になると思います。

ソースコードは、内部で次に高い2の累乗の初期容量をジャンプします。つまり、たとえば、513、600、700、800、900、1000、および1024の初期容量は、すべて同じ初期容量(1024)を使用します。これは@G_Hによって行われたテストを無効にするものではありませんが、彼の結果を分析する前にこれが行われていることを理解する必要があります。そして、それはいくつかのテストの奇妙な振る舞いを説明しています。

これは、JDKソースのコンストラクター権限です。

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity];
    init();
}

それは非常に興味深いです!私はこれを知りませんでした。確かに私がテストで見たものを説明しています。また、コンパイラやコードが背後で何をしているのか本当にわからない(または実際に知る必要がある)ため、時期尚早の最適化が役立つことがよくあることを確認します。そしてもちろん、バージョン/実装ごとに異なる場合があります。これを片付けてくれてありがとう!
G_H 2013

@G_Hこの情報を考慮して、より適切な数値を選択して、テストが再度実行されることを望んでいます。たとえば、1200個の要素がある場合、1024マップ、2048マップ、または4096マップを使用する必要がありますか?元の質問に対する答えがわからないので、このスレッドを最初に見つけました。とはいえ、グアバはあなたがそうexpectedSizeする1.33ときにあなたを倍増させることを私は知っていますMaps.newHashMap(int expectedSize)
durron597 2013

HashMapがの2の累乗の値に切り上げられない場合capacity、一部のバケットは使用されません。マップデータを配置する場所のバケットインデックスは、によって決定されbucketIndex = hashCode(key) & (capacity-1)ます。したがってcapacity、2の累乗以外の場合、のバイナリ表現に(capacity-1)はいくつかのゼロが含まれます。つまり、&(バイナリおよび)演算は常にhashCodeの特定の下位ビットをゼロにします。例:(capacity-1)ある111110(62)の代わりに111111(63)。この場合、インデックスが偶数のバケットのみを使用できます。
MichaelGeier19年

2

一緒に行くだけ 101です。実際に必要かどうかはわかりませんが、わざわざ調べてみる価値はないでしょう。

...追加するだけです 1です。


編集:私の答えの正当化。

まず、私はあなたHashMapがそれを超えて成長しないと仮定しています100含まれている場合は、負荷率をそのままにしておく必要があります。同様に、パフォーマンスが懸念される場合は、負荷率をそのままにしておきます。メモリが気になる場合は、静的サイズを設定することでいくらか節約できます。これはかもしれませんメモリにたくさんのものを詰め込んでいる場合、おそらく行う価値があるしれん。つまり、多くのマップを保存している、またはヒープスペースストレスサイズのマップを作成しています。

次に、101読みやすさを向上させるために値を選択します...後でコードを調べて、初期容量をに設定し、要素を100ロードしていることがわかった場合は、次のようにする必要があります。100Javadocを読んで、正確に到達したときにサイズが変更されないことを確認し100ます。もちろん、そこには答えが見つからないので、ソースを調べる必要があります。これは価値がありません...そのままにしておくと101、誰もが満足し、のソースコードを誰も見ていませんjava.util.HashMap。フーラ。

第三に、「のHashMap負荷係数で期待するものの正確な容量に設定すると、ルックアップと挿入のパフォーマンスが低下するという主張は、太字で作成されていても真実ではありません。1

...nバケットがあり、nアイテムをnバケットにランダムに割り当てる場合、そうです、同じバケット内のアイテムになってしまうでしょう...しかし、それは世界の終わりではありません...実際には、それはほんの2、3の同等の比較です。実際、特にあります。代替手段がnアイテムをn/0.75バケットに割り当てることであると考えると、ほとんど違いはありません。

私の言葉を信じる必要はありません...


クイックテストコード:

static Random r = new Random();

public static void main(String[] args){
    int[] tests = {100, 1000, 10000};
    int runs = 5000;

    float lf_sta = 1f;
    float lf_dyn = 0.75f;

    for(int t:tests){
        System.err.println("=======Test Put "+t+"");
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        long norm_put = testInserts(map, t, runs);
        System.err.print("Norm put:"+norm_put+" ms. ");

        int cap_sta = t;
        map = new HashMap<Integer,Integer>(cap_sta, lf_sta);
        long sta_put = testInserts(map, t, runs);
        System.err.print("Static put:"+sta_put+" ms. ");

        int cap_dyn = (int)Math.ceil((float)t/lf_dyn);
        map = new HashMap<Integer,Integer>(cap_dyn, lf_dyn);
        long dyn_put = testInserts(map, t, runs);
        System.err.println("Dynamic put:"+dyn_put+" ms. ");
    }

    for(int t:tests){
        System.err.println("=======Test Get (hits) "+t+"");
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        fill(map, t);
        long norm_get_hits = testGetHits(map, t, runs);
        System.err.print("Norm get (hits):"+norm_get_hits+" ms. ");

        int cap_sta = t;
        map = new HashMap<Integer,Integer>(cap_sta, lf_sta);
        fill(map, t);
        long sta_get_hits = testGetHits(map, t, runs);
        System.err.print("Static get (hits):"+sta_get_hits+" ms. ");

        int cap_dyn = (int)Math.ceil((float)t/lf_dyn);
        map = new HashMap<Integer,Integer>(cap_dyn, lf_dyn);
        fill(map, t);
        long dyn_get_hits = testGetHits(map, t, runs);
        System.err.println("Dynamic get (hits):"+dyn_get_hits+" ms. ");
    }

    for(int t:tests){
        System.err.println("=======Test Get (Rand) "+t+"");
        HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
        fill(map, t);
        long norm_get_rand = testGetRand(map, t, runs);
        System.err.print("Norm get (rand):"+norm_get_rand+" ms. ");

        int cap_sta = t;
        map = new HashMap<Integer,Integer>(cap_sta, lf_sta);
        fill(map, t);
        long sta_get_rand = testGetRand(map, t, runs);
        System.err.print("Static get (rand):"+sta_get_rand+" ms. ");

        int cap_dyn = (int)Math.ceil((float)t/lf_dyn);
        map = new HashMap<Integer,Integer>(cap_dyn, lf_dyn);
        fill(map, t);
        long dyn_get_rand = testGetRand(map, t, runs);
        System.err.println("Dynamic get (rand):"+dyn_get_rand+" ms. ");
    }
}

public static long testInserts(HashMap<Integer,Integer> map, int test, int runs){
    long b4 = System.currentTimeMillis();

    for(int i=0; i<runs; i++){
        fill(map, test);
        map.clear();
    }
    return System.currentTimeMillis()-b4;
}

public static void fill(HashMap<Integer,Integer> map, int test){
    for(int j=0; j<test; j++){
        if(map.put(r.nextInt(), j)!=null){
            j--;
        }
    }
}

public static long testGetHits(HashMap<Integer,Integer> map, int test, int runs){
    long b4 = System.currentTimeMillis();

    ArrayList<Integer> keys = new ArrayList<Integer>();
    keys.addAll(map.keySet());

    for(int i=0; i<runs; i++){
        for(int j=0; j<test; j++){
            keys.get(r.nextInt(keys.size()));
        }
    }
    return System.currentTimeMillis()-b4;
}

public static long testGetRand(HashMap<Integer,Integer> map, int test, int runs){
    long b4 = System.currentTimeMillis();

    for(int i=0; i<runs; i++){
        for(int j=0; j<test; j++){
            map.get(r.nextInt());
        }
    }
    return System.currentTimeMillis()-b4;
}

試験結果:

=======Test Put 100
Norm put:78 ms. Static put:78 ms. Dynamic put:62 ms. 
=======Test Put 1000
Norm put:764 ms. Static put:763 ms. Dynamic put:748 ms. 
=======Test Put 10000
Norm put:12921 ms. Static put:12889 ms. Dynamic put:12873 ms. 
=======Test Get (hits) 100
Norm get (hits):47 ms. Static get (hits):31 ms. Dynamic get (hits):32 ms. 
=======Test Get (hits) 1000
Norm get (hits):327 ms. Static get (hits):328 ms. Dynamic get (hits):343 ms. 
=======Test Get (hits) 10000
Norm get (hits):3304 ms. Static get (hits):3366 ms. Dynamic get (hits):3413 ms. 
=======Test Get (Rand) 100
Norm get (rand):63 ms. Static get (rand):46 ms. Dynamic get (rand):47 ms. 
=======Test Get (Rand) 1000
Norm get (rand):483 ms. Static get (rand):499 ms. Dynamic get (rand):483 ms. 
=======Test Get (Rand) 10000
Norm get (rand):5190 ms. Static get (rand):5362 ms. Dynamic get (rand):5236 ms. 

re:↑—これについては→||←異なる設定の間には大きな違いがあります


私の元の答え(最初の水平線の少し上)に関しては、ほとんどの場合このタイプのマイクロ最適化は適切ではないため、意図的にglibでした


@EJP、私の当て推量は正しくありません。上記の編集を参照してください。あなたの当て推量は、誰の当て推量が正しく、誰の当て推量が正しくないかについて正しくありません。
badroit 2011

(...多分私は少し卑劣です...私は少しイライラしています:P)
badroit 2011

3
EJPにイライラするのは当然かもしれませんが、今度は私の番です; P-早漏は早漏によく似ていることに同意しますが、私の場合、通常は努力する価値のないものが努力する価値がないと思い込まないでください。 。私の場合、推測したくないほど重要なので、調べました-私の場合は+1は必要ありません(ただし、初期/実際の容量が同じでなく、loadFactorが1でない場合もあります) HashMapのintへのキャストを参照してください:threshold =(int)(capacity * loadFactor))。
domchi 2011

@badroitあなたは、それが必要かどうかは実際にはわからないと明示的に言った」。したがって、それは当て推量でした。調査を行って投稿したので、それはもはや当て推量ではなく、事前に明らかそれを行っていなかったので、明らか当て推量でした。そうでなければ、あなたは確信していたでしょう。'incorrect'に関しては、Javadocは、数十年にわたる調査とG_​​Hの回答と同様に、0.75の負荷係数を明示的に義務付けています。最後に、「努力する価値はないだろう」については、ここでDomchiのコメントを参照してください。一般的に私はマイクロ最適化についてあなたに同意しますが、それは正しいことをあまり残していません。
ローン侯爵

みなさん、リラックスしてください。はい、私の答えは物事を誇張しました。非常に重いequals機能を持たないオブジェクトが100個ある場合は、それらをリストに入れて「含む」を使用するだけでうまくいくでしょう。このような小さなセットでは、パフォーマンスに大きな違いはありません。速度やメモリの問題が何よりも重要である場合、または等号とハッシュが非常に具体的である場合にのみ、本当に重要です。後で、大規模なコレクションとさまざまな負荷係数、および初期容量を使用してテストを行い、がらくたでいっぱいかどうかを確認します。
G_H 2011


1

HashMapJavaDocから:

原則として、デフォルトの負荷係数(.75)は、時間とスペースのコストの間で適切なトレードオフを提供します。値を大きくすると、スペースオーバーヘッドは減少しますが、ルックアップコストは増加します(getおよびputを含むHashMapクラスのほとんどの操作に反映されます)。再ハッシュ操作の数を最小限に抑えるために、初期容量を設定するときは、マップ内の予想されるエントリ数とその負荷率を考慮に入れる必要があります。初期容量が最大エントリ数を負荷率で割った値よりも大きい場合、再ハッシュ操作は発生しません。

したがって、100エントリを期待している場合は、負荷係数0.75と初期容量の上限(100 / 0.75)が最適です。それは134になります。

確かに、負荷率が高いほどルックアップコストが高くなる理由はわかりません。HashMapがより「混雑している」からといって、より多くのオブジェクトが同じバケットに配置されるという意味ではありませんか?私が間違っていなければ、それは彼らのハッシュコードにのみ依存します。それで、まともなハッシュコードの広がりを仮定すると、負荷率に関係なく、ほとんどの場合はまだO(1)であるべきではありませんか?

編集:投稿する前にもっと読む必要があります...もちろん、ハッシュコードはいくつかの内部インデックスに直接マップすることはできません。現在の容量に適合する値に減らす必要があります。つまり、初期容量が大きいほど、ハッシュ衝突の数は少なくなると予想できます。負荷係数が1のオブジェクトセットのサイズ(または+1)を正確に初期容量として選択すると、マップのサイズが変更されないようになります。しかしながら、ルックアップと挿入のパフォーマンスが低下します。サイズ変更はまだ比較的速く、おそらく1回だけ発生しますが、ルックアップはマップで関連するほとんどすべての作業で実行されます。結果として、クイックルックアップ用に最適化することがここで本当に必要なことです。JavaDocが言うように、サイズを変更する必要がないことと組み合わせることができます。必要な容量を取得し、最適な負荷係数(0.75など)で除算して、その負荷係数で初期容量として使用します。1を追加して、丸めが発生しないようにします。


1
それはあなたのルックアップと挿入のパフォーマンスを殺します」。これは誇張しすぎ/単純に正しくありません。
badroit 2011

1
私のテストでは、ロードファクターを1に設定しても、ルックアップパフォーマンスは影響を受けないことが示されています。挿入パフォーマンスは実際に改善されています。サイズ変更がないため、高速です。したがって、あなたのステートメントは一般的なケースでは正しいですが(要素数が少ないHashMapの検索は、1よりも0.75の方が高速です)、HashMapが常に最大容量までいっぱいで、変更されないという私の特定のケースでは正しくありません。初期サイズを高く設定するという提案は興味深いものですが、テーブルが大きくならないため、私の場合には関係ありません。したがって、負荷率はサイズ変更の観点からのみ重要です。
domchi 2011
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.