用Java增加Map值的最有效方法

与世无争的帅哥 提交于 2020-03-03 12:17:05

我希望这个问题对于本论坛来说不是太基本了,但是我们会看到的。 我想知道如何重构一些代码以获得更好的性能,而这些性能已经运行了很多次。

假设我正在使用地图(可能是HashMap)创建一个单词频率列表,其中每个键是一个带有要计算单词的String,并且值是一个Integer,每次找到该单词的标记时都会增加。

在Perl中,增加这样的值非常容易:

$map{$word}++;

但是在Java中,它要复杂得多。 这是我目前的操作方式:

int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);

当然,哪个依赖于较新的Java版本中的自动装箱功能。 我想知道您是否可以建议一种更有效的递增此值的方法。 避开Collections框架并改用其他东西,甚至有良好的性能原因吗?

更新:我已经测试了几个答案。 见下文。


#1楼

一些测试结果

对于这个问题,我已经得到了很多很好的答案-谢谢大家-因此,我决定进行一些测试,找出哪种方法实际上最快。 我测试的五种方法是:

  • 我在问题中介绍的“ ContainsKey”方法
  • Aleksandar Dimitrov建议的“ TestForNull”方法
  • Hank Gay建议的“ AtomicLong”方法
  • jrudolph建议的“激励”方法
  • phax.myopenid.com建议的“ MutableInt”方法

方法

这是我做的...

  1. 创建了五个相同的类,除了以下所示的差异。 每个班级都必须执行我所介绍的场景的典型操作:打开一个10MB的文件并读入它,然后对文件中所有单词标记的频率进行计数。 由于平均只需要3秒钟,因此我让它执行了10次频率计数(而不是I / O)。
  2. 定时10次迭代的循环而不是I / O操作的时间,并基本上使用Java Cookbook中的Ian Darwin的方法记录所花费的总时间(以时钟秒为单位)。
  3. 依次执行了所有五个测试,然后又进行了三次。
  4. 平均每种方法的四个结果。

结果

我将首先提供结果,并为感兴趣的人提供以下代码。

如所预期的, ContainsKey方法是最慢的,因此,与该方法的速度相比,我将给出每种方法的速度。

  • ContainsKey: 30.654秒(基准)
  • AtomicLong: 29.780秒(速度的1.03倍)
  • TestForNull: 28.804秒(速度的1.06倍)
  • 宝座 26.313秒(1.16倍的速度)
  • MutableInt: 25.747秒(1.19倍的速度)

结论

似乎只有MutableInt方法和Trove方法要快得多,因为它们的性能提升超过10%。 但是,如果线程成为问题,AtomicLong可能比其他线程更具吸引力(我不确定)。 我还运行了带有final变量的TestForNull,但是差异可以忽略不计。

请注意,我没有介绍不同情况下的内存使用情况。 我很高兴听到任何对MutableInt和Trove方法将如何影响内存使用有深刻见解的人。

我个人认为MutableInt方法最吸引人,因为它不需要加载任何第三方类。 因此,除非我发现问题,否则这是我最有可能采取的方法。

编码

这是每种方法的关键代码。

ContainsKey

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
int count = freq.containsKey(word) ? freq.get(word) : 0;
freq.put(word, count + 1);

TestForNull

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
Integer count = freq.get(word);
if (count == null) {
    freq.put(word, 1);
}
else {
    freq.put(word, count + 1);
}

原子长

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
...
final ConcurrentMap<String, AtomicLong> map = 
    new ConcurrentHashMap<String, AtomicLong>();
...
map.putIfAbsent(word, new AtomicLong(0));
map.get(word).incrementAndGet();

宝藏

import gnu.trove.TObjectIntHashMap;
...
TObjectIntHashMap<String> freq = new TObjectIntHashMap<String>();
...
freq.adjustOrPutValue(word, 1, 1);

MutableInt

import java.util.HashMap;
import java.util.Map;
...
class MutableInt {
  int value = 1; // note that we start at 1 since we're counting
  public void increment () { ++value;      }
  public int  get ()       { return value; }
}
...
Map<String, MutableInt> freq = new HashMap<String, MutableInt>();
...
MutableInt count = freq.get(word);
if (count == null) {
    freq.put(word, new MutableInt());
}
else {
    count.increment();
}

#2楼

MutableInt方法的一种变体是使用单元素int数组,如果稍作改动,它可能会更快一些:

Map<String,int[]> map = new HashMap<String,int[]>();
...
int[] value = map.get(key);
if (value == null) 
  map.put(key, new int[]{1} );
else
  ++value[0];

如果您可以使用此版本重新运行性能测试,那将很有趣。 它可能是最快的。


编辑:上面的模式对我来说很好用,但是最终我改变为使用Trove的集合来减少我正在创建的一些非常大的地图中的内存大小-而且,它还更快。

一个很好的特点是, TObjectIntHashMap类有一个adjustOrPutValue呼叫,取决于是否已经有在该键的值,要么把一个初始值,或增加现有的值。 这非常适合递增:

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);

#3楼

Google Guava是您的朋友...

...至少在某些情况下。 他们有这个漂亮的AtomicLongMap 。 特别好,因为您正在处理地图中的长期价值。

例如

AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(word);

也可以将值加1以上:

map.getAndAdd(word, 112L); 

#4楼

如果您使用的是Eclipse Collections ,则可以使用HashBag 。 就内存使用而言,这将是最有效的方法,并且在执行速度方面也将表现良好。

HashBagHashBag支持,该MutableObjectIntMap存储原始int而不是Counter对象。 这样可以减少内存开销并提高执行速度。

HashBag提供了所需的API,因为它是一个Collection ,它还允许您查询项目的出现次数。

这是Eclipse Collections Kata的示例。

MutableBag<String> bag =
  HashBag.newBagWith("one", "two", "two", "three", "three", "three");

Assert.assertEquals(3, bag.occurrencesOf("three"));

bag.add("one");
Assert.assertEquals(2, bag.occurrencesOf("one"));

bag.addOccurrences("one", 4);
Assert.assertEquals(6, bag.occurrencesOf("one"));

注意:我是Eclipse Collections的提交者。


#5楼

2016年的一些研究: https : //github.com/leventov/java-word-count基准源代码

每种方法的最佳结果(越小越好):

                 time, ms
kolobokeCompile  18.8
koloboke         19.8
trove            20.8
fastutil         22.7
mutableInt       24.3
atomicInteger    25.3
eclipse          26.9
hashMap          28.0
hppc             33.6
hppcRt           36.5

时间/空间结果:

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!