固定大小的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标度是时间(单位为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);
  }

}

1
从某种意义上说,在这种情况下更改默认值会带来更好的性能(更快的put()执行),这是最佳的。
Domchi

2
@Peter GC =垃圾回收。
Domchi

2
这些图表很整齐...您会用什么来生成/渲染它们?
G_H 2011年

1
@G_H没什么好看的-以上程序和Excel的输出。:)
Domchi

2
下次,使用点代替线。这将使比较在视觉上更容易。
Paul Draper

Answers:


74

好了,让这件事变得轻松,我创建了一个测试应用程序,可以运行几个场景并获得结果的可视化显示。测试方法如下:

  • 尝试了多种不同的收集大小:十万,十万和十万个条目。
  • 使用的键是由ID唯一标识的类的实例。每个测试使用唯一的键,并以递增的整数作为ID。该equals方法仅使用ID,因此没有键映射会覆盖另一个ID。
  • 密钥将获得一个哈希码,该哈希码由其ID的模块其余部分和某个预设数字组成。我们将该数字称为哈希限制。这使我能够控制预期的哈希冲突次数。例如,如果我们的集合大小为100,我们将拥有ID范围为0到99的键。如果哈希限制为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个获取)的输出图像。图像是二维“热图”,显示了初始容量和负载因子的每种组合的颜色。
  • 图像中的颜色基于从最佳到最差结果的标准化时间的平均时间,从饱和绿色到饱和红色。换句话说,最佳时间将完全变为绿色,而最差时间将完全变为红色。两种不同的时间测量绝对不能使用相同的颜色。
  • 颜色图是针对放置和获取而单独计算的,但涵盖了各自类别的所有测试。
  • 可视化图在其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

初始容量为100,负载系数为1似乎很好。出人意料的是,较高的初始容量和较低的负载系数不一定是好的。


集合大小:1000。散列限制:500。这里越来越严重,有1000个条目。就像第一个测试一样,哈希重载为2比1。

size_1000_hlimit_500_puts

左下角的效果仍然不理想。但是,较低的初始计数/高负载因子与较高的初始计数/低负载因子的组合之间似乎存在对称性。


集合大小:1000。哈希限制:900。这意味着十分之一的哈希码将出现两次。关于碰撞的合理场景。

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。好吧,它现在变得越来越严重,每个密钥的样本大小为十万,并且有100个哈希码重复项。

大小_100000_hlimit_10000_puts

kes!我认为我们发现了较低的频谱。在这里,加载大小为1的集合大小的初始容量确实做得很好,但除此之外,它遍及整个商店。


集合大小:100_000。哈希限制:90_000。比之前的测试更实际,这里我们的哈希码过载了10%。

大小_100000_hlimit_90000_puts

左下角仍然是不可取的。较高的初始容量效果最佳。


集合大小:100_000。哈希限制:99_000。好方案,这个。具有1%哈希码重载的大型集合。

大小_100000_hlimit_99000_puts

在这里使用精确的集合大小作为初始容量(负载因子为1)会胜出!不过,稍大的init容量工作得很好。


集合大小:100_000。哈希限制:100_000。大的那个。具有完善哈希函数的最大集合。

大小_100000_hlimit_100000_puts

这里有些令人惊讶的东西。初始容量为50%的额外空间(负载系数为1)获胜。


好了,仅此而已。现在,我们将检查获取。请记住,下面的图都是相对于最佳/最差获取时间的,不再考虑放置时间。

获得结果


集合大小:100。哈希限制:50。这意味着每个哈希码应出现两次,并且每个其他键都应在哈希图中碰撞。

size_100_hlimit_50_gets

嗯...什么?


集合大小:100。哈希限制:90。十分之一的键具有重复的哈希码。

size_100_hlimit_90_gets

哇,奈利!这是最可能与质问者的问题相关的场景,显然,初始容量为100且负载系数为1是这里最糟糕的事情之一!我发誓我没有假冒。


集合大小:100。散列限制:100。每个键作为其自己的唯一散列码。预计不会发生碰撞。

size_100_hlimit_100_gets

这看起来更加和平。总体而言,结果大致相同。


集合大小:1000。哈希限制:500。就像在第一个测试中一样,哈希过载为2:1,但是现在有更多条目。

size_1000_hlimit_500_gets

看起来任何设置都会在这里产生不错的结果。


集合大小:1000。哈希限制:900。这意味着十分之一的哈希码将出现两次。关于碰撞的合理场景。

size_1000_hlimit_900_gets

就像与此设置的推杆一样,我们在一个奇怪的地方出现了异常。


集合大小:1000。哈希限制:990。有一些冲突,但只有少数。在这方面非常现实。

size_1000_hlimit_990_gets

除具有高初始容量和低负载因数的组合外,任何地方都具有不错的性能。我希望在puts上使用它,因为可能需要调整两个哈希图的大小。但是为什么要得到呢?


集合大小:1000。哈希限制:1000。没有重复的哈希码,但现在的样本大小为1000。

size_1000_hlimit_1000_gets

完全不可视的可视化。无论如何,这似乎都有效。


集合大小:100_000。哈希限制:10_000。再次进入100K,有很多哈希码重叠。

大小_100000_hlimit_10000_gets

尽管坏点非常局限,但看起来并不漂亮。这里的性能似乎很大程度上取决于设置之间的某种协同作用。


集合大小:100_000。哈希限制:90_000。比之前的测试更实际,这里我们的哈希码过载了10%。

大小_100000_hlimit_90000_gets

差异很大,但是如果斜眼可以看到指向右上角的箭头。


集合大小:100_000。哈希限制:99_000。好方案,这个。具有1%哈希码重载的大型集合。

大小_100000_hlimit_99000_gets

很混乱。在这里很难找到很多结构。


集合大小:100_000。哈希限制:100_000。大的那个。具有完善哈希函数的最大集合。

大小_100000_hlimit_100000_gets

其他人认为这开始看起来像Atari图形吗?这似乎有利于收集容量正好为-25%或+ 50%的初始容量。


好了,现在是结论的时候了...

  • 关于放置时间:您希望避免初始容量小于预期的映射条目数。如果事先知道确切的数字,那么该数字或稍微高于该数字的数字似乎效果最好。由于较早的哈希图调整大小,高负载因子可以抵消较低的初始容量。对于更高的初始容量,它们似乎并不重要。
  • 关于获取时间:这里的结果有点混乱。结论不多。它似乎很大程度上依赖于哈希码重叠,初始容量和负载因子之间的微妙比率,一些据说不好的设置执行得很好,而好的设置则执行得很差。
  • 当谈到有关Java性能的假设时,我显然满是垃圾。事实是,除非您完美地将设置调整为的实现,否则HashMap结果将无处不在。如果要解决的是一件事,那就是默认的初始大小16对于除了最小的地图之外的任何东西来说都有点笨,因此,如果您对大小的顺序有任何想法,请使用设置初始大小的构造函数这将是。
  • 我们在这里以纳秒为单位进行测量。每10个看跌期权的最佳平均时间是1179 ns,而我的机器则是最差的5105 ns。每10次获取的最佳平均时间为547 ns,最差的3484 ns。这可能相差6倍,但我们的通话时间不到一毫秒。在比原始海报构思的要大得多的收藏中。

好,就是这样。我希望我的代码不会受到可怕的监督,从而使我在此处发布的所有内容失效。这很有趣,而且我了解到,最终,您可以依靠Java来完成其工作,而不是期望从微小的优化中获得很大的不同。这并不是说不应该避免某些事情,但是我们主要是在谈论在for循环中构造冗长的String,使用错误的数据结构并使O(n ^ 3)算法。


1
感谢您的努力,看起来很棒!别偷懒,我也在结果中添加了一些漂亮的图表。我的测试比您的测试更有力,但是我发现使用更大的地图时差异更加明显。使用小地图,无论做什么,您都不会错过。由于JVM优化和GC,性能趋于混乱,而且我有一种理论认为,对于一些较小的数据集,任何有力的结论都会被这种混乱所吞噬。
Domchi

关于获得性能的另一条评论。看起来很混乱,但是我发现它在非常狭窄的范围内变化很大,但是总的来说,它是恒定的,令人讨厌。我确实偶尔会遇到像您在100/90上那样奇怪的峰值。我无法解释,但是在实践中这可能并不明显。
Domchi

G_H,请看一下我的回答,我知道这是一个非常旧的线程,但是考虑到这一点,可能应该重做测试。
durron597 2013年

嘿,您应该将此作为会议论文发布到ACM :)多么努力!
yerlilbilgin '16

12

这是一个非常棒的线程,除了您缺少一件事。你说:

奇怪的是,容量,容量+1,容量+2,容量1甚至容量10都产生完全相同的结果。我希望至少容量1和容量10会产生较差的结果。

源代码在内部将初始容量跃升为第二高的二乘方。这意味着,例如,初始容量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

@G_H我希望看到您的测试再次运行,鉴于此信息,请选择更合适的数字。例如,如果我有1200个元素,是否应该使用1024映射,2048映射或4096映射?我不知道原始问题的答案,这就是为什么我发现此线程开始的原因。虽然,我知道,番石榴乘以你expectedSize1.33,当你做Maps.newHashMap(int expectedSize)
durron597

如果HashMap无法将舍入为2的幂capacity,则将永远不会使用某些存储桶。用于放置地图数据的存储区索引由确定bucketIndex = hashCode(key) & (capacity-1)。因此,如果capacity除2的幂以外的其他任何值,的二进制表示形式都(capacity-1)将带有一些零,这意味着&(二进制和)运算将始终使hashCode的某些低位清零。示例:(capacity-1)111110(62)而不是111111(63)。在这种情况下,只能使用索引为偶数的存储桶。
Michael Geier

2

随便去吧101。我实际上不确定是否需要它,但是花时间去确定确实是不值得的。

...只需添加1


编辑:我的答案有些道理。

首先,我假设你HashMap不会超越100如果是这样,则应保持负载系数不变。同样,如果您关注的是性能,请保持负载因子不变。如果您担心内存问题,可以通过设置静态大小来节省一些内存。这可能也许是值得做的,如果你正在恶补了很多的记忆的东西; 也就是说,正在存储许多地图,或创建堆空间压力大小的地图。

其次,我选择该值101是因为它提供了更好的可读性...如果以后再查看您的代码,并且看到您已将初始容量设置为,100并且正在使用100元素加载该值,那么我将不得不仔细阅读Javadoc以确保它在精确到达时不会调整大小100。当然,我在那里找不到答案,所以我必须看看源头。这是不值得的……只要留下它101,每个人都会很高兴,而且没人在看的源代码java.util.HashMap。呼啦。

第三,声称即使将其设置为HashMap您期望的确切容量,并且负载因子为1 将会杀死您的查找和插入性能”,这是不正确的,即使它以粗体显示也是如此。

...如果您有n存储桶,并且将n物品随机分配到n存储桶中,是的,您将最终在同一存储桶中放置物品,当然...但这还不是世界末日...实际上,只是比较了几个。实际上,有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:↑—不同设置之间存在此→||←差异


对于我的原始答案(第一条水平线上方的位),这是故意的glib,因为在大多数情况下这种微优化效果不好


@EJP,我的猜测并不正确。参见上面的编辑。您的猜测是关于谁的猜测是正确的以及谁的猜测是错误的。
badroit

(...也许我有点狡猾...虽然我有点生气:P)
badroit 2011年

3
您可能对EJP感到很生气,但是现在轮到我了; P-虽然我同意过早的优化很像早泄,但请不要以为我通常不值得付出的努力就不值得。就我而言,我不想猜测就非常重要,因此我进行了查找-在我的情况下不需要+1(但可能是您的初始/实际容量不相同且loadFactor不是1的情况)请参见在HashMap中将此类型转换为int:threshold =(int)(capacity * loadFactor))。
Domchi

@badroit您明确表示我实际上不确定是否需要它。因此,这是猜测。既然您已经完成并发布了研究,则不再是猜测,而且您显然事先没有做过,显然猜测,否则您将可以肯定。至于“不正确”,Javadoc明确规定了0.75的负载系数,数十年来的研究也是如此,G_H的回答也是如此。最后,关于“这可能不值得付出努力”,请参阅此处的Domchi评论。没什么大不了的,尽管总的来说,我同意您关于微优化的观点。
罗恩侯爵,

大家放松 是的,我的回答夸大了事情。如果您有100个没有强大equals功能的对象,则可能会把它们放到List中,而只使用“ contains”。如此之小,性能永远不会有太大差异。只有在速度或内存问题高于一切,或者等于和哈希是非常特定的条件时,这才真正重要。稍后,我将使用大量集合以及各种负载系数和初始容量进行测试,以查看是否充满垃圾。
2011年


1

来自 HashMapJavaDoc:

通常,默认负载因子(.75)在时间和空间成本之间提供了一个很好的权衡。较高的值会减少空间开销,但会增加查找成本(在HashMap类的大多数操作中都得到体现,包括get和put)。设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度地减少重新哈希操作的次数。如果初始容量大于最大条目数除以负载因子,则将不会发生任何哈希操作。

因此,如果您希望有100个条目,则最好是0.75的负载系数和上限(100 / 0.75)的初始容量。降至134。

我不得不承认,我不确定为什么对于较高的负载系数查找成本会更高。仅仅因为HashMap更“拥挤”并不意味着将更多对象放置在同一存储桶中,对吗?如果我没有记错的话,那仅取决于他们的哈希码。因此,假设散列码散布得很好,那么不管负载系数如何,大多数情况下是否仍应为O(1)?

编辑:我应该在发布之前阅读更多内容……当然,哈希码不能直接映射到某些内部索引。必须将其减小到适合当前容量的值。这意味着您的初始容量越大,可以预期的哈希冲突次数就越小。选择负载系数为1的对象集的大小(或+1)的初始容量确实可以确保地图永远不会调整大小。然而,它将破坏您的查找和插入性能。调整大小仍然相对较快,可能只会进行一次,而查找几乎是在与地图相关的所有工作上完成的。因此,您真正想要的是针对快速查找进行优化。您可以按照JavaDoc所说的那样将其与无需调整大小相结合:获取所需的容量,除以最佳负载系数(例如0.75),然后将其用作具有该负载系数的初始容量。加1以确保四舍五入不会使您失望。


1
它将杀死您的查找和插入性能”。这过于夸张/不正确。
badroit

1
我的测试表明,将负载因子设置为1不会影响查找性能。由于没有调整大小,因此速度更快。因此,对于一般情况,您的陈述是正确的(使用0.75的元素比使用1的元素的HashMap的查找更快),但是对于我的特定情况,当HashMap始终充满其最大容量(永远不变)时,这是不正确的。您建议将初始大小设置为更高的建议很有趣,但由于我的表没有增长,因此与我的情况无关,因此,仅考虑到调整大小,负载因子才重要。
Domchi
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.