指定精确容量时,为什么HashMap再次resize()?

时间:2018-10-05 18:25:59

标签: java optimization data-structures hashmap cpu

代码所讲的不只是文字,所以:

final int size = 100;
Map<Integer, String> m = new HashMap<>(size);
for (int i = 0; i < size; i++) m.put(i, String.valueOf(i));

为什么HashMap在内部调用resize() 21 2次!(感谢Andreas确认JVM使用在内部的HashMaps中,21个校准中的19个来自其他进程)

我的应用程序仍然不能接受两个resize()调用。我需要对此进行优化。

如果我是一名新的Java开发人员,那么我对HashMap构造函数中“容量”的含义的第一个直观猜测就是它是我(HashMap的使用者)要放入其中的元素数量的容量。地图。但这不是真的。

如果我想优化HashMap的用法,从而根本不需要调整自身大小,那么我需要足够了解HashMap的内部知识,以确切了解HashMap存储桶数组需要多么稀疏。我认为这很奇怪。 HashMap应该隐式为您执行此操作。这是OOP封装的全部要点。

注意:我已经确认resize()是我的应用程序用例的瓶颈,所以这就是为什么我的目标是减少对resize()的调用次数。

问题:

如果我知道确切的条目数量,我将事先输入地图。我选择了什么容量来阻止 任何 额外的呼叫resize()操作?像size * 10一样?我还想了解为何HashMap这样设计的背景。

编辑:我经常被问到为什么这种优化是必要的。我的应用程序在hashmap.resize()中花费了大量的CPU时间。我的应用程序使用的哈希图的初始化容量等于我们放入其中的元素数量。因此,如果我们可以减少resize()调用(通过选择更好的初始容量),那么我的应用程序性能将得到改善。

5 个答案:

答案 0 :(得分:10)

默认的加载因子为3/4,即resize(),这意味着在添加100个值中的75个后,内部哈希表将被调整大小。

仅供参考: size <= capacity * 0.75仅被调用两次。一次添加第一个值,一次填充到75%的水平。

为防止调整大小,您需要确保第100个值不会引起调整大小,即size <= capacity * 3/4 aka size * 4/3 <= capacity aka capacity = size * 4/3 + 1 ,因此请确保:

size = 100

对于capacity = 134,表示{{1}}。

答案 1 :(得分:6)

如有疑问,请阅读文档。 HashMap的文档很好地解释了initial capacityload-factor的取舍。

根据文档,如果initCapacity = (maxEntries / loadFactor) + 1,则在添加条目时将不会进行任何哈希操作。在这种情况下,maxEntries是您指定的100,而loadFactor将是默认的负载因子.75

但是除了设置初始大小以避免重新散列(resize())之外,您还应仔细阅读HashMap的文档以对其进行适当调整,同时考虑初始容量和负载因子

如果您关心的是查找花费多于空间花费,那么可以尝试使用loadFactor之类的较低.5或更低的final float loadFactor = 0.5; final int maxEntries = 100; final int initCapacity = (int) maxEntries / loadFactor + 1; new HashMap<>(initCapacity, loadFactor); 。在这种情况下,您将使用以下两个参数创建哈希映射:

resources

(重点是我的)

  

HashMap的实例具有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中存储桶的数量,初始容量只是创建哈希表时的容量。负载因子是在自动增加其哈希表容量之前允许哈希表获得的满度的度量。当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将被重新哈希(即,内部数据结构将被重建),因此哈希表的存储桶数大约为两倍。 />   ...
  通常,默认负载因子(.75)在时间和空间成本之间提供了很好的折衷方案。较高的值会减少空间开销,但会增加查找成本(在HashMap类的大多数操作中都得到体现,包括get和put)。设置映射表的初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度地减少重新哈希操作的数量。 如果初始容量大于最大条目数除以负载系数,将不会进行任何哈希操作。

答案 2 :(得分:1)

这很容易证明:

private static <K, V> void debugResize(Map<K, V> map, K key, V value) throws Throwable {

    Field table = map.getClass().getDeclaredField("table");
    AccessibleObject.setAccessible(new Field[] { table }, true);
    Object[] nodes = ((Object[]) table.get(map));

    // first put
    if (nodes == null) {
        map.put(key, value);
        return;
    }

    map.put(key, value);

    Field field = map.getClass().getDeclaredField("table");
    AccessibleObject.setAccessible(new Field[] { field }, true);
    int x = ((Object[]) field.get(map)).length;
    if (nodes.length != x) {
        ++currentResizeCalls;
    }
}

以及一些用法:

static int currentResizeCalls = 0;

public static void main(String[] args) throws Throwable {

    int size = 100;
    Map<Integer, String> m = new HashMap<>(size);
    for (int i = 0; i < size; i++) {
        DeleteMe.debugResize(m, i, String.valueOf(i));
    }

    System.out.println(DeleteMe.currentResizeCalls);
}     

我只记录resize实际上 调整大小所需的时间,因为第一个调用正在初始化。根据文档规定:

  

初始化或加倍表大小


您的第二点要有趣得多。一个HashMap定义了capacity,现在的容量是多少?这不是很明显:

对于HashMapcapacity是调整大小之前的buckets数,对于ConcurrentHashMap,这是执行调整大小之前的条目数。

因此,在HashMap的情况下,请勿在内部调用调整大小的命令:

(int)(1.0 + (long)initialCapacity / LOAD_FACTOR)

但这远非理想,例如,您想要1024个条目而无需调整大小,通过使用该公式,您可以获得1367个存储桶,这些存储桶在内部四舍五入为2的幂,因此{ {1}}-远远超出您的要求。

对于2048,直接指定尺寸 。使用上一个代码中的一个修改即可轻松证明:

CHM

这将导致 // use CHM instead of HashMap Map<Integer, String> m = new ConcurrentHashMap<>(size); 调整大小,实际上将数组加倍。但是有时甚至zero内部代码也是confusing,几乎不需要修补。

答案 3 :(得分:1)

这里有很多很棒的答案。我非常感谢您的贡献。

我已决定不重新发明该滚轮,因为Google似乎已经解决了该问题。

我将使用Google's guava library中的实用方法Maps.newHashMapWithExpectedSize(int)

答案 4 :(得分:0)

  • 调整大小是哈希图保持较低负载因子的重要组成部分。

  • 负载因数必须较低,因为当hashmap的存储桶达到最大时,hashmap的哈希函数将不可避免地开始发生冲突。如果您的条目每次都哈希到占用的存储桶,则冲突可能从第二个条目本身开始。


但是,在您的特定情况下,冲突不是问题,只有hashmap的大小可以调整。

哈希图通常调整为0.75(分数等于3/4)的加载因子。使用此信息,您可以将哈希映射设置为需要存储的条目计数的4/3倍。


关于破坏封装的异议:

我同意你的观点这值得商

您可以说,如果capacity表示直到调整大小才发生的条目数,而不是可以存储在哈希图中的最大可能条目数,那么我会更好-同意你的看法。

但是其他人也可以在另一端争论,为什么哈希图占用的空间比其指示的要多。

此问题的解决方案在于Java的领域。 Java可以提供两个构造函数,这些构造函数对它们将要执行的工作是足够明确的,然后开发人员可以在哈希映射的初始化期间进行选择。