Java HashMap性能优化/替代


102

我想创建一个大型HashMap,但put()性能不够好。有任何想法吗?

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

map.get(key)

就我而言,我想创建一个包含2600万个条目的地图。使用标准的Java HashMap,插入2到3百万次后,放置速度会变得异常缓慢。

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

我的哈希码方法:

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;
}

我正在使用adding的关联属性来确保相等的对象具有相同的哈希码。数组是字节,值的范围是0-51。在两个数组中,值只能使用一次。如果a数组包含相同的值(任一顺序)并且b数组的对象相同,则对象相等。因此a = {0,1} b = {45,12,33}和a = {1,0} b = {33,45,12}是相等的。

编辑,一些注意事项:

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

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

  • 有人建议在其他情况下使用数据库,这绝对是明智的选择。但是我确实是在问一个数据结构和算法问题,一个完整的数据库会比一个好的数据结构解决方案矫kill过正,并且速度慢得多(毕竟,所有数据库只是软件,但是会有通信和磁盘开销)。


29
如果HashMap变慢,则很可能您的哈希函数不够好。
Pascal Cuoq

12
医生,它伤害,当我做这个
skaffman

12
这是一个很好的问题。一个很好的演示,说明了散列算法为何重要以及它们可能对性能产生什么影响
oxbow_lakes

12
a的总和的范围是0到102,b的总和的范围是0到153,因此您只有15606个可能的哈希值和具有相同hashCode的平均1666个键。您应该更改您的哈希码,以便可能的hashCodes的数量远大于键的数量。
彼得·劳瑞

6
我已经从心理上确定您正在建模Texas Hold'Em Poker ;-)
bacar 2012年

Answers:


56

正如许多人指出的那样,这种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个看跌期权(100,000到2,000,000)的平均每秒看跌次数为:

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

好多了。旧方法很快终止,而新方法保持了良好的吞吐量。


17
我建议不要在hashCode方法中修改数组。按照约定,hashCode不更改对象的状态。也许构造函数将是对它们进行排序的更好的地方。
迈克尔·迈尔斯

我同意数组的排序应在构造函数中进行。显示的代码似乎从未设置过hashCode。可以如下简化代码的计算:int result = a[0]; result = result * 52 + a[1]; //etc
rsp

我同意在构造函数中排序,然后按照mmyers和rsp的建议计算哈希码会更好。就我而言,我的解决方案是可以接受的,我想强调一个事实,即必须对数组进行排序hashCode()才能正常工作。
nash

3
请注意,您还可以缓存哈希码(并且如果您的对象是可变的,则可以使其无效)。
NateS

1
只需使用java.util.Arrays.hashCode()即可。它更简单(没有代码可以自己编写和维护),其计算可能更快(乘法次数更少),并且其哈希代码的分布可能更加均匀。
jcsahnwaldt恢复莫妮卡

18

有一件事我在您的通知hashCode()方法是,在数组中元素的顺序a[],并b[]没有问题。因此(a[]={1,2,3}, b[]={99,100})将散列为与相同的值(a[]={3,1,2}, b[]={100,99})。实际上,所有键k1以及k2在哪里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如果需要的话)。那应该使事情更加平衡。


尽管我还应该补充一点,因为我希望具有相同元素(以不同顺序排列)的数组给出相同的哈希码的属性对我来说不起作用。
nash

5
在这种情况下,您具有52C2 + 52C3哈希码(根据我的计算器为23426),并且哈希图非常适合此工作。
kdgregory

实际上,这将提高性能。冲突数量越多,哈希表eq中的条目就越少。减少工作量。我敢打赌,它不是哈希(看起来不错)还是哈希表(效果很好),不是在性能下降的对象创建上。
OscarRyz

7
@Oscar-更多碰撞等于要做更多的工作,因为现在您必须对哈希链进行线性搜索。如果每个equals()有26,000,000个不同的值,每个hashCode()有26,000个不同的值,则存储桶链将每个有1,000个对象。
kdgregory

@ Nash0:您似乎在说要它们具有相同的hashCode,但同时不相等(由equals()方法定义)。你为什么要那个?
MAK

17

详细介绍Pascal:您了解HashMap的工作原理吗?您的哈希表中有一些插槽。找到每个键的哈希值,然后将其映射到表中的条目。如果两个哈希值映射到同一条目(即“哈希冲突”),则HashMap会构建一个链接列表。

哈希冲突可能会破坏哈希映射的性能。在极端情况下,如果所有键都具有相同的哈希码,或者它们具有不同的哈希码,但它们都映射到相同的插槽,则您的哈希图将变成一个链表。

因此,如果您发现性能问题,那么我要检查的第一件事是:我是否正在获得哈希代码的随机分布?如果不是,则需要更好的哈希函数。好吧,在这种情况下,“更好”可能意味着“对于我的特定数据集更好”。像,假设您正在使用字符串,并且将字符串的长度作为哈希值。(这不是Java的String.hashCode的工作方式,但我只是组成一个简单的示例。)如果您的字符串的长度在1到10,000之间变化很大,并且在该范围内分布相当均匀,那么这可能是一个很好的选择哈希函数。但是,如果您的字符串都是1或2个字符,那么这将是一个非常糟糕的哈希函数。

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

@奥斯卡:嗯,我不明白那是一个“不是真的”。这更像是“让我澄清”。但是是的,的确,如果您使用与现有条目相同的键来创建新条目,则会覆盖第一个条目。这就是我在上一段中谈论寻找重复项的意思:每当密钥散列到相同的插槽时,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,然后乘以5。所以...实际上,他为每个值加上503 * 5381。这有什么作用?向每个哈希值添加常量只会消耗CPU周期,而不会完成任何有用的操作。这里的教训:不是将复杂度添加到哈希函数中是目标。目标是获得广泛的不同值,而不仅仅是为了复杂而增加复杂性。


3
是的,错误的哈希函数会导致这种行为。+1
Henning

并不是的。在哈希相同但键不同的情况下创建列表。例如,如果String给出哈希码2345,而Integer给出相同的哈希码2345,则将整数插入到列表中,因为String.equals( Integer )is false但是,如果您具有相同的类(或至少.equals返回true),那么将使用相同的条目。例如new String("one"),将`new String(“ one”)用作键,将使用相同的条目。其实这是WHOLE HashMap中的第一名点!自己看看:pastebin.com/f20af40b9
OscarRyz

3
@Oscar:请参阅我的原始文章后的回复。
杰伊,

我知道这是一个非常老的线程,但是这里是术语“冲突”的引用,因为它与哈希码有关:link。当您通过使用相同的键放置另一个值来替换哈希图中的一个值时,这称为碰撞
Tahir Akhtar 2012年

@Tahir完全正确。也许我的帖子措辞不佳。感谢您的澄清。
周杰伦

7

我的第一个想法是确保您适当地初始化了HashMap。从JavaDocs for HashMap中

HashMap的实例具有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中存储桶的数量,初始容量只是创建哈希表时的容量。负载因子是散列表的容量自动增加之前允许其填充的完整程度的度量。当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将被重新哈希(即,内部数据结构将被重建),因此哈希表的存储桶数约为两倍。

因此,如果您开始时使用的HashMap太小,则每次需要调整其大小时,都会重新计算所有哈希值……这可能是您达到2-3百万插入点时的感觉。


我认为它们永远不会被重新计算。桌子的大小增加了,散列得以保留。
亨宁

Hashmap只是针对每个条目进行按位操作:newIndex = storedHash&newLength;
亨宁

4
汉宁:也许在delfuego方面措辞不佳,但这是正确的。是的,在不重新计算hashCode()的输出的意义上,不会重新计算哈希值。但是,当增加表大小时,必须将所有键重新插入表中,也就是说,必须重新散列哈希值才能在表中获得新的插槽号。
杰伊

周杰伦,是的-确实措辞不佳,以及您所说的话。:)
delfuego

1
@delfuego和@ nash0:是的,将初始容量设置为等于元素数量会降低性能,因为您有数以亿计的碰撞,因此仅使用了少量的该容量。即使您使用了所有可用的条目,设置相同的容量也会使它变得更糟!,因为由于负载因素,将需要更多空间。您必须使用initialcapactity = maxentries/loadcapacity(例如30M,0.95表示26M条目),但事实并非如此,因为您遇到的所有冲突仅使用了大约20k或更少。
OscarRyz

7

我建议采取三管齐下的方法:

  1. 运行具有更多内存的Java:java -Xmx256M例如以256 MB运行。如果需要,请使用更多的内存,并且有很多RAM。

  2. 根据另一个发布者的建议来缓存计算出的哈希值,因此每个对象仅计算一次其哈希值。

  3. 使用更好的哈希算法。您发布的那个将在a = {0,1}时返回与在a = {1,0}处相同的哈希值,其他所有条件都相同。

利用Java免费提供的功能。

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

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


对于这些类型的映射和数组,RAM可能很小,所以我已经怀疑内存限制问题。
ReneS

7

进入“ on / off主题”的灰色区域,但是有必要消除关于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断言而不是打印行。此示例用于支持以下建议:相同的哈希码会导致冲突,并且当类相同时,仅创建一个条目(例如,在此特定情况下,仅创建一个String):

@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) && s.equals("ese")

第二个测试表明,得出结论,认为同一类上的相同哈希码是键->值s -> 1被测试一中的ese -> 2when 覆盖的原因是错误的map.put(ese, 2)。在试验二,s并且ese仍然有相同的哈希码(通过验证assertEquals(s.hashCode(), ese.hashCode());),他们是同一类。但是,s并且eseMyString该测试中的实例,而不是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;
    }
}

根据后来的评论,奥斯卡似乎改变了他先前所说的话,并承认平等的重要性。但是,似乎仍然很重要的概念是不清楚,而不是“同一阶级”(强调我的意思):

“不是真的。仅在哈希相同但键不同的情况下才创建列表。例如,如果String给出哈希码2345,而Integer给出相同的哈希码2345,则由于String,将整数插入列表。 equals(Integer)是false。但是,如果您具有相同的类(或至少.equals返回true),则使用相同的条目。例如,new String(“ one”)和`new String(“ one”)用作键,将使用相同的条目。实际上,这是HashMap的整个起点!亲自查看:pastebin.com/f20af40b9 – Oscar Reyes”

与先前的注释明确指出了相同的类和相同的哈希码的重要性,而没有提及equals:

“ @delfuego:自己看看:pastebin.com/f20af40b9因此,在这个问题中,使用了相同的类(请等待一分钟,使用相同的类对吗?),这意味着当使用相同的哈希时,应使用相同的条目被使用,并且没有条目“列表”。– Oscar Reyes”

要么

“实际上,这将提高性能。冲突越多,哈希表中的条目越少。要做的工作越少。不是我认为它在对象上的哈希(看起来不错)或哈希表(效果很好)表现令人失望的创作。–奥斯卡·雷耶斯(Oscar Reyes)

要么

“ @kdgregory:是的,但是仅当冲突发生在不同的类上时,对于相同的类(这种情况),将使用相同的条目。– Oscar Reyes”

再一次,我可能会误解奥斯卡实际上想说的话。但是,他的原始评论引起了足够的混乱,以至于通过一些明确的测试来清除所有内容似乎是谨慎的做法,因此没有挥之不去的疑问。


[1] -Joshua Bloch撰写的有效Java第二版

  • 只要在应用程序执行期间在同一个对象上多次调用它,则hashCode方法必须一致地返回相同的整数,前提是未修改在该对象的equals比较中使用的信息。从一个应用程序的执行到同一应用程序的另一执行,此整数不必保持一致。

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

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


5

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

a [0] + a [1]始终在0到512之间。加上b总是得到0到768之间的数字。将它们相乘就可以得出400,000个唯一组合的上限,假设您的数据分布完美每个字节的每个可能值之间。如果您的数据完全正常,则此方法的唯一输出可能会少得多。



4

如果键具有任何模式,则可以将地图拆分为较小的地图并具有索引地图。

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

因此,您将进行两次查找,但是密钥集将为1,000,000与28,000,000。您也可以轻松使用刺痛模式进行此操作。

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


1
即使键是随机的,也可以使用(key.hashCode()%28)选择用于存储键值的映射。
JuhaSyrjälä09年

4

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

在这种情况下,如果使用数组而不是HashMap并从0到25989599对其进行索引,则从数据存储中填充和检索值当然会更快。


这是一个非常好的主意,实际上,我正在针对另一个包含12亿个元素的数据存储问题这样做。在这种情况下,我想采取简单的方法并使用预制的数据结构:)
nash

4

我来晚了,但是关于大地图的一些评论:

  1. 正如在其他文章中详细讨论的那样,具有良好的hashCode(),Map中的26M条目并不重要。
  2. 但是,这里潜在的潜在问题是巨型地图的GC影响。

我假设这些地图是长期存在的。也就是说,您填充它们,它们在应用程序运行期间会一直存在。我还假设该应用程序的寿命很长-就像某种服务器。

Java HashMap中的每个条目都需要三个对象:键,值和将它们联系在一起的Entry。因此,映射中的2600万个条目表示26M * 3 == 78M个对象。这很好,直到您达到完整的GC。然后,您遇到了一个世界暂停问题。GC将查看每个78M对象,并确定它们都还活着。78M +个对象只是很多要查看的对象。如果您的应用程序可以容忍偶尔的长时间(也许几秒钟)的暂停,则没有问题。如果您试图实现任何延迟保证,那么您可能会遇到一个主要问题(当然,如果您想要延迟保证,则不是Java平台可供选择:))如果地图中的值快速波动,您可能会频繁地进行全收集这使问题更加复杂。

对于这个问题,我不知道有什么好的解决方案。想法:

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

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



3

就我而言,我想创建一个包含2600万个条目的地图。使用标准的Java HashMap,插入2到3百万次后,放置速度会变得异常缓慢。

根据我的实验(2009年的学生项目):

  • 我为10.000到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?简而言之,它是一个类似于Binary Tree的树数据结构,其分支编号是质数(而不是“ 2”二进制)。


您能否分享一些链接或实现?



1

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

它将所有内容存储为“键->值”对,而不是RDBMS。它的目标是要快。


2
由于序列化/ IO开销,对于这样数量的条目,Berkeley DB远远不够快。它永远不会比哈希图更快,并且OP不在乎持久性。您的建议不是一个好建议。
oxbow_lakes

1

首先,您应该检查自己是否正确使用了Map,是否使用了正确的hashCode()方法,键的初始容量,正确的Map实现等,如许多其他答案所述。

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

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

另一个选择是某些数据库引擎的权重比完整的SQL RDBMS轻。像伯克利DB可能。

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


1

您可以尝试将计算得出的哈希码缓存到键对象。

像这样:

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之后更改键的内容。

编辑:将每个键仅添加一次到映射中时,具有代码值的缓存似乎不值得。在其他情况下,这可能会很有用。


正如下面所指出的,调整HashMap的大小时,不会对对象的哈希码进行重新计算,因此不会为您带来任何好处。
delfuego

1

另一位发布者已经指出,由于您将值加在一起的方式,您的哈希码实现会导致很多冲突。我愿意的是,如果您在调试器中查看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}会发生冲突),并且会比平移慢。

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

@kdgregory:我在其他地方回答过“更多的碰撞意味着更多的工作” :)
OscarRyz

1

如前所述,您的哈希码实现有太多冲突,并且对其进行修复应可导致不错的性能。此外,缓存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;  

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


1

有效的Java中:编程语言指南(Java系列)

在第3章中,您可以找到计算hashCode()时要遵循的良好规则。

特别:

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


0

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

您确定您有足够的内存来存储2600万个条目以及2600万个键和值吗?对我来说,这听起来像是很多记忆。您确定垃圾收集在2到3百万大关的情况下仍能正常运行吗?我可以想象这是一个瓶颈。


2
哦,另一件事。您的哈希码必须均匀分布,以避免在地图中的单个位置出现大型链表。
ReneS

0

您可以尝试两件事:

  • 使您的 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
$

因此,由于进行了更换,因此使用的初始容量从21s降至16s。这使我们将您的hashCode方法视为“机会领域”;)

编辑

不是HashMap

根据您的最新版本。

我认为您应该对应用程序进行概要分析,并查看在哪里消耗了内存/ cpu。

我创建了一个实现与您相同的类 hashCode

该哈希码产生数百万个冲突,因此HashMap中的条目大大减少了。

我在以前的测试中从21s,16s传递到10s和8s。原因是因为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

3
正如以上其他地方所指出的(针对您的评论),奥斯卡,您似乎在假设更多的冲突是好的;这不是很好。冲突意味着给定哈希中的插槽从包含单个条目变为包含条目列表,并且每次访问该插槽时都必须搜索/遍历此列表。
delfuego

@delfuego:并非如此,仅当您使用不同的类发生碰撞时,才会发生这种情况,但是对于相同的类,则使用相同的条目;)
OscarRyz

2
@Oscar-看看我对MAK的回答。HashMap在每个哈希存储区维护一个条目的链接列表,并在每个元素上调用equals()遍历该列表。该对象的类与它无关(除了equals()上的短路)。
kdgregory

1
@Oscar-阅读您的答案,似乎您假设如果哈希码相同,则equals()将返回true。这不是equals / hashcode合同的一部分。如果我误会了,请忽略此评论。
kdgregory,2009年

1
非常感谢您为Oscar所做的努力,但我认为您混淆了关键对象是否相等以及具有相同的哈希码。同样,在您的代码链接之一中,您使用等号字符串作为键,请记住Java中的字符串是不可变的。我想我们今天都从哈希中学到了很多东西:)
nash


0

我用列表和哈希图进行了一次小测试,有趣的事情是遍历列表,发现对象与使用哈希图获取函数花了相同的时间(以毫秒为单位)……只是一个麻烦。哦,是的,使用这种大小的哈希表时,内存是个大问题。


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.