固定大小的 HashMap 的最佳容量和负载因子是多少?

Posted

技术标签:

【中文标题】固定大小的 HashMap 的最佳容量和负载因子是多少?【英文标题】:What is the optimal capacity and load factor for a fixed-size HashMap? 【发布时间】:2011-10-30 05:39:37 【问题描述】:

我正在尝试找出特定情况下的最佳容量和负载系数。我想我明白了它的要点,但我仍然会感谢比我更有知识的人的确认。 :)

如果我知道我的 HashMap 将被填满以包含 100 个对象,并且大部分时间将花费 100 个对象,我猜测最佳值是初始容量 100 和负载因子 1?或者我需要容量 101,还是有其他问题?

编辑:好的,我留出几个小时进行了一些测试。结果如下:

奇怪的是,容量、容量+1、容量+2、容量-1 甚至容量-10 都会产生完全相同的结果。我预计至少容量 1 和容量 10 会产生更差的结果。 使用初始容量(而不是使用默认值 16)可以显着提高 put() - 速度提高 30%。 使用负载因子 1 可以为少量对象提供相同的性能,而为大量对象 (>100000) 提供更好的性能。然而,这并没有与对象的数量成比例地提高;我怀疑还有其他因素会影响结果。 get() 性能对于不同数量的对象/容量会有所不同,但尽管可能会因情况而异,但通常不受初始容量或负载因子的影响。

EDIT2:我也添加了一些图表。这是说明负载因子 0.75 和 1 之间差异的一个例子,在我初始化 HashMap 并将其填充到最大容量的情况下。在 y 尺度上是以毫秒为单位的时间(越低越好),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);
  


【问题讨论】:

@Peter GC = 垃圾回收。 那些图表很整洁......你用什么来生成/渲染它们? @G_H 没什么特别的 - 上述程序和 Excel 的输出。 :) 下次用点代替线。这将使比较在视觉上更容易。 【参考方案1】:

好的,为了解决这个问题,我创建了一个测试应用程序来运行几个场景并获得一些结果的可视化。以下是测试的完成方式:

已尝试了多种不同的集合大小:一百、一千和十万个条目。 使用的键是由 ID 唯一标识的类的实例。每个测试都使用唯一的键,并以递增的整数作为 ID。 equals 方法仅使用 ID,因此不会覆盖另一个键映射。 密钥获得一个哈希码,该哈希码由其 ID 的模块剩余部分与某个预设编号组成。我们将该数字称为哈希限制。这使我能够控制预期的哈希冲突的数量。例如,如果我们的集合大小为 100,我们将拥有 ID 范围为 0 到 99 的键。如果哈希限制为 100,则每个键将具有唯一的哈希码。如果哈希限制为 50,则密钥 0 将具有与密钥 50 相同的哈希码,1 将具有与 51 相同的哈希码等。换句话说,每个密钥的预期哈希冲突数是集合大小除以哈希限制。 对于集合大小和散列限制的每种组合,我使用不同设置初始化的散列映射运行测试。这些设置是负载因子,以及表示为集合设置因子的初始容量。例如,一个集合大小为 100 且初始容量因子为 1.25 的测试将初始化一个初始容量为 125 的哈希映射。 每个键的值只是一个新的Object。 每个测试结果都封装在一个 Result 类的实例中。在所有测试结束时,结果按总体性能从最差到最好的顺序排列。 put 和 get 的平均时间是按每 10 个 puts/get 计算的。 所有测试组合都运行一次,以消除 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 文件。下面给出了这个类。如果您想运行它,请取消注释上面代码中的相应行。请注意:可视化器类假定您在 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 次获取)的输出图像。这些图像是二维“热图”,根据初始容量和负载因子的组合显示一种颜色。 图像中的颜色基于从最佳结果到最差结果的标准化尺度的平均时间,范围从饱和绿色到饱和红色。换句话说,最好的时间是全绿的,而最差的时间是全红的。两个不同的时间测量值决不能使用相同的颜色。 颜色图是针对 put 和 get 单独计算的,但包含其各自类别的所有测试。 可视化在其 x 轴上显示初始容量,在 y 轴上显示负载系数。

事不宜迟,让我们来看看结果。我将从 puts 的结果开始。

放置结果


集合大小:100。哈希限制:50。这意味着每个哈希码应该出现两次,并且在哈希映射中每隔一个键就会发生冲突。

嗯,这并不是一开始就很好。我们看到有一个大的热点,初始容量比集合大小高 25%,负载因子为 1。左下角的性能不太好。


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

这是一个稍微现实一点的场景,没有完美的哈希函数,但仍然有 10% 的过载。热点没有了,但低初始容量和低负载率的组合显然行不通。


集合大小:100。哈希限制:100。每个键作为自己唯一的哈希码。如果有足够的桶,则不会发生冲突。

负载因子为 1 的初始容量为 100 似乎很好。令人惊讶的是,较高的初始容量和较低的负载率并不一定是好的。


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

左下角还是不行。但在较低初始计数/高负载因子和较高初始计数/低负载因子的组合之间似乎存在对称性。


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

初始容量太低且负载因子高于 1 的不太可能的组合发生了一些非常有趣的事情,这是相当违反直觉的。否则,还是挺对称的。


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

我们在这里有一个很好的对称性。左下角仍然不是最理想的,但 1000 初始容量/1.0 负载系数与 1250 初始容量/0.75 负载系数的组合处于同一水平。


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

这里不多说了。较高的初始容量与 0.75 的负载因子的组合似乎略优于 1000 个初始容量与负载因子 1 的组合。


集合大小:100_000。哈希限制:10_000。好吧,现在情况越来越严重了,每个键的样本大小为 10 万和 100 个哈希码重复。

哎呀!我认为我们找到了较低的频谱。加载因子为 1 的集合大小的初始容量在这里做得非常好,但除此之外,它遍布整个商店。


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

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


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

使用确切的集合大小作为加载因子为 1 的初始化容量在这里胜出!不过,稍大一点的初始化容量也能很好地工作。


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

这里有一些令人惊讶的东西。在负载系数为 1 的情况下,具有 50% 额外空间的初始容量获胜。


好的,看跌期权就是这样。现在,我们将检查获取。请记住,以下地图都是相对于最佳/最差获取时间的,不再考虑放置时间。

获得结果


集合大小:100。哈希限制:50。这意味着每个哈希码应该出现两次,并且每个其他键都应该在哈希映射中发生冲突。

呃……什么?


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

哇,耐莉!这是与提问者的问题相关的最可能的情况,显然负载因子为 1 的初始容量为 100 是这里最糟糕的事情之一!我发誓我没有伪造这个。


集合大小:100。哈希限制:100。每个键作为自己唯一的哈希码。预计不会发生冲突。

这看起来更平静一些。总体结果基本相同。


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

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


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

就像这个设置的 put 一样,我们在一个奇怪的地方遇到了异常。


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

到处都有不错的性能,除了高初始容量和低负载率的组合。我希望看跌期权是这样的,因为可能需要两个哈希映射调整大小。但是为什么会这样呢?


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

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


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

它看起来并不漂亮,虽然坏点非常本地化。这里的性能似乎很大程度上取决于设置之间的某种协同作用。


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

差异很大,虽然如果你眯着眼睛可以看到一个指向右上角的箭头。


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

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


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

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


好吧,现在该下结论了……

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

嗯,就是这样。我希望我的代码没有一些可怕的疏忽,使我在这里发布的所有内容都无效。这很有趣,而且我了解到,最终您可能完全依赖 Java 来完成它的工作,而不是期望与微小的优化有很大的不同。这并不是说某些东西不应该避免,而是我们主要讨论的是在 for 循环中构造冗长的字符串,使用错误的数据结构并制作 O(n^3) 算法。

【讨论】:

感谢您的努力,看起来很棒!不要偷懒,我还在我的结果中添加了一些漂亮的图表。我的测试比你的更暴力,但我发现使用更大的地图时差异更明显。有了小地图,无论做什么,都不能错过。由于 JVM 优化和 GC,性能趋于混乱,我有一个理论认为,对于一些较小的数据集,任何强有力的结论都会被这种混乱所吞噬。 关于获取性能的更多评论。看起来很混乱,但我发现它在一个很窄的范围内变化很大,但总的来说,它是恒定的,无聊得要死。我确实偶尔会遇到奇怪的峰值,例如您在 100/90 上所做的。我无法解释它,但实际上它可能并不明显。 G_H,请看看我的回答,我知道这是一个非常古老的线程,但可能你的测试应该考虑到这一点。 嘿,你应该把它作为会议论文发布到 ACM :) 多么努力!【参考方案2】:

这是一个非常棒的线程,除了你缺少一个关键的东西。你说:

奇怪的是,容量、容量+1、容量+2、容量-1 甚至容量-10 都会产生完全相同的结果。我预计至少容量 1 和容量 10 会产生更差的结果。

源代码在内部将初始容量跳转到下一个最高的二次方。这意味着,例如,初始容量 513、600、700、800、900、1000 和 1024 都将使用相同的初始容量 (1024)。不过,这并没有使@G_H 所做的测试无效,人们应该意识到这是在分析他的结果之前完成的。它确实解释了一些测试的奇怪行为。

This is the constructor right for the JDK source:

/**
 * 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 我很想看到你的测试再次运行,根据这些信息选择更合适的数字。例如,如果我有 1200 个元素,我应该使用 1024 映射、2048 映射还是 4096 映射?我不知道原始问题的答案,这就是为什么我从这个线程开始。不过,我知道Guava 将您的expectedSize 乘以1.33 当您执行Maps.newHashMap(int expectedSize) 如果 HashMap 不能将 capacity 的值四舍五入为 2 的幂,则永远不会使用某些存储桶。放置地图数据的桶索引由bucketIndex = hashCode(key) &amp; (capacity-1) 确定。因此,如果 capacity 不是 2 的幂,则 (capacity-1) 的二进制表示中会包含一些零,这意味着 &amp;(二进制与)运算总是会将 hashCode 的某些低位清零.示例:(capacity-1)111110 (62) 而不是 111111 (63)。在这种情况下,只能使用具有偶数索引的存储桶。【参考方案3】:

来自HashMapJavaDoc:

作为一般规则,默认负载因子 (.75) 在时间和空间成本之间提供了良好的折衷。较高的值会减少空间开销,但会增加查找成本(反映在 HashMap 类的大多数操作中,包括 get 和 put)。在设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以尽量减少重新哈希操作的次数。如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。

因此,如果您预计有 100 个条目,那么负载因子可能为 0.75,初始容量上限 (100/0.75) 是最好的。归结为 134。

我不得不承认,我不确定为什么查找成本会因更高的负载率而更高。仅仅因为 HashMap 更“拥挤”并不意味着更多的对象将被放置在同一个桶中,对吧?如果我没记错的话,那只取决于他们的哈希码。因此,假设散列码分布不错,无论负载因子如何,大多数情况不应该仍然是 O(1) 吗?

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

【讨论】:

"它会破坏您的查找和插入性能"。这是过度夸大/完全不正确。 我的测试表明,查找性能不受将负载因子设置为 1 的影响。插入性能实际上有所提高;由于没有调整大小,因此速度更快。因此,您的陈述对于一般情况是正确的(使用 0.75 查找具有少量元素的 HashMap 将比使用 1 更快),但对于我的特定情况是不正确的,因为 HashMap 始终满载其最大容量,永远不会改变。您将初始大小设置得更高的建议很有趣,但与我的情况无关,因为我的表没有增长,因此负载因子仅在调整大小时才重要。【参考方案4】:

只需使用101。我实际上并不确定它是否需要,但它可能不值得费心费力去确定。

...只需添加1


编辑:我的回答有一些理由。

首先,我假设您的HashMap 不会超过100如果是这样,您应该保持负载因子不变。同样,如果您关心性能,保持负载因子不变。如果您关心的是内存,您可以通过设置静态大小来节省一些。如果您在内存中塞满了很多东西,这可能也许值得一试;即,正在存储许多地图,或创建堆空间压力大小的地图。

其次,我选择值 101 是因为它提供了更好的可读性... 100 元素,我将不得不通读 Javadoc 以确保它在精确到达 100 时不会调整大小。当然,我不会在那里找到答案,所以我必须查看源代码。这是不值得的......只要留下它101,每个人都很高兴,没有人在看java.util.HashMap的源代码。万岁。

第三,声称将HashMap 设置为您期望的确切容量以及1 "will kill your lookup and insertion performance" 的负载因子是不正确的,即使它是用粗体表示的。

...如果您有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: ↑ — 关于这个 →||← 不同设置之间有很大差异


关于我的原始答案(第一条水平线上方的位),这是故意油嘴滑舌的,因为在大多数情况下,this type of micro-optimising is not good。

【讨论】:

@EJP,我的猜测不是不正确。请参阅上面的编辑。 你的猜测不正确,谁的猜测正确,谁的猜测不正确。 (......也许我有点刻薄......虽然我有点恼火:P) 您可能对 EJP 感到恼火,但现在轮到我了 ;P - 虽然我同意过早优化很像早泄,但请不要认为通常不值得努力的事情在我的情况下不值得努力。就我而言,这很重要,我不想猜测,所以我查了一下 - 在我的情况下不需要 +1(但可能是您的初始/实际容量不同且 loadFactor 不是 1,在 HashMap 中看到这个转换为 int:threshold = (int)(capacity * loadFactor))。 @badroit 您明确表示我实际上不确定是否需要它。因此,这是猜测。现在您已经完成并发布了研究,这不再是猜测,而且由于您显然没有事先做过,所以很明显 是猜测,否则你会确定的。至于“不正确”,Javadoc 明确规定负载因子为 0.75,几十年的研究和 G_H 的回答也是如此。最后,关于“这不可能值得付出努力”,请参见 Domchi 的评论。没有留下太多正确的地方,尽管总的来说我同意你关于微优化的看法。 大家放松。是的,我的回答夸大了事情。如果你有 100 个没有非常重的 equals 函数的对象,你可能会侥幸把它们放在一个列表中并只使用“包含”。有了这么小的一套,性能上永远不会有太大的差异。只有当速度或内存问题高于一切,或者等于和哈希非常具体时,它才真正重要。稍后我将使用大型集合和各种负载因子和初始容量进行测试,看看我是否充满了垃圾。【参考方案5】:

在实现方面,Google Guava 有一个方便的工厂方法

Maps.newHashMapWithExpectedSize(expectedSize)

其中calculates the capacity使用公式

capacity = expectedSize / 0.75F + 1.0F

【讨论】:

以上是关于固定大小的 HashMap 的最佳容量和负载因子是多少?的主要内容,如果未能解决你的问题,请参考以下文章

在元素的装载数量明确的时候HashMap的大小应该如何选择。

Java中HashMap的初始容量设置

HashMap系列之重要方法源码详解

hashMap记录

HashMap初始容量如何设置

HashMap中加载因子的意义是什么?