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();

针对包含相同值的HashMapConcurrentHashMapImmutableMap 运行此程序,在使用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 - Stringequals()hashCode() 方法效率低下?这似乎是一个大问题 - 它是用作映射键的主要类型......我对代码的印象是ImmutableMapHashMap 非常相似,就像你说的那样,但是我看到的访问时间不要不同意。 #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() 首先进行== 检查,它(据说)应该比== &amp;&amp; .equals() 3.这当然是可能的(HashMap 默认为.75,其中ImmutableMap 动态选择一个不大于1.2 的值)但是即使将我的buildMap() 方法更改为使用new HashMap&lt;&gt;(16,1.2f) 这应该更糟糕ImmutableMap 的实现,HashMap 仍然明显快,如果比原始基准稍慢的话。同样值得注意的是RegularImmutableMap 中的评论说“即使在高负载因子的情况下,封闭寻址也往往表现良好...... [T] 在节省空间的同时,表仍然可能相对稀疏(因此它会快速丢失)。”

以上是关于Guava ImmutableMap 的访问速度明显比 HashMap 慢的主要内容,如果未能解决你的问题,请参考以下文章

Java技术指南「并发编程专题」针对于Guava RateLimiter限流器的入门到精通(含实

使用Guava RateLimiter限流以及源码解析

使用Guava RateLimiter限流以及源码解析

java 在另一个ImmutableMap上叠加/合并ImmutableMap

如何guava的RateLimiter使用

如何guava的RateLimiter使用