Guava ImmutableMap 的访问速度明显比 HashMap 慢
Posted
技术标签:
【中文标题】Guava ImmutableMap 的访问速度明显比 HashMap 慢【英文标题】:Guava ImmutableMap has noticeably slower access than HashMap 【发布时间】:2013-03-23 07:46:42 【问题描述】:在对一些高吞吐量数据结构进行内存基准测试时,我意识到我可以使用ImmutableMap
,只需进行一点重构。
认为这将是一个改进,我将其投入其中,并惊讶地发现它不仅比 HashMap
慢,在单线程环境中它似乎始终比 ConcurrentHashMap
慢!
你可以看到full benchmark
测试的内容非常简单,计算获取地图中可能存在的大量随机字符串需要多长时间。
public static void timeAccess(Map<String,String> map)
Random rnd = new Random(seed);
int foundCount = 0;
long start = System.nanoTime();
for(int i = 0; i < loop; i++)
String s = map.get(RndString.build(rnd));
if(s != null)
foundCount++;
long stop = System.nanoTime() - start;
System.out.println("Found "+foundCount+" strings out of "+loop+" attempts - "+
String.format("%.2f",100.0*foundCount/loop)+" success rate.");
System.out.println(map.getClass().getSimpleName()+" took "+
String.format("%.4f", stop/1_000_000_000.0)+" seconds.");
System.out.println();
针对包含相同值的HashMap
、ConcurrentHashMap
和ImmutableMap
运行此程序,在使用ImmutableMap
时始终显示出显着的减速 - 通常慢 15% 以上。地图越稀疏(即,map.get()
返回 null 的频率越高)差异越大。这是示例运行的结果:
Found 35312152 strings out of 100000000 attempts - 35.31 success rate.
HashMap took 29.4538 seconds.
Found 35312152 strings out of 100000000 attempts - 35.31 success rate.
ConcurrentHashMap took 32.1465 seconds.
Found 35312152 strings out of 100000000 attempts - 35.31 success rate.
RegularImmutableMap took 37.9709 seconds.
这是记录/预期的问题吗? Guava Docs 表示 Immutable***
内存效率更高,但没有说明速度。对于这种幅度的减速,我倾向于处理内存成本并在速度成为问题时避免Immutable***
(什么时候不是?!)。我错过了什么吗?
另见:https://groups.google.com/forum/?fromgroups=#!topic/guava-discuss/I7yPpa5Hlpg
【问题讨论】:
该邮件列表线程中提到的问题肯定仍然适用于您的基准测试。此外,请参阅ImmutableMap
Javadoc:“与 HashMap 不同,ImmutableMap 未针对具有慢 Object.equals(java.lang.Object) 或 Object.hashCode() 实现的元素类型进行优化。您可以通过让您的元素获得更好的性能类型缓存它自己的哈希码,并通过使用缓存的值来短路慢速等于算法。”这肯定是String
的问题。最后,ImmutableMap
的实现与HashMap
的实现基本相同。
如果你喜欢,你可以尝试一些 Guava 的基准测试:code.google.com/p/guava-libraries/source/browse/guava-tests/…
此外,“处理内存成本”可能会导致 GC 负载显着增加,这可能会减慢您的程序的速度,就像更慢但更紧凑的实现一样。使用您的特定实际应用程序进行分析真的无可替代。
@LouisWasserman #1 - String
的 equals()
和 hashCode()
方法效率低下?这似乎是一个大问题 - 它是用作映射键的主要类型......我对代码的印象是ImmutableMap
与HashMap
非常相似,就像你说的那样,但是我看到的访问时间不要不同意。 #2 谢谢,我去看看。
#1: String
有一个线性时间 equals
方法——我不会称其为“大问题”,这只是算法定律——它不是使用它的hashCode
缓存来短路,这……并不理想,但无论哪种方式都可以争论。 #3:当然,这对你的用例来说是正确的,但是 ImmutableMap 被优化为一个通才,在许多不同的用例中都是好的,如果不是完美的。 (例如,android 应用程序从 ImmutableMap 的紧凑设计中受益匪浅。)
【参考方案1】:
正如 Louis Wasserman 所说,ImmutableMap
没有针对具有慢速 equals
方法的对象进行优化。我认为主要区别在这里:
HashMap:
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
ImmtubleMap:
if (key.equals(candidateKey))
return entry.getValue();
如您所见,要检查冲突,HashMap
首先检查哈希。这允许快速拒绝具有不同哈希值的值。由于String
没有在其equals
方法中进行此检查,这使得HashMap
更快。 ImmutableMap
不使用此优化,因为当 equals
已经优化时,它会使测试变慢。
【讨论】:
啊,这更有意义,我明白他在说什么。令人着迷的是,String 缓存了String.hashCode()
中的哈希值,但没有利用 String.equals()
中的数据...
这似乎是问题的很大一部分,将 Holder
包装器添加到缓存对象哈希码的键中,将 ImmutableMap 的时间从 ~35
秒降至 ~32
.然而,HashMap
在 ~28
秒内的表现仍然明显更快。如果 String
(最常见的地图键)不符合该假设,则 Guava 认为键将/应该表现良好的假设似乎是一个非常糟糕的假设。【参考方案2】:
一些可能的原因:
这是否取决于您对 RndString.build() 的实现?
看看两个地图的 get() 实现: com.google.common.collect.RegularImmutableMap.get(Object) java.util.HashMap.getEntry(对象) java.util.HashMap 首先尝试与“==”进行比较。 RegularImmutableMap 没有。这可能会加快速度
可能是不同的负载系数造成的吗?也许 RegularImmutableMap 需要更多的迭代才能找到正确的条目。
【讨论】:
1.我的问题中有完整代码的链接。RndString.build()
应该使用最少的内存并始终使用相同的种子来访问地图。这肯定会导致一些偏见,但我不知道如何。
2.好点,但是RegularImmutableMap.get()
说如下:“如果我们做了 [an == check],对于最注重性能的 [equals 方法] 来说只会让事情变得更糟”。由于String.equals()
首先进行==
检查,它(据说)应该比== && .equals()
快
3.这当然是可能的(HashMap
默认为.75
,其中ImmutableMap
动态选择一个不大于1.2
的值)但是即使将我的buildMap()
方法更改为使用new HashMap<>(16,1.2f)
这应该更糟糕 比ImmutableMap
的实现,HashMap
仍然明显快,如果比原始基准稍慢的话。同样值得注意的是RegularImmutableMap
中的评论说“即使在高负载因子的情况下,封闭寻址也往往表现良好...... [T] 在节省空间的同时,表仍然可能相对稀疏(因此它会快速丢失)。” 以上是关于Guava ImmutableMap 的访问速度明显比 HashMap 慢的主要内容,如果未能解决你的问题,请参考以下文章
Java技术指南「并发编程专题」针对于Guava RateLimiter限流器的入门到精通(含实