it-swarm.cn

在的的的Java中增加地图值的最有效方法

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

假设我正在创建一个Word频率列表,使用Map(可能是HashMap),其中每个键都是一个字符串,其中包含正在计数的Word,并且值是一个Integer,每次找到Word的标记时它都会递增。

在Perl中,增加这样的值将非常简单:

$map{$Word}++;

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

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

这当然依赖于较新Java版本中的自动装箱功能。我想知道你是否可以提出一种更有效的方法来增加这样的价值。是否有良好的性能原因可以避开Collections框架并使用其他东西?

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

323
gregory

一些测试结果

我已经得到了很多这个问题的好答案 - 感谢大家 - 所以我决定运行一些测试并找出哪种方法实际上最快。我测试的五种方法是:

  • 我提出的“ContainsKey”方法 问题
  • aleksandar Dimitrov建议的“TestForNull”方法
  • hank Gay建议的“AtomicLong”方法
  • jrudolph建议的“Trove”方法
  • phax.myopenid.com建议的“MutableInt”方法

方法

这就是我做的......

  1. 创建了五个相同的类,除了下面显示的差异。每个类都必须执行我所呈现的场景的典型操作:打开10MB文件并读入,然后执行文件中所有Word令牌的频率计数。由于这平均只花了3秒钟,我让它执行频率计数(不是I/O)10次。
  2. 定时循环10次迭代,但不是I/O操作并记录了基本上使用 Ian Darwin在Java Cookbook中的方法所花费的总时间(以秒为单位)
  3. 连续完成了所有五项测试,然后又做了三次。
  4. 平均每种方法的四个结果。

结果

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

正如预期的那样, ContainsKey 方法是最慢的,所以我将给出每种方法的速度与该方法的速度相比较。

  • ContainsKey: 30.654秒(基线)
  • AtomicLong: 29.780秒(快1.03倍)
  • TestForNull: 28.804秒(快1.06倍)
  • Trove: 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);
}

的AtomicLong

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();
}
341
gregory

好的,可能是一个老问题,但Java 8有一个更短的方法:

Map.merge(key, 1, Integer::sum)

它做什么:if key 不存在,把 1 作为值,否则 sum 1 到链接到 key 的值。更多信息 这里

167
LE GALL Benoît

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

时间\空间结果: 

42
leventov

谷歌 番石榴 是你的朋友......

......至少在某些情况下。他们有这个Nice AtomicLongMap 。特别好,因为你在地图中处理 long 作为值。

例如。

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

也可以为值添加多于1:

map.getAndAdd(Word, 112L); 
32
H6.

@Hank Gay

作为我自己(相当无用的)评论的后续行动:Trove看起来像是要走的路。无论出于何种原因,如果你想坚持使用标准JDK, ConcurrentMapAtomicLong 可以使代码成为 tiny bit更好,尽管是YMMV。

    final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.putIfAbsent("foo", new AtomicLong(0));
    map.get("foo").incrementAndGet();

1作为foo地图中的值。实际上,增加对线程的友好性就是这种方法必须推荐的。

31
Hank Gay

查看 Google Collections Library 这类事情总是一个好主意。在这种情况下, Multiset 将起作用:

Multiset bag = Multisets.newHashMultiset();
String Word = "foo";
bag.add(Word);
bag.add(Word);
System.out.println(bag.count(Word)); // Prints 2

有类似于Map的方法来迭代键/条目等。在内部,实现当前使用HashMap<E, AtomicInteger>,因此您不会产生拳击成本。

25
Chris Nokleberg

你应该知道你原来的尝试

int count = map.containsKey(Word)? map.get(Word):0;

在地图上包含两个可能很昂贵的操作,即containsKeyname__和getname__。前者执行的操作可能与后者非常相似,所以你要做同样的工作 两次

如果查看Map的API,getname__操作通常会在地图不包含请求的元素时返回nullname__。

请注意,这将成为一个解决方案

map.put(key,map.get(key)+ 1);

危险,因为它可能会产生NullPointerExceptionname__s。您应该首先检查nullname__。

另请注意,这非常重要,根据定义,HashMapname__s 可以 包含nullsname__。所以不是每个返回的nullname__都说“没有这样的元素”。在这方面,containsKeyname__表示 不同 来自getname__实际告诉你 是否 是否存在这样的元素。有关详细信息,请参阅API。

但是,对于您的情况,您可能不想区分存储的nullname__和“noSuchElement”。如果您不想允许nullname__s,您可能更喜欢Hashtablename__。使用其他答案中已经提出的包装库可能是手动处理的更好解决方案,具体取决于应用程序的复杂程度。

为了完成答案(我忘了先把它放进去,多亏了编辑功能!),本地最好的做法是将getname__变成finalname__变量,用1检查nullname__和putname__。 。变量应该是finalname__,因为它无论如何都是不可变的。编译器可能不需要这个提示,但它更清晰。

 final HashMap map = generateRandomHashMap(); 
 final Object key = fetchSomeKey(); 
 final Integer i = map.get(key); 
 if(i != null){
 map.put(i + 1); 
} else {
 //做某事
} 

如果你不想依赖自动装箱,你应该说map.put(new Integer(1 + i.getValue()));之类的东西。

21
Aleksandar Dimitrov
Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0);
map.put(key, count + 1);

这就是你用简单的代码增加一个值的方法。

效益:

  • 不为mutable int创建另一个类
  • 短代码
  • 容易明白
  • 没有空指针异常

另一种方法是使用合并方法,但这对于增加值来说太多了。

map.merge(key, 1, (a,b) -> a+b);

建议:在大多数情况下,您应该关注代码可读性而不是小的性能提升。

20
off99555

另一种方法是创建一个可变整数:

class MutableInt {
  int value = 0;
  public void inc () { ++value; }
  public int get () { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt> ();
MutableInt value = map.get (key);
if (value == null) {
  value = new MutableInt ();
  map.put (key, value);
} else {
  value.inc ();
}

当然这意味着创建一个额外的对象,但与创建一个Integer(即使使用Integer.valueOf)相比,开销不应该那么多。

18
Philip Helger

您可以在 Java 8 中提供的Map接口中使用 computeIfAbsent method。

final Map<String,AtomicLong> map = new ConcurrentHashMap<>();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("B", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet(); //[A=2, B=1]

方法computeIfAbsent检查指定的键是否已经与值相关联?如果没有关联值,则它尝试使用给定的映射函数计算其值。在任何情况下,它返回与指定键关联的当前(现有或计算)值,如果计算值为null,则返回null。

另外,如果您有多个线程更新公共总和的情况,您可以查看 LongAdder class。在高争用情况下,此类的预期吞吐量明显高于AtomicLong,但代价是空间更大消费。

9
i_am_zero

内存轮换可能是一个问题,因为每次装入大于或等于128的int会导致对象分配(请参阅Integer.valueOf(int))。虽然垃圾收集器非常有效地处理短期对象,但性能会受到一定程度的影响。

如果您知道所做的增量数量将大大超过键的数量(在这种情况下为单词),请考虑使用int holder。 Phax已经为此提供了代码。这里再次进行两次更改(holder类为static,初始值为1):

static class MutableInt {
  int value = 1;
  void inc() { ++value; }
  int get() { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt>();
MutableInt value = map.get(key);
if (value == null) {
  value = new MutableInt();
  map.put(key, value);
} else {
  value.inc();
}

如果您需要极高的性能,请寻找直接针对原始值类型的Map实现。 jrudolph提到 GNU Trove

顺便说一下,这个主题的一个好的搜索词是“直方图”。

7
volley

而不是调用containsKey(),只需调用map.get并检查返回的值是否为null。

    Integer count = map.get(Word);
    if(count == null){
        count = 0;
    }
    map.put(Word, count + 1);
5
Glever

有几种方法:

  1. 使用像Google集合中包含的集合一样的Bag算法。

  2. 创建可在Map中使用的可变容器:


    class My{
        String Word;
        int count;
    }

并使用put(“Word”,new My(“Word”));然后你可以检查它是否存在并在添加时增加。

避免使用列表滚动您自己的解决方案,因为如果您进行内部搜索和排序,您的性能将会很糟糕。第一个HashMap解决方案实际上非常快,但像Google Collections中的那个更合适可能更好。

使用Google Collections计算单词,看起来像这样:



    HashMultiset s = new HashMultiset();
    s.add("Word");
    s.add("Word");
    System.out.println(""+s.count("Word") );

使用HashMultiset是非常好的,因为在计算单词时你需要一个包算法。

3
tovare

Google Collections HashMultiset:
- 使用起来相当优雅
- 但消耗CPU和内存

最好的方法是:Entry<K,V> getOrPut(K);(优雅,低成本)

这样的方法只计算一次哈希和索引,然后我们可以用条目做我们想要的(替换或更新值)。

更优雅:
- 拿一个HashSet<Entry>
- 扩展它以便get(K)在需要时放入一个新条目
- 条目可能是您自己的对象。
- > (new MyHashSet()).get(k).increment();

3
the felis leo

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
Eamonn O'Brien-Strain

我认为您的解决方案将是标准方式,但是 - 正如您自己指出的那样 - 它可能不是最快的方式。

你可以看一下 GNU Trove 。这是一个包含各种快速原始集合的库。你的例子将使用 TObjectIntHashMap 它有一个方法adjustOrPutValue,它完全符合你的要求。

3
jrudolph

你确定这是一个瓶颈吗?你做过任何性能分析吗?

尝试使用NetBeans探查器(它是免费的并内置于NB 6.1)来查看热点。

最后,JVM升级(比如从1.5-> 1.6)通常是一个廉价的性能助推器。即使是内部版本号的升级也可以提供良好的性能提升。如果您在Windows上运行并且这是服务器类应用程序,请在命令行上使用-server来使用Server Hotspot JVM。在Linux和Solaris计算机上,这是自动检测的。

3
John Wright

“put”需要“get”(以确保没有重复键)。
所以直接做“放”,
如果有以前的值,那么请添加:

Map map = new HashMap ();

MutableInt newValue = new MutableInt (1); // default = inc
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.add(oldValue); // old + inc
}

如果count从0开始,则添加1 :(或任何其他值...)

Map map = new HashMap ();

MutableInt newValue = new MutableInt (0); // default
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.setValue(oldValue + 1); // old + inc
}

注意: 此代码不是线程安全的。使用它来构建然后使用地图,而不是同时更新它。

优化: 在循环中,保持旧值成为下一个循环的新值。

Map map = new HashMap ();
final int defaut = 0;
final int inc = 1;

MutableInt oldValue = new MutableInt (default);
while(true) {
  MutableInt newValue = oldValue;

  oldValue = map.put (key, newValue); // insert or...
  if (oldValue != null) {
    newValue.setValue(oldValue + inc); // ...update

    oldValue.setValue(default); // reuse
  } else
    oldValue = new MutableInt (default); // renew
  }
}
2
the felis leo

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

HashBagMutableObjectIntMap支持,它存储原始的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的提交者。

1
Craig P. Motlin

我将使用Apache Collections Lazy Map(将值初始化为0)并使用Apache Lang中的MutableIntegers作为该映射中的值。

最大的成本是必须在方法中两次搜索地图。在我的,你只需要做一次。只需获取值(如果不存在则会初始化)并递增它。

1
jb.

Functional Java library的TreeMap数据结构在最新的主干头中有一个update方法:

public TreeMap<K, V> update(final K k, final F<V, V> f)

用法示例:

import static fj.data.TreeMap.empty;
import static fj.function.Integers.add;
import static fj.pre.Ord.stringOrd;
import fj.data.TreeMap;

public class TreeMap_Update
  {public static void main(String[] a)
    {TreeMap<String, Integer> map = empty(stringOrd);
     map = map.set("foo", 1);
     map = map.update("foo", add.f(1));
     System.out.println(map.get("foo").some());}}

该程序打印“2”。

1
Apocalisp

我不知道它的效率如何,但下面的代码也可以。你需要在开头定义一个BiFunction。此外,您可以使用此方法进行更多增量。

public static Map<String, Integer> strInt = new HashMap<String, Integer>();

public static void main(String[] args) {
    BiFunction<Integer, Integer, Integer> bi = (x,y) -> {
        if(x == null)
            return y;
        return x+y;
    };
    strInt.put("abc", 0);


    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abcd", 1, bi);

    System.out.println(strInt.get("abc"));
    System.out.println(strInt.get("abcd"));
}

输出是

3
1
1
MGoksu

各种原始包装器,例如Integer是不可变的,所以实际上没有更简洁的方法来做你要求的事情 除非 你可以用 AtomicLong 这样做。我可以在一分钟内完成并更新。 BTW, HashtableCollections Framework的一部分

1
Hank Gay

@Vilmantas Baranauskas:关于这个答案,我会评论我是否有代表点,但我没有。我想要注意,那里定义的Counter类没有线程安全,因为仅仅同步inc()而不同步value()是不够的。除非已经与更新建立了先发生关系,否则不保证调用value()的其他线程看到该值。

1
Alex Miller

很简单,只需使用Map.Java中的内置函数即可

map.put(key, map.getOrDefault(key, 0) + 1);
0
sudoz