[java] 고정 크기 HashMap에 대한 최적의 용량 및로드 계수는 무엇입니까?

특정 경우에 대한 최적의 용량과 부하율을 파악하려고합니다. 요점은 알았지 만 나보다 더 잘 아는 사람으로부터 확인을 받아 주셔서 감사합니다. 🙂

내 HashMap이 100 개의 객체를 포함하도록 채워지고 100 개의 객체를 갖는 데 대부분의 시간을 소비 할 것이라는 것을 알고 있다면 최적의 값은 초기 용량 100과로드 팩터 1이라고 추측하고 있습니다. 아니면 용량 101이 필요합니까, 아니면 다른 문제가 있습니까?

편집 : 좋아, 몇 시간을 따로두고 테스트를했습니다. 결과는 다음과 같습니다.

  • 흥미롭게도 용량, 용량 +1, 용량 +2, 용량 -1, 심지어 용량 -10도 모두 정확히 동일한 결과를 산출합니다. 나는 적어도 capacity-1과 capacity-10이 더 나쁜 결과를 줄 것이라고 기대합니다.
  • 초기 용량 (기본값 16을 사용하는 것과 반대)을 사용하면 put ()이 눈에 띄게 향상되어 최대 30 % 더 빨라집니다.
  • 부하율 1을 사용하면 적은 수의 개체에 대해 동일한 성능을 제공하고 많은 수의 개체 (> 100000)에 대해 더 나은 성능을 제공합니다. 그러나 이것은 개체 수에 비례하여 개선되지 않습니다. 결과에 영향을 미치는 추가 요인이 있다고 생각합니다.
  • get () 성능은 개체 수 / 용량에 따라 조금씩 다르지만 케이스마다 약간 씩 다를 수 있지만 일반적으로 초기 용량이나 부하 계수의 영향을받지 않습니다.

EDIT2 : 내 부분에 차트를 추가합니다. 다음은 HashMap을 초기화하고 최대 용량까지 채우는 경우 부하 계수 0.75와 1의 차이를 보여주는 것입니다. y 스케일에서 시간은 ms (낮을수록 좋음)이고 x 스케일은 크기 (개체 수)입니다. 크기가 선형 적으로 변경되기 때문에 필요한 시간도 선형 적으로 증가합니다.

그래서 내가 무엇을 얻었는지 보자. 다음 두 차트는 부하율의 차이를 보여줍니다. 첫 번째 차트는 HashMap이 용량이 가득 차면 어떻게되는지 보여줍니다. 부하 계수 0.75는 크기 조정으로 인해 성능이 저하됩니다. 그러나 일관되게 나쁘지는 않으며 모든 종류의 범프와 홉이 있습니다. GC가 이것에서 중요한 역할을한다고 생각합니다. 부하율 1.25는 1과 동일하게 수행되므로 차트에 포함되지 않습니다.

완전히 채워진

이 차트는 크기 조정으로 인해 0.75가 더 나빴 음을 증명합니다. HashMap을 절반 용량으로 채우면 0.75는 나쁘지 않습니다. 단지 … 다릅니다 (그리고 더 적은 메모리를 사용하고 눈에 띄지 않게 더 나은 반복 성능을 가져야합니다).

반쯤 찬

한 가지 더 보여 드리고 싶습니다. 이것은 세 가지로드 요소 및 다른 HashMap 크기 모두에 대한 성능을 얻습니다. 로드 팩터 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);
  }

}



답변

좋습니다.이 문제를 해결하기 위해 몇 가지 시나리오를 실행하고 결과를 시각화 할 수있는 테스트 앱을 만들었습니다. 테스트가 수행되는 방법은 다음과 같습니다.

  • 다양한 컬렉션 크기가 시도되었습니다 : 백, 천, 십만 항목.
  • 사용되는 키는 ID로 고유하게 식별되는 클래스의 인스턴스입니다. 각 테스트는 정수를 ID로 증분하는 고유 키를 사용합니다. 이 equals방법은 ID 만 사용하므로 키 매핑이 다른 ID를 덮어 쓰지 않습니다.
  • 키는 일부 사전 설정 번호에 대한 ID의 나머지 모듈로 구성된 해시 코드를 얻습니다. 이 숫자를 해시 제한이라고 합니다. 이를 통해 예상되는 해시 충돌 수를 제어 할 수있었습니다. 예를 들어 컬렉션 크기가 100이면 0에서 99 사이의 ID를 가진 키가 있습니다. 해시 제한이 100이면 모든 키는 고유 한 해시 코드를 갖게됩니다. 해시 제한이 50 인 경우 키 0은 키 50과 동일한 해시 코드를 갖고 1은 51과 동일한 해시 코드를 갖습니다. 즉, 키당 예상되는 해시 충돌 수는 컬렉션 크기를 해시로 나눈 값입니다. 한도.
  • 컬렉션 크기와 해시 제한의 각 조합에 대해 다른 설정으로 초기화 된 해시 맵을 사용하여 테스트를 실행했습니다. 이러한 설정은 부하 계수이며 수집 설정의 계수로 표현되는 초기 용량입니다. 예를 들어, 컬렉션 크기가 100이고 초기 용량 계수가 1.25 인 테스트는 초기 용량이 125 인 해시 맵을 초기화합니다.
  • 각 키의 값은 단순히 새로운 Object.
  • 각 테스트 결과는 Result 클래스의 인스턴스에 캡슐화됩니다. 모든 테스트가 끝나면 결과는 최악의 전체 성능에서 최고로 정렬됩니다.
  • 풋 및 겟의 평균 시간은 풋 / 겟 10 회당 계산됩니다.
  • 모든 테스트 조합은 JIT 컴파일 영향을 제거하기 위해 한 번 실행됩니다. 그 후 실제 결과에 대한 테스트가 실행됩니다.

수업은 다음과 같습니다.

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 파일로 출력하는 시각화 도우미를 호출합니다. 이에 대한 클래스는 다음과 같습니다. 실행하려면 위 코드에서 해당 줄의 주석 처리를 제거하십시오. 경고 : Visualizer 클래스는 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 차원 “히트 맵”입니다.
  • 이미지의 색상은 포화 된 녹색에서 포화 된 빨간색에 이르기까지 최상의 결과에서 최악의 결과까지 정규화 된 척도의 평균 시간을 기반으로합니다. 즉, 가장 좋은 시간은 완전히 녹색이고 최악의 시간은 완전히 빨간색입니다. 두 개의 다른 시간 측정은 동일한 색상을 가져서는 안됩니다.
  • 색상 맵은 풋과 겟에 대해 별도로 계산되지만 해당 범주에 대한 모든 테스트를 포함합니다.
  • 시각화는 x 축의 초기 용량과 y 축의 부하 계수를 보여줍니다.

더 이상 고민하지 않고 결과를 살펴 보겠습니다. 풋에 대한 결과부터 시작하겠습니다.

결과 넣기


컬렉션 크기 : 100. 해시 제한 : 50. 이는 각 해시 코드가 두 번 발생하고 다른 모든 키가 해시 맵에서 충돌해야 함을 의미합니다.

size_100_hlimit_50_puts

글쎄요, 그게 좋은 시작은 아닙니다. 컬렉션 크기보다 25 % 더 큰 초기 용량에 대한 큰 핫스팟이 있고로드 계수 1이 있음을 알 수 있습니다. 왼쪽 하단 모서리가 너무 잘 수행되지 않습니다.


컬렉션 크기 : 100. 해시 제한 : 90. 키 열 개 중 하나에 중복 된 해시 코드가 있습니다.

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 개의 해시 코드 중 하나가 두 번 발생 함을 의미합니다. 충돌에 관한 합리적인 시나리오.

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 % 추가 공간이있는 초기 용량이 이깁니다.


좋아, 그게 풋내기 다. 이제 겟을 확인하겠습니다. 아래지도는 모두 최고 / 최악의 획득 시간에 상대적이며 풋 시간은 더 이상 고려되지 않습니다.

결과 얻기


컬렉션 크기 : 100. 해시 제한 : 50. 이는 각 해시 코드가 두 번 발생하고 다른 모든 키가 해시 맵에서 충돌 할 것으로 예상됨을 의미합니다.

size_100_hlimit_50_gets

어 … 뭐?


컬렉션 크기 : 100. 해시 제한 : 90. 키 열 개 중 하나에 중복 된 해시 코드가 있습니다.

size_100_hlimit_90_gets

워 넬리! 이것은 질문자의 질문과 관련이있을 가능성이 가장 높은 시나리오이며, 부하 계수가 1 인 초기 용량 100은 여기서 최악의 상황 중 하나입니다! 나는 이것을 위조하지 않았다고 맹세합니다.


컬렉션 크기 : 100. 해시 제한 : 100. 각 키는 고유 한 해시 코드입니다. 예상되는 충돌이 없습니다.

size_100_hlimit_100_gets

이것은 좀 더 평화로워 보입니다. 전반적으로 거의 동일한 결과입니다.


컬렉션 크기 : 1000. 해시 제한 : 500. 첫 번째 테스트와 마찬가지로 해시 오버로드가 2 대 1이지만 이제는 더 많은 항목이 있습니다.

size_1000_hlimit_500_gets

모든 설정이 여기에서 괜찮은 결과를 낳을 것 같습니다.


컬렉션 크기 : 1000. 해시 제한 : 900. 이것은 10 개의 해시 코드 중 하나가 두 번 발생 함을 의미합니다. 충돌에 관한 합리적인 시나리오.

size_1000_hlimit_900_gets

그리고이 설정의 풋과 마찬가지로 이상한 지점에서 이상 현상이 발생합니다.


컬렉션 크기 : 1000. 해시 제한 : 990. 일부 충돌이 있지만 몇 가지만 있습니다. 이 점에서 매우 현실적입니다.

size_1000_hlimit_990_gets

높은 초기 용량과 낮은 부하 계수의 조합을 제외하고 어디에서나 적절한 성능을 제공합니다. 두 번의 해시 맵 크기 조정이 예상 될 수 있으므로 풋에 대해 이것을 기대합니다. 하지만 왜 얻을 수 있습니까?


컬렉션 크기 : 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 %의 초기 용량을 선호하는 것 같습니다.


좋습니다. 이제 결론을 내릴 시간입니다 …

  • 넣기 시간과 관련하여 : 예상되는 맵 항목 수보다 적은 초기 용량을 피하는 것이 좋습니다. 정확한 숫자를 미리 알고 있다면 그 숫자 또는 약간 위의 숫자가 가장 잘 작동하는 것 같습니다. 높은 부하 요인은 초기 해시 맵 크기 조정으로 인해 낮은 초기 용량을 상쇄 할 수 있습니다. 더 높은 초기 용량의 경우 그다지 중요하지 않은 것 같습니다.
  • GET 시간과 관련하여 : 여기서 결과는 약간 혼란 스럽습니다. 결론이별로 없습니다. 해시 코드 겹침, 초기 용량 및로드 팩터 사이의 미묘한 비율에 크게 의존하는 것 같습니다.
  • Java 성능에 대한 가정에 관해서는 분명히 쓰레기로 가득 차 있습니다. 사실은의 구현에 맞게 설정을 완벽하게 조정하지 않는 한 HashMap결과가 모든 곳에 나타날 것입니다. 여기서 빼야 할 것이 있다면, 기본 초기 크기 인 16은 가장 작은지도를 제외하고는 조금 멍청하다는 것입니다. 따라서 어떤 크기의 순서에 대한 아이디어가 있다면 초기 크기를 설정하는 생성자를 사용하십시오. 곧 될거야.
  • 여기서는 나노초 단위로 측정하고 있습니다. 10 개 풋당 최고 평균 시간은 1179ns이고 내 컴퓨터에서 최악의 5105ns였습니다. 10 회당 최고 평균 시간은 547ns이고 최악의 경우 3484ns입니다. 그것은 요소 6 차이 일 수 있지만 우리는 1 밀리 초 미만으로 이야기하고 있습니다. 원래 포스터가 생각했던 것보다 훨씬 더 큰 컬렉션.

글쎄, 그게 다야. 내 코드에 내가 여기에 게시 한 모든 것을 무효화하는 끔찍한 감독이 없기를 바랍니다. 이것은 재미 있었고 결국에는 작은 최적화와 많은 차이를 기대하는 것보다 Java에 의존하여 작업을 수행 할 수 있다는 것을 알게되었습니다. 그것은 어떤 것들을 피해서는 안된다는 말은 아니지만, 우리는 주로 for 루프에서 긴 문자열을 구성하고, 잘못된 데이터 구조를 사용하고, O (n ^ 3) 알고리즘을 만드는 것에 대해 이야기하고 있습니다.


답변

이것은 당신이 놓친 중요한 것이 하나 있다는 점을 제외하면 꽤 훌륭한 스레드입니다. 당신은 말했다 :

흥미롭게도 용량, 용량 +1, 용량 +2, 용량 -1, 심지어 용량 -10도 모두 정확히 동일한 결과를 산출합니다. 나는 적어도 capacity-1과 capacity-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();
}


답변

그냥 101. 실제로 이것이 필요한지 확신 할 수는 없지만 확실히 알아 내기 위해 노력할 가치가있을 수는 없습니다.

… 단지 1.


편집 : 내 대답에 대한 몇 가지 정당화.

첫째, 나는 당신의 HashMap의지가 그 이상으로 성장하지 않을 것이라고 가정합니다 100. 그렇다면로드 팩터를 그대로 두어야합니다. 마찬가지로 성능이 문제인 경우로드 팩터를 그대로 둡니다 . 메모리가 염려된다면 정적 크기를 설정하여 일부를 절약 할 수 있습니다. 이 는 메모리에 물건을 많이 밀어 넣는다면 어쩌면 가치는 일을 할; 즉, 많은 맵을 저장하거나 힙 공간 스트레스 크기의 맵을 생성합니다.

둘째, 101더 나은 가독성을 제공하기 때문에 값을 선택합니다. 나중에 코드를보고 초기 용량을로 설정하고 요소로 100로드하는 100것을 확인하면 정확하게 도달했을 때 크기가 조정되지 않는지 확인하려면 Javadoc을 읽어보십시오 100. 물론 거기에서 답을 찾을 수 없으므로 출처를 봐야합니다. 이것은 그만한 가치가 없습니다 … 그냥 그냥두면 101모두가 행복하고 아무도 java.util.HashMap. 후라.

셋째, 설정한다는 주장 HashMap이의 부하 계수와 기대의 정확한 용량을 1 당신 조회 및 삽입 성능을 죽일 것이다는 이 굵게 만들어졌다하더라도, 단지 사실이 아니다.

n버킷이 있고 무작위 nn버켓에 항목을 할당 하면 동일한 버켓에있는 항목으로 끝날 것입니다. 물론 … 그것이 세상의 끝이 아닙니다. 실제로는 그것은 단지 몇 가지 더 동등한 비교입니다. 사실 esp가 있습니다. 대안이 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 : ↑ — 이것에 대해 → || ← 다른 설정 사이에 많은 차이가 있습니다.


에 있기 때문에 내 원래의 대답 (첫 번째 수평 라인 위의 비트)와 관련하여, 그것은 의도적으로 입심 된 대부분의 경우 , 마이크로 최적화의이 유형은 좋지 않다 .


답변

구현 측면에서 Google Guava에는 편리한 공장 방법이 있습니다.

Maps.newHashMapWithExpectedSize(expectedSize)

어느 용량 계산 수식을 사용하여

capacity = expectedSize / 0.75F + 1.0F


답변

로부터 HashMap의 JavaDoc :

일반적으로 기본 부하 계수 (.75)는 시간과 공간 비용간에 적절한 균형을 제공합니다. 값이 높을수록 공간 오버 헤드가 감소하지만 조회 비용이 증가합니다 (get 및 put을 포함한 HashMap 클래스의 대부분의 작업에 반영됨). 재해시 작업 수를 최소화하기 위해 초기 용량을 설정할 때 맵의 예상 항목 수와로드 계수를 고려해야합니다. 초기 용량이 최대 항목 수를로드 계수로 나눈 값보다 크면 재해시 작업이 발생하지 않습니다.

따라서 100 개의 항목이 예상되는 경우로드 계수 0.75와 천장의 초기 용량 (100 / 0.75)이 가장 좋습니다. 그것은 134로 내려갑니다.

나는 인정해야한다. 더 높은 부하율에 대해 조회 비용이 더 큰 이유는 확실하지 않다. HashMap이 더 붐비다고해서 동일한 버킷에 더 많은 객체가 배치되는 것은 아닙니다. 내가 착각하지 않으면 해시 코드에만 달려 있습니다. 따라서 적절한 해시 코드 확산을 가정하면 대부분의 경우 부하 계수에 관계없이 여전히 O (1)이어야하지 않습니까?

편집 : 게시하기 전에 더 읽어야합니다 … 물론 해시 코드는 일부 내부 색인에 직접 매핑 할 수 없습니다. 현재 용량에 맞는 값으로 줄여야합니다. 즉, 초기 용량이 클수록 해시 충돌 횟수가 줄어들 것으로 예상 할 수 있습니다. 로드 팩터가 1 인 개체 세트의 크기 (또는 +1)와 정확히 일치하는 초기 용량을 선택하면 실제로 맵의 크기가 조정되지 않습니다. 그러나 조회 및 삽입 성능이 저하됩니다.. 크기 조정은 여전히 ​​상대적으로 빠르며지도와 관련된 거의 모든 작업에서 조회가 수행되는 동안 한 번만 발생합니다. 결과적으로 빠른 조회를위한 최적화가 여기에서 정말로 원하는 것입니다. JavaDoc이 말한대로 크기를 조정할 필요없이 결합 할 수 있습니다. 필요한 용량을 최적의로드 요소 (예 : 0.75)로 나누고 해당로드 요소를 사용하여 초기 용량으로 사용합니다. 반올림하지 않도록 1을 더하십시오.


답변