簡體   English   中英

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

[英]Most efficient way to increment a Map value in Java

我希望這個問題對於這個論壇來說不是太基礎,但我們會看到。 我想知道如何重構一些代碼以獲得更好的性能,這些代碼運行了很多次。

假設我正在創建一個詞頻列表,使用 Map(可能是 HashMap),其中每個鍵是一個字符串,其中包含正在計算的單詞,而值是一個 Integer,每次找到單詞的標記時都會遞增。

在 Perl 中,增加這樣的值非常容易:

$map{$word}++;

但在 Java 中,情況要復雜得多。 這是我目前的做法:

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

這當然依賴於較新的 Java 版本中的自動裝箱功能。 我想知道您是否可以建議一種更有效的方法來增加這樣的值。 避免使用 Collections 框架並使用其他東西來代替甚至有很好的性能原因嗎?

更新:我已經對幾個答案進行了測試。 見下文。

部分測試結果

對於這個問題,我得到了很多很好的答案——謝謝大家——所以我決定運行一些測試並找出哪種方法實際上最快。 我測試的五種方法是:

  • 我在問題中提出的“ContainsKey”方法
  • Aleksandar Dimitrov 建議的“TestForNull”方法
  • Hank Gay 建議的“AtomicLong”方法
  • jrudolph 建議的“Trove”方法
  • phax.myopenid.com 建議的“MutableInt”方法

方法

這就是我所做的...

  1. 創建了五個相同的類,除了下面顯示的差異。 每個班級都必須執行我介紹的場景中的典型操作:打開一個 10MB 的文件並將其讀入,然后對文件中的所有單詞進行頻率計數。 由於這平均只需要 3 秒,我讓它執行頻率計數(不是 I/O)10 次。
  2. 對 10 次迭代的循環而不是 I/O 操作進行計時,並基本上使用Java Cookbook 中的 Ian Darwin 方法記錄所用的總時間(以時鍾秒為單位)。
  3. 連續執行所有五次測試,然后再執行三次。
  4. 平均每種方法的四個結果。

結果

我將首先向有興趣的人展示結果和下面的代碼。

正如預期的那樣, 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 方法最有吸引力,因為它不需要加載任何第三方類。 因此,除非我發現它的問題,否則我最有可能采用這種方式。

代碼

這是每個方法的關鍵代碼。

包含密鑰

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

測試為空

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

可變整數

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

現在有一個使用Map::merge Java 8 更短的方法。

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

它能做什么:

  • 如果不存在,則將1作為值
  • 否則將1與鏈接到的值相加

更多信息在這里

2016年的一點研究: https : //github.com/leventov/java-word-count,benchmark 源碼

每種方法的最佳結果(越小越好):

                 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

時間\\空間結果:

Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0); // ensure count will be one of 0,1,2,3,...
map.put(key, count + 1);

這就是使用簡單代碼增加值的方法。

益處:

  • 無需添加新類或使用可變 int 的另一個概念
  • 不依賴任何庫
  • 易於理解到底發生了什么(不要太抽象)

缺點:

  • 哈希映射將搜索兩次 get() 和 put()。 所以它不會是性能最好的代碼。

理論上,一旦你調用 get(),你就已經知道把()放在哪里,所以你不必再次搜索。 但是在哈希映射中搜索通常只需要很少的時間,您可以忽略這個性能問題。

但是如果你對這個問題非常認真,你是一個完美主義者,另一種方法是使用合並方法,這(可能)比之前的代碼片段更有效,因為你將(理論上)只搜索一次地圖:(雖然這段代碼乍一看並不明顯,它簡短而高效)

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

建議:在大多數情況下,您應該關心代碼可讀性而不是性能提升。 如果第一個代碼片段對您來說更容易理解,請使用它。 但是,如果您能夠理解第二個罰款,那么您也可以繼續學習!

作為我自己評論的后續行動:Trove 看起來是要走的路。 如果出於某種原因,你想堅持使用標准的JDK, ConcurrentMapAtomicLong的可以使代碼一點點更好,但情況因人而異。

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

1作為foo映射中的值。 實際上,增加對線程的友好性是這種方法必須推薦的全部內容。

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

...至少在某些情況下。 他們有這個很好的AtomicLongMap 特別好,因為您正在處理地圖中的長期價值。

例如

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

也可以向值添加多於 1:

map.getAndAdd(word, 112L); 

查看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> ,因此您不會產生裝箱成本。

你應該意識到你最初的嘗試

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

在地圖上包含兩個潛在的開銷較大的操作,即containsKeyget 前者執行的操作可能與后者非常相似,因此您要做兩次相同的工作!

如果您查看 Map 的 API,當地圖不包含請求的元素時, get操作通常會返回null

請注意,這將產生一個類似的解決方案

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

危險,因為它可能會產生NullPointerException 您應該首先檢查null

另請注意,這非常重要,根據定義HashMap可以包含nulls 所以不是每個返回的null都說“沒有這樣的元素”。 在這方面, containsKey行為不同於get in 實際上告訴您是否存在這樣的元素。 有關詳細信息,請參閱 API。

但是,對於您的情況,您可能不想區分存儲的null和“noSuchElement”。 如果您不想允許null您可能更喜歡Hashtable 使用其他答案中已經提出的包裝庫可能是手動處理的更好解決方案,具體取決於您的應用程序的復雜性。

要完成答復(我忘了是在第一,由於編輯功能!),本身做的最好的辦法,就是get進入final變量,檢查null ,並put它放回了1 . 變量應該是final因為它無論如何都是不可變的。 編譯器可能不需要這個提示,但這樣更清楚。

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

如果你不想依賴自動裝箱,你應該說類似map.put(new Integer(1 + i.getValue())); 反而。

另一種方法是創建一個可變整數:

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)相比,開銷不應該那么多。

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

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檢查指定的鍵是否已經與值關聯? 如果沒有關聯值,則它嘗試使用給定的映射函數計算其值。 在任何情況下,它都返回與指定鍵關聯的當前(現有或計算出的)值,如果計算出的值為空,則返回空值。

附帶說明一下,如果您遇到多個線程更新公共總和的情況,您可以查看LongAdder類。在高爭用情況下,此類的預期吞吐量明顯高於AtomicLong ,但代價是空間消耗更高。

內存輪換在這里可能是一個問題,因為大於或等於 128 的 int 的每次裝箱都會導致對象分配(請參閱 Integer.valueOf(int))。 盡管垃圾收集器非常有效地處理生命周期較短的對象,但性能會受到一定程度的影響。

如果您知道增量的數量將大大超過鍵的數量(在本例中為字數),請考慮使用 int 持有者。 Phax 已經為此提供了代碼。 這又是一次,有兩個更改(持有者類設為靜態,初始值設置為 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

順便說一下,這個主題的一個很好的搜索詞是“直方圖”。

很簡單,直接使用Map.java的內置函數如下

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

與調用 containsKey() 相比,調用 map.get 並檢查返回值是否為 null 會更快。

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

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

谷歌集合 HashMultiset :
- 使用起來非常優雅
- 但消耗 CPU 和內存

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

這樣的方法只會計算哈希和索引一次,然后我們可以對條目做我們想做的事情(替換或更新值)。

更優雅:
- 取一個HashSet<Entry>
- 擴展它以便get(K)在需要時放置一個新條目
- 條目可能是您自己的對象。
--> (new MyHashSet()).get(k).increment();

我建議使用 Java 8 Map::compute()。 它也考慮密鑰不存在的情況。

Map.compute(num, (k, v) -> (v == null) ? 1 : v + 1);

我希望這個問題對於本論壇來說不是太基本了,但是我們會看到的。 我想知道如何重構一些代碼以獲得更好的性能,而這些性能已經運行了很多次。

假設我正在使用地圖(可能是HashMap)創建一個單詞頻率列表,其中每個鍵是一個帶有要計數單詞的字符串,並且值是一個整數,每次找到該單詞的標記時,該值都會增加。

在Perl中,增加這樣的值非常容易:

$map{$word}++;

但是在Java中,它要復雜得多。 這是我目前的操作方式:

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

當然,哪個依賴於較新的Java版本中的自動裝箱功能。 我想知道您是否可以建議一種更有效的遞增此值的方法。 避開Collections框架並改用其他東西,甚至有良好的性能原因嗎?

更新:我已經對幾個答案進行了測試。 見下文。

有幾種方法:

  1. 使用 Bag 算法,如 Google Collections 中包含的集合。

  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 非常優雅,因為袋算法正是您計算單詞時所需要的。

你確定這是瓶頸? 你做過性能分析嗎?

嘗試使用 NetBeans 分析器(它是免費的並內置於 NB 6.1)來查看熱點。

最后,JVM 升級(比如從 1.5->1.6)通常是一種廉價的性能助推器。 即使是內部版本號的升級也可以提供良好的性能提升。 如果您在 Windows 上運行並且這是一個服務器類應用程序,請在命令行上使用 -server 以使用服務器熱點 JVM。 在 Linux 和 Solaris 機器上,這是自動檢測到的。

“放置”需要“獲取”(以確保沒有重復的鍵)。
所以直接做一個“put”,
如果有一個以前的值,那么做一個加法:

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
}

如果計數從 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
  }
}

如果您使用Eclipse Collections ,則可以使用HashBag 就內存使用而言,這將是最有效的方法,並且在執行速度方面也將表現良好。

HashBag由支持MutableObjectIntMap存儲原始的整數,而不是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 的提交者。

我不知道它的效率如何,但下面的代碼也能工作。你需要在開始時定義一個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

各種原始包裝器,例如Integer是不可變的,因此除非您可以使用AtomicLong 之類的東西來完成您的要求,否則確實沒有更簡潔的方法來完成您的要求。 我可以在一分鍾內試一試並更新。 順便說一句, HashtableCollections Framework的一部分。

我會使用 Apache Collections Lazy Map(將值初始化為 0)並使用來自 Apache Lang 的 MutableIntegers 作為該映射中的值。

最大的成本是必須在您的方法中兩次搜索地圖。 在我的,你只需要做一次。 只需獲取值(如果不存在,它將被初始化)並增加它。

我希望這個問題對於本論壇來說不是太基本了,但是我們會看到的。 我想知道如何重構一些代碼以獲得更好的性能,而這些性能已經運行了很多次。

假設我正在使用地圖(可能是HashMap)創建一個單詞頻率列表,其中每個鍵是一個帶有要計數單詞的字符串,並且值是一個整數,每次找到該單詞的標記時,該值都會增加。

在Perl中,增加這樣的值非常容易:

$map{$word}++;

但是在Java中,它要復雜得多。 這是我目前的操作方式:

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

當然,哪個依賴於較新的Java版本中的自動裝箱功能。 我想知道您是否可以建議一種更有效的遞增此值的方法。 避開Collections框架並改用其他東西,甚至有良好的性能原因嗎?

更新:我已經對幾個答案進行了測試。 見下文。

Functional Java庫的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”。

使用流和getOrDefault計數:

String s = "abcdeff";
s.chars().mapToObj(c -> (char) c)
 .forEach(c -> {
     int count = countMap.getOrDefault(c, 0) + 1;
     countMap.put(c, count);
  });

由於很多人在 Java 主題中搜索 Groovy 的答案,以下是您在 Groovy 中的操作方法:

dev map = new HashMap<String, Integer>()
map.put("key1", 3)

map.merge("key1", 1) {a, b -> a + b}
map.merge("key2", 1) {a, b -> a + b}

希望我能正確理解你的問題,我是從 Python 來到 Java 的,所以我可以理解你的掙扎。

如果你有

map.put(key, 1)

你會做

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

希望這可以幫助!

java 8中的簡單方法如下:

final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.computeIfAbsent("foo", key -> new AtomicLong(0)).incrementAndGet();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM