Java HashMap 性能优化/替代

Posted

技术标签:

【中文标题】Java HashMap 性能优化/替代【英文标题】:Java HashMap performance optimization / alternative 【发布时间】:2010-12-17 23:02:39 【问题描述】:

我想创建一个大的 HashMap 但put() 的性能不够好。有什么想法吗?

欢迎其他数据结构建议,但我需要 Java Map 的查找功能:

map.get(key)

就我而言,我想创建一个包含 2600 万个条目的地图。使用标准的 Java HashMap 插入 2-3 百万次后,put 速率变得非常慢。

另外,有谁知道对密钥使用不同的哈希码分布是否有帮助?

我的哈希码方法:

byte[] a = new byte[2];
byte[] b = new byte[3];
...

public int hashCode() 
    int hash = 503;
    hash = hash * 5381 + (a[0] + a[1]);
    hash = hash * 5381 + (b[0] + b[1] + b[2]);
    return hash;

我使用加法的关联属性来确保相等的对象具有相同的哈希码。数组是值在 0 - 51 范围内的字节。值在任一数组中仅使用一次。如果 a 数组包含相同的值(以任意顺序),则对象相等,b 数组也是如此。所以 a = 0,1 b = 45,12,33 和 a = 1,0 b = 33,45,12 是相等的。

编辑,一些注释:

有些人批评使用哈希映射或其他数据结构来存储 2600 万个条目。我不明白为什么这看起来很奇怪。对我来说,它看起来像是一个经典的数据结构和算法问题。我有 2600 万个项目,我希望能够将它们快速插入并从数据结构中查找:给我数据结构和算法。

将默认 Java HashMap 的初始容量设置为 2600 万会降低性能。

有些人建议使用数据库,在其他一些情况下,这绝对是明智的选择。但我真的在问一个数据结构和算法的问题,一个完整的数据库会比一个好的数据结构解决方案大用而且慢得多(毕竟数据库只是软件,但会有通信和可能的磁盘开销)。

李>

【问题讨论】:

如果 HashMap 变慢,很有可能你的哈希函数不够好。 医生,我这样做的时候很痛这个 这是一个非常好的问题;很好地演示了为什么散列算法很重要以及它们可能对性能产生什么影响 a 的总和的范围为 0 到 102,b 的总和的范围为 0 到 153,因此您只有 15,606 个可能的哈希值和平均 1,666 个具有相同 hashCode 的键。您应该更改您的哈希码,以便可能的哈希码数量远大于键的数量。 我已经从心理上确定你正在模拟德州扑克;-) 【参考方案1】:

就我而言,我想创建一个包含 2600 万个条目的地图。使用标准的 Java HashMap 插入 2-3 百万次后,put 速率变得非常慢。

来自我的实验(2009 年的学生项目):

我为从 1 到 100.000 的 100.000 个节点构建了一个红黑树。耗时 785.68 秒(13 分钟)。而且我未能为 100 万个节点构建 RBTree(就像您使用 HashMap 的结果一样)。 使用“Prime Tree”,我的算法数据结构。我可以在 21.29 秒(RAM:1.97Gb)内为 1000 万个节点构建一个树/映射。搜索键值成本为 O(1)。

注意:“Prime Tree”最适用于 1 到 1000 万的“连续键”。要使用像 HashMap 这样的键,我们需要进行一些次要调整。


那么,#PrimeTree 是什么?简而言之,它是一种类似于二叉树的树型数据结构,分支编号是素数(而不是“2”-二进制)。

【讨论】:

你能分享一些链接或实现吗?【参考方案2】:

我在您的hashCode() 方法中注意到的一件事是数组a[]b[] 中元素的顺序无关紧要。因此(a[]=1,2,3, b[]=99,100) 将散列到与(a[]=3,1,2, b[]=100,99) 相同的值。实际上所有键 k1k2 其中sum(k1.a)==sum(k2.a)sum(k1.b)=sum(k2.b) 都会导致冲突。我建议为数组的每个位置分配一个权重:

hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);

其中c0c1c3不同 常量(如果需要,您可以为b 使用不同的常量)。这应该会使事情变得更加平衡。

【讨论】:

虽然我还应该补充一点,它对我不起作用,因为我希望具有不同顺序的相同元素的数组的属性给出相同的哈希码。 在这种情况下,您有 52C2 + 52C3 个哈希码(根据我的计算器,哈希码是 23426),而哈希图对于这项工作来说是非常错误的工具。 实际上这会提高性能。冲突越多,哈希表 eq 中的条目越少。更少的工作要做。不是哈希(看起来不错)也不是哈希表(效果很好)我敢打赌它是在性能下降的对象创建上。 @Oscar - 更多的冲突等于 more 工作要做,因为现在你必须对哈希链进行线性搜索。如果每个 equals() 有 26,000,000 个不同的值,每个 hashCode() 有 26,000 个不同的值,那么每个桶链将有 1,000 个对象。 @Nash0:您似乎在说您希望它们具有相同的 hashCode 但同时不相等(由 equals() 方法定义)。你为什么想要那个?【参考方案3】:

正如许多人指出的那样,hashCode() 方法是罪魁祸首。它只为 2600 万个不同的对象生成了大约 20,000 个代码。也就是说,每个哈希桶平均有 1,300 个对象 = 非常非常糟糕。但是,如果我将这两个数组转换为以 52 为基数的数字,则可以保证为每个对象获得一个唯一的哈希码:

public int hashCode()        
    // assume that both a and b are sorted       
    return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);


public static int powerOf52(byte b, int power) 
    int result = b;
    for (int i = 0; i < power; i++) 
        result *= 52;
    
    return result;

对数组进行排序以确保此方法满足hashCode() 合约,即相等的对象具有相同的哈希码。使用旧方法,在 100,000 个 put(100,000 到 2,000,000 个)的块上,每秒平均 put 数是:

168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083

使用新方法给出:

337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25

好多了。旧方法很快就结束了,而新方法保持了良好的吞吐量。

【讨论】:

我建议不要修改hashCode方法中的数组。按照惯例,hashCode 不会更改对象的状态。也许构造函数是对它们进行排序的更好地方。 我同意数组的排序应该在构造函数中进行。显示的代码似乎从未设置 hashCode。计算代码可以更简单如下:int result = a[0]; result = result * 52 + a[1]; //etc. 请注意,您还可以缓存哈希码(如果您的对象是可变的,则适当地使其无效)。 只需使用java.util.Arrays.hashCode()。它更简单(无需自己编写和维护代码),它的计算可能更快(乘法更少),其哈希码的分布可能会更均匀。 @JavaMan 它们代表卡片,所以只有 52 个选项,通过使用 52 的幂,我保证每个组合都会获得唯一的哈希码。【参考方案4】:

详细说明 Pascal:您了解 HashMap 的工作原理吗?您的哈希表中有一些插槽。找到每个键的哈希值,然后映射到表中的条目。如果两个哈希值映射到同一个条目——“哈希冲突”——HashMap 构建一个链表。

哈希冲突会影响哈希映射的性能。在极端情况下,如果你所有的键都有相同的哈希码,或者它们有不同的哈希码但它们都映射到同一个槽,那么你的哈希映射就会变成一个链表。

因此,如果您发现性能问题,我首先要检查的是:我是否得到了看起来随机分布的哈希码?如果没有,您需要一个更好的哈希函数。好吧,在这种情况下,“更好”可能意味着“对我的特定数据集更好”。就像,假设您正在使用字符串,并且您将字符串的长度作为哈希值。 (不是 Java 的 String.hashCode 是如何工作的,但我只是在编一个简单的例子。)如果您的字符串长度变化很大,从 1 到 10,000,并且在该范围内分布相当均匀,那么这可能是一个非常好的哈希函数。但如果你的字符串都是 1 或 2 个字符,这将是一个非常糟糕的哈希函数。

编辑:我应该添加:每次添加新条目时,HashMap 都会检查这是否是重复的。当发生哈希冲突时,它必须将传入的密钥与映射到该插槽的每个密钥进行比较。因此,在最坏的情况下,所有内容都散列到单个插槽中,第二个键与第一个键进行比较,第三个键与#1 和#2 进行比较,第四个键与#1、#2 和#3 进行比较等。当您获得关键的 #100 万时,您已经完成了超过一万亿次比较。

@Oscar:嗯,我不明白那是“不是真的”。这更像是“让我澄清一下”。但是,是的,如果您使用与现有条目相同的键创建一个新条目,这确实会覆盖第一个条目。这就是我在上一段中谈到寻找重复项时的意思:每当一个键散列到同一个插槽时,HashMap 必须检查它是否是现有键的副本,或者它们是否恰好在同一个插槽中哈希函数。我不知道这是 HashMap 的“重点”:我会说“重点”是您可以通过键快速检索元素。

但无论如何,这不会影响我试图提出的“重点”:当您有两个键时——是的,不同的键,不再出现相同的键——映射到同一个插槽在表中,HashMap 构建了一个链表。然后,因为它必须检查每个新键以查看它是否实际上是现有键的副本,所以每次尝试添加映射到同一插槽的新条目都必须跟踪检查每个现有条目的链表以查看是否是以前看到的密钥的副本,或者如果它是新密钥。

在原帖很久之后更新

在发布 6 年后,我刚刚对这个答案投了赞成票,这让我重新阅读了这个问题。

问题中给出的散列函数对于 2600 万个条目来说不是一个好的散列。

它将a[0]+a[1]和b[0]+b[1]+b[2]相加。他说每个字节的值范围从 0 到 51,因此仅给出 (51*2+1)*(51*3+1)=15,862 个可能的哈希值。有 2600 万个条目,这意味着每个哈希值平均大约有 1639 个条目。那是大量的冲突,需要通过链表进行大量的顺序搜索。

OP 说数组 a 和数组 b 内的不同顺序应该被认为是相等的,即 [[1,2],[3,4,5]].equals([[2,1],[5,3 ,4]]),因此要履行合同,它们必须具有相等的哈希码。好的。尽管如此,仍有超过 15,000 个可能的值。他提出的第二个哈希函数要好得多,范围更广。

尽管正如其他人评论的那样,哈希函数似乎不适合更改其他数据。在创建对象时“规范化”对象或让哈希函数从数组的副本中工作会更有意义。此外,每次通过函数都使用循环来计算常数是低效的。由于这里只有四个值,我会写成

return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;

这将导致编译器在编译时执行一次计算;或在类中定义 4 个静态常量。

此外,哈希函数的初稿有几个计算,这些计算对增加输出范围没有任何作用。请注意,他首先设置 hash =503,然后再乘以 5381,然后才考虑类中的值。所以......实际上他将 503*5381 添加到每个值。这有什么作用?为每个哈希值添加一个常量只会消耗 CPU 周期,而不会完成任何有用的操作。这里的教训:增加哈希函数的复杂性不是目标。目标是获得广泛的不同值,而不仅仅是为了复杂性而增加复杂性。

【讨论】:

是的,一个糟糕的哈希函数会导致这种行为。 +1 并非如此。 在哈希相同但键不同时创建列表。例如,如果 String 给出哈希码 2345,而 Integer 给出相同的哈希码 2345,则整数被插入到列表中,因为 String.equals( Integer )false但是如果你有相同的类(或至少 .equals 返回 true ),那么使用相同的条目。例如 new String("one") 和 `new String("one") 用作键,将使用相同的条目。实际上,这首先是 HashMap 的 WHOLE 点!自己看:pastebin.com/f20af40b9 @Oscar:请参阅我的回复附加到我的原始帖子。 我知道这是一个非常古老的线程,但这里有一个与哈希码相关的术语“碰撞”的参考:link。当您通过放置另一个具有相同键的值来替换哈希图中的值时,它称为冲突 @Tahir 没错。也许我的帖子措辞不佳。感谢您的澄清。【参考方案5】:

使用的流行散列方法对于大型集合并不是很好,而且如上所述,使用的散列特别糟糕。更好的是使用具有高混合和覆盖率的哈希算法,例如 BuzHash(示例实现在http://www.java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htm)

【讨论】:

【参考方案6】:

在Effective Java: Programming Language Guide (Java Series)

第 3 章你可以找到计算 hashCode() 时要遵循的好规则。

特别是:

如果该字段是一个数组,则将其视为每个元素都是一个单独的字段。 也就是说,通过应用计算每个重要元素的哈希码 这些规则递归,并在每个步骤 2.b 中组合这些值。如果每个 数组字段中的元素很重要,您可以使用其中一种 1.5 版中添加了 Arrays.hashCode 方法。

【讨论】:

【参考方案7】:

进入“开/关主题”的灰色区域,但有必要消除关于 Oscar Reyes 建议的混淆,即更多的哈希冲突是一件好事,因为它减少了 HashMap 中的元素数量。我可能误解了 Oscar 在说什么,但我似乎并不是唯一一个:kdgregory、delfuego、Nash0,而且我似乎都有相同的(错误)理解。

如果我理解 Oscar 对具有相同哈希码的同一类的说法,他建议只有具有给定哈希码的类的一个实例将插入 HashMap。例如,如果我有一个哈希码为 1 的 SomeClass 实例和另一个哈希码为 1 的 SomeClass 实例,则只插入一个 SomeClass 实例。

http://pastebin.com/f20af40b9 的 Java pastebin 示例似乎表明上述内容正确地总结了 Oscar 的提议。

不管有任何理解或误解,如果同一类的不同实例具有相同的哈希码,只会将它们插入 HashMap 一次 - 直到确定键是否为相等或不相等。哈希码合约要求相等的对象具有相同的哈希码;但是,它并不要求不相等的对象具有不同的哈希码(尽管出于其他原因这可能是可取的)[1]。

以下是 pastebin.com/f20af40b9 示例(Oscar 至少提到了两次),但稍作修改以使用 JUnit 断言而不是 printlines。此示例用于支持相同哈希码导致冲突的提议,并且当类相同时仅创建一个条目(例如,在此特定情况下仅创建一个字符串):

@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() 
    String s = new String("ese");
    String ese = new String("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // AND equal
    assertTrue(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(2, map.size());

    assertEquals(2, map.get("ese"));
    assertEquals(3, map.get(some));

    assertTrue(s.equals(ese) && s.equals("ese"));


class SomeClass 
    public int hashCode() 
        return 100727;
    

但是,哈希码并不是完整的故事。 pastebin 示例忽略了sese 相等的事实:它们都是字符串“ese”。因此,使用sese"ese" 作为键插入或获取映射的内容都是等价的,因为s.equals(ese) &amp;&amp; s.equals("ese")

第二个测试表明,当在测试一个中调用map.put(ese, 2) 时,得出相同类上的相同哈希码是键-> 值s -&gt; 1ese -&gt; 2 覆盖的结论是错误的。在测试二中,sese 仍然具有相同的哈希码(由assertEquals(s.hashCode(), ese.hashCode()); 验证)并且它们是同一个类。但是,sese 在此测试中是 MyString 实例,而不是 Java String 实例 - 与此测试相关的唯一区别是等于:String s equals String ese 在上面的测试中,而 MyStrings s does not equal MyString ese 在测试二:

@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() 
    MyString s = new MyString("ese");
    MyString ese = new MyString("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // BUT not equal
    assertFalse(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(3, map.size());

    assertEquals(1, map.get(s));
    assertEquals(2, map.get(ese));
    assertEquals(3, map.get(some));


/**
 * NOTE: equals is not overridden so the default implementation is used
 * which means objects are only equal if they're the same instance, whereas
 * the actual Java String class compares the value of its contents.
 */
class MyString 
    String i;

    MyString(String i) 
        this.i = i;
    

    @Override
    public int hashCode() 
        return 100727;
    

根据后来的评论,奥斯卡似乎颠倒了他之前所说的并承认平等的重要性。但是,似乎重要的是等于,而不是“同一类”的概念仍然不清楚(强调我的):

"不是真的。仅当哈希相同但键不同时才会创建列表。例如,如果字符串给出哈希码 2345,而整数给出相同的哈希码 2345,则插入整数进入列表,因为 String.equals( Integer ) 为 false。但是 如果您有相同的类(或至少 .equals 返回 true ),则使用相同的条目。例如 new String("one ") 和 `new String("one") 用作键,将使用相同的条目。实际上这是 HashMap 的全部要点!您自己看看:pastebin.com/f20af40b9 – Oscar Reyes"

与较早的 cmet 相比,它们明确解决了相同类和相同哈希码的重要性,但没有提及 equals:

"@delfuego:自己看看:pastebin.com/f20af40b9 所以,在这个问题中,使用的是同一个类(等一下,使用的是同一个类对吗?)这意味着当相同哈希使用相同的条目,并且没有条目的“列表”。- Oscar Reyes"

“实际上这会提高性能。冲突越多,哈希表 eq 中的条目越少。要做的工作就越少。不是哈希(看起来不错)也不是哈希表(效果很好)我会打赌它是在性能下降的对象创建上。– Oscar Reyes"

“@kdgregory:是的,但只有当碰撞发生在不同的类时,对于同一个类(就是这种情况),使用相同的条目。- Oscar Reyes”

再一次,我可能误解了奥斯卡实际上想说什么。然而,他最初的 cmets 已经引起了足够多的混乱,似乎谨慎的做法是通过一些明确的测试来澄清一切,这样就没有挥之不去的疑问。


[1] - 来自 Joshua Bloch 的 Effective Java, Second Edition

在应用程序执行期间,只要在同一个对象上多次调用它,hashCode 方法必须始终返回 相同的整数,提供没有在相等的比较中使用的信息 对象被修改。该整数不需要在应用程序的一次执行与同一应用程序的另一次执行之间保持一致。

如果两个对象根据 equal s(Obj ect) 方法相等,则对两个对象中的每一个调用 hashCode 方法必须产生相同 整数结果。

不要求如果两个对象根据equal s(Object)方法不相等,则对两个对象分别调用hashCode方法 必须产生不同的整数结果。但是,程序员应该 意识到为不相等的对象产生不同的整数结果可能会提高 哈希表的性能。

【讨论】:

【参考方案8】:

不久前我用列表和哈希图做了一个小测试,有趣的是遍历列表并找到对象所花费的时间与使用哈希图 get 函数的毫秒数相同……仅供参考。哦,是的,在使用这种大小的哈希图时,内存是一个大问题。

【讨论】:

【参考方案9】:

如果需要同步,可以尝试使用

http://commons.apache.org/collections/api/org/apache/commons/collections/FastHashMap.html

【讨论】:

【参考方案10】:

正如所指出的,您的哈希码实现有太多冲突,修复它应该会带来不错的性能。此外,缓存 hashCodes 和有效地实现 equals 会有所帮助。

如果您需要进一步优化:

根据您的描述,只有 (52 * 51 / 2) * (52 * 51 * 50 / 6) = 29304600 个不同的键(其中 26000000 个,即大约 90% 将存在)。因此,您可以设计一个没有任何冲突的哈希函数,并使用简单的数组而不是哈希图来保存您的数据,从而减少内存消耗并提高查找速度:

T[] array = new T[Key.maxHashCode];

void put(Key k, T value) 
    array[k.hashCode()] = value;

T get(Key k) 
    return array[k.hashCode()];

(一般情况下,不可能设计出一个高效、无冲突且能很好聚类的哈希函数,这就是为什么 HashMap 会容忍冲突,这会产生一些开销)

假设ab 已排序,您可以使用以下哈希函数:

public int hashCode() 
    assert a[0] < a[1]; 
    int ahash = a[1] * a[1] / 2 
              + a[0];

    assert b[0] < b[1] && b[1] < b[2];

    int bhash = b[2] * b[2] * b[2] / 6
              + b[1] * b[1] / 2
              + b[0];
    return bhash * 52 * 52 / 2 + ahash;


static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;  

我认为这是无冲突的。证明这一点留给有数学倾向的读者作为练习。

【讨论】:

【参考方案11】:

我来晚了,但是有几个关于大地图的问题:

    正如其他帖子中详细讨论的那样,使用良好的 hashCode(),地图中的 26M 条目没什么大不了的。 但是,这里可能隐藏的问题是巨型地图的 GC 影响。

我假设这些地图是长期存在的。即您填充它们并且它们在应用程序的持续时间内一直存在。我还假设应用程序本身是长期存在的——就像某种服务器一样。

Java HashMap 中的每个条目都需要三个对象:键、值和将它们联系在一起的条目。所以映射中的 26M 条目意味着 26M * 3 == 78M 对象。在你达到完整的 GC 之前,这很好。那么你就有了一个暂停世界的问题。 GC 将查看 78M 对象中的每一个并确定它们都处于活动状态。 7800 万多个对象只是很多要查看的对象。如果您的应用程序可以容忍偶尔的长时间(可能是几秒钟)暂停,那么就没有问题。如果您试图实现任何延迟保证,您可能会遇到一个主要问题(当然,如果您想要延迟保证,Java 不是可以选择的平台 :))如果您的地图中的值快速流失,您最终可能会出现频繁的完整收集这使问题更加复杂。

我不知道解决这个问题的好方法。想法:

有时可以调整 GC 和堆大小以“主要”防止完全 GC。 如果您的地图内容流失很多,您可以尝试Javolution's FastMap -- 它可以合并 Entry 对象,这可以降低完全收集的频率 您可以创建自己的 map impl 并对 byte[] 进行显式内存管理(即,通过将数百万个对象序列化为单个 byte[] 来换取 cpu 以获得更可预测的延迟——呃!) 不要在这部分使用 Java - 通过套接字与某种可预测的内存中 DB 对话 希望新的G1收集器能有所帮助(主要适用于高流失率的情况)

只是一些花费大量时间在 Java 中使用巨型地图的人的一些想法。


【讨论】:

【参考方案12】:

如果您提到的两个字节数组是您的整个键,值在 0-51 范围内,唯一且 a 和 b 数组中的顺序无关紧要,我的数学告诉我只有大约 2600 万可能的排列,并且您可能正在尝试使用所有可能键的值填充映射。

在这种情况下,如果您使用数组而不是 HashMap 并将其从 0 到 25989599 索引,那么从数据存储中填充和检索值当然会快得多。

【讨论】:

这是一个非常好的主意,事实上我正在为另一个具有 12 亿个元素的数据存储问题这样做。在这种情况下,我想采取简单的方法并使用预制数据结构:)【参考方案13】:

你可以尝试两件事:

让您的 hashCode 方法返回更简单、更有效的内容,例如连续的 int

将您的地图初始化为:

Map map = new HashMap( 30000000, .95f );

这两个动作将极大地减少结构的重新散列量,并且我认为很容易测试。

如果这不起作用,请考虑使用其他存储,例如 RDBMS。

编辑

奇怪的是,设置初始容量会降低您的情况下的性能。

见javadocs:

如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。

我做了一个微海滩标记(无论如何都不是确定的,但至少证明了这一点)

$cat Huge*java
import java.util.*;
public class Huge 
    public static void main( String [] args ) 
        Map map = new HashMap( 30000000 , 0.95f );
        for( int i = 0 ; i < 26000000 ; i ++ )  
            map.put( i, i );
        
    

import java.util.*;
public class Huge2 
    public static void main( String [] args ) 
        Map map = new HashMap();
        for( int i = 0 ; i < 26000000 ; i ++ )  
            map.put( i, i );
        
    

$time java -Xms2g -Xmx2g Huge

real    0m16.207s
user    0m14.761s
sys 0m1.377s
$time java -Xms2g -Xmx2g Huge2

real    0m21.781s
user    0m20.045s
sys 0m1.656s
$

因此,由于重新分配,使用初始容量从 21 秒下降到 16 秒。这给我们留下了您的hashCode 方法作为“机会领域”;)

编辑

不是HashMap

根据您的上一版本。

我认为你应该真正分析你的应用程序,看看它在哪里消耗了内存/cpu。

我创建了一个类来实现你的 hashCode

该哈希码会产生数百万次冲突,然后 HashMap 中的条目会大大减少。

我从之前的测试中的 21 秒、16 秒通过到 10 秒和 8 秒。原因是因为 hashCode 会引发大量冲突,而您并没有存储您认为的 26M 对象,而是存储的数量要少得多(我会说大约 20k)所以:

问题不是哈希图在您的代码中的其他地方。

现在是时候获取分析器并找出位置了。我认为这是在创建项目,或者您可能正在写入磁盘或从网络接收数据。

这是我对你的类的实现。

注意我没有像你那样使用 0-51 范围,而是 -126 到 127 作为我的值并承认重复,那是因为我在你更新你的问题之前做了这个测试

唯一的区别是你的班级会有更多的碰撞,因此地图中存储的项目更少。

import java.util.*;
public class Item 

    private static byte w = Byte.MIN_VALUE;
    private static byte x = Byte.MIN_VALUE;
    private static byte y = Byte.MIN_VALUE;
    private static byte z = Byte.MIN_VALUE;

    // Just to avoid typing :) 
    private static final byte M = Byte.MAX_VALUE;
    private static final byte m = Byte.MIN_VALUE;


    private byte [] a = new byte[2];
    private byte [] b = new byte[3];

    public Item () 
        // make a different value for the bytes
        increment();
        a[0] = z;        a[1] = y;    
        b[0] = x;        b[1] = w;   b[2] = z;
    

    private static void increment() 
        z++;
        if( z == M ) 
            z = m;
            y++;
        
        if( y == M ) 
            y = m;
            x++;
        
        if( x == M ) 
            x = m;
            w++;
        
    
    public String toString() 
        return "" + this.hashCode();
    



    public int hashCode() 
        int hash = 503;
        hash = hash * 5381 + (a[0] + a[1]);
        hash = hash * 5381 + (b[0] + b[1] + b[2]);
        return hash;
    
    // I don't realy care about this right now. 
    public boolean equals( Object other ) 
        return this.hashCode() == other.hashCode();
    

    // print how many collisions do we have in 26M items.
    public static void main( String [] args ) 
        Set set = new HashSet();
        int collisions = 0;
        for ( int i = 0 ; i < 26000000 ; i++ ) 
            if( ! set.add( new Item() ) ) 
                collisions++;
            
        
        System.out.println( collisions );
    

使用这个类有上一个程序的Key

 map.put( new Item() , i );

给我:

real     0m11.188s
user     0m10.784s
sys 0m0.261s


real     0m9.348s
user     0m9.071s
sys  0m0.161s

【讨论】:

奥斯卡,正如上面其他地方所指出的(回应您的 cmets),您似乎认为更多的碰撞是好的;这非常不好。冲突意味着给定哈希的槽从包含单个条目变为包含条目列表,并且每次访问该槽时都必须搜索/遍历此列表。 @delfuego:不是真的,只有当你使用不同的类发生碰撞但对于同一个类使用相同的条目时才会发生这种情况;) @Oscar - 查看我对 MAK 的回答。 HashMap 在每个哈希桶中维护一个条目的链接列表,并遍历该列表,在每个元素上调用 equals()。对象的类与它无关(除了 equals() 上的短路)。 @Oscar - 阅读您的答案,您似乎假设如果哈希码相同,equals() 将返回 true。这不是 equals/hashcode 合约的一部分。如果我误解了,请忽略此评论。 非常感谢 Oscar 所做的努力,但我认为您混淆了关键对象是否相等与具有相同的哈希码。同样在您的代码链接之一中,您使用等号字符串作为键,请记住 Java 中的字符串是不可变的。我想我们今天都学到了很多关于散列的知识:)【参考方案14】:

另一位发帖人已经指出,由于您将值相加的方式,您的哈希码实现会导致很多冲突。我愿意这样,如果您在调试器中查看 HashMap 对象,您会发现您可能有 200 个不同的哈希值,并且存储桶链非常长。

如果您的值始终在 0..51 范围内,则这些值中的每一个都需要 6 位来表示。如果你总是有 5 个值,你可以创建一个带有左移和加法的 30 位哈希码:

    int code = a[0];
    code = (code << 6) + a[1];
    code = (code << 6) + b[0];
    code = (code << 6) + b[1];
    code = (code << 6) + b[2];
    return code;

左移速度很快,但会留下不均匀分布的哈希码(因为 6 位意味着范围 0..63)。另一种方法是将哈希乘以 51 并添加每个值。这仍然不会完美分布(例如,2,0 和 1,52 会发生冲突),并且会比 shift 慢。

    int code = a[0];
    code *= 51 + a[1];
    code *= 51 + b[0];
    code *= 51 + b[1];
    code *= 51 + b[2];
    return code;

【讨论】:

@kdgregory:我已经在其他地方回答了“更多的冲突意味着更多的工作”:)【参考方案15】:

您可以尝试将计算的哈希码缓存到密钥对象。

类似这样的:

public int hashCode() 
  if(this.hashCode == null) 
     this.hashCode = computeHashCode();
  
  return this.hashCode;


private int computeHashCode() 
   int hash = 503;
   hash = hash * 5381 + (a[0] + a[1]);
   hash = hash * 5381 + (b[0] + b[1] + b[2]);
   return hash;

当然你要注意在第一次计算hashCode之后不要改变key的内容。

编辑:当您将每个键仅添加一次到地图时,缓存具有代码值似乎是不值得的。在其他一些情况下,这可能很有用。

【讨论】:

正如下面所指出的,当 HashMap 调整大小时,不会重新计算对象的哈希码,因此这不会为您带来任何好处。【参考方案16】:

首先,您应该检查您是否正确使用 Map、键的良好 hashCode() 方法、Map 的初始容量、正确的 Map 实现等,就像许多其他答案所描述的那样。

然后我建议使用分析器来查看实际发生的情况以及执行时间花费在哪里。例如,hashCode() 方法是否执行了数十亿次?

如果这没有帮助,那么使用EHCache 或memcached 之类的东西怎么样?是的,它们是用于缓存的产品,但您可以对其进行配置,以便它们具有足够的容量,并且永远不会从缓存存储中驱逐任何值。

另一个选择是一些比完整的 SQL RDBMS 更轻的数据库引擎。可能是Berkeley DB 之类的东西。

请注意,我个人对这些产品的性能没有经验,但值得一试。

【讨论】:

【参考方案17】:

如果您发布的 hashCode 中的数组是字节,那么您最终可能会得到很多重复。

a[0] + a[1] 将始终介于 0 和 512 之间。 添加 b 将始终导致 0 到 768 之间的数字。将它们相乘,您将获得 400,000 个唯一组合的上限,假设您的数据完美地分布在每个字节的每个可能值之间。如果您的数据完全有规律,那么这种方法的独特输出可能会少得多。

【讨论】:

【参考方案18】:

如果键有任何模式,那么您可以将映射拆分为较小的映射并拥有一个索引映射。

示例: 键:1,2,3,.... n 28 张地图,每张 100 万张。 索引图: 1-1,000,000 -> 地图1 1,000,000-2,000,000 -> 地图2

因此,您将进行两次查找,但键集将是 1,000,000 与 28,000,000。您也可以使用 sting 模式轻松做到这一点。

如果密钥是完全随机的,那么这将不起作用

【讨论】:

即使键是随机的,您也可以使用 (key.hashCode() % 28) 选择存储该键值的映射。【参考方案19】:

我建议三管齐下:

    以更多内存运行 Java:java -Xmx256M 例如以 256 MB 运行。如果需要,可以使用更多,并且您有大量 RAM。

    按照另一张海报的建议缓存计算的哈希值,这样每个对象只计算一次哈希值。

    使用更好的散列算法。您发布的内容将在 a = 0, 1 处返回与 a =1, 0 处相同的哈希值,其他所有条件都相同。

免费使用 Java 提供的功能。

public int hashCode() 
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);

我很确定这比您现有的 hashCode 方法发生冲突的可能性要小得多,尽管它取决于您数据的确切性质。

【讨论】:

对于这类地图和数组来说,内存可能太小了,所以我已经怀疑内存限制问题了。【参考方案20】:

您是否考虑过使用嵌入式数据库来执行此操作。看Berkeley DB。它是开源的,现在归 Oracle 所有。

它将所有内容存储为 Key->Value 对,它不是 RDBMS。它的目标是快速。

【讨论】:

由于序列化/IO 开销,Berkeley DB 对于这么多条目的速度还远远不够;它永远不会比哈希图快,并且 OP 不关心持久性。你的建议不好。【参考方案21】:

SQLite 让你在内存中使用它。

【讨论】:

【参考方案22】:

您可以尝试使用内存数据库,例如HSQLDB。

【讨论】:

【参考方案23】:

在开始时分配一张大地图。如果您知道它将有 2600 万个条目并且您有足够的内存,请执行 new HashMap(30000000)

您确定您有足够的内存来容纳 2600 万个包含 2600 万键和值的条目吗?这对我来说听起来像是很多记忆。您确定垃圾收集在您的 2 到 3 百万标记处仍然正常吗?我可以把它想象成一个瓶颈。

【讨论】:

哦,另一件事。您的哈希码必须均匀分布,以避免在地图中的单个位置出现大型链表。【参考方案24】:

我的第一个想法是确保正确初始化 HashMap。来自JavaDocs for HashMap:

HashMap 的实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中的桶数,初始容量只是哈希表创建时的容量。负载因子是哈希表在其容量自动增加之前允许达到的程度的度量。当哈希表中的条目数超过负载因子和当前容量的乘积时,对哈希表进行重新哈希(即重建内部数据结构),使哈希表的桶数大约增加一倍。

因此,如果您从一个太小的 HashMap 开始,那么每次需要调整大小时,所有哈希都会重新计算...这可能是您在到达 2-3 百万个插入点。

【讨论】:

我认为它们永远不会被重新计算。表的大小增加了,哈希值被保留了。 Hashmap 只是按位对每个条目进行:newIndex = storedHash & newLength; Hanning:也许对 delfuego 来说措辞很糟糕,但这一点是正确的。是的,在 hashCode() 的输出没有被重新计算的意义上,哈希值没有被重新计算。但是当表的大小增加时,所有的键都必须重新插入到表中,也就是必须对哈希值重新进行散列,才能得到表中的新槽号。 杰伊,是的——确实措辞很差,你说的也是。 :) @delfuego 和 @nash0:是的,将初始容量设置为元素数量会降低性能,因为您有数百万次碰撞,因此您只使用了少量那个能力。 即使如果你使用所有可用的条目,设置相同的容量会使其最糟糕!因为由于负载因素,将需要更多空间。您必须使用 initialcapactity = maxentries/loadcapacity (例如 30M ,0.95 用于 26M 条目)但这是 NOT 您的情况,因为您遇到的所有冲突仅使用了大约 20k 或更少.【参考方案25】:

HashMap具有初始容量,HashMap的性能非常依赖于产生底层对象的hashCode。

尝试同时调整两者。

【讨论】:

以上是关于Java HashMap 性能优化/替代的主要内容,如果未能解决你的问题,请参考以下文章

Android应用性能优化之使用SparseArray替代HashMap

android性能优化

性能优化SparseArray

java性能优化之HashMap,LinkedHashMap,TreeMap读取大量数据效率的比较

.NET性能优化-使用结构体替代类

Day782.HashMap的设计与优化 -Java 性能调优实战