[英]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方法是最慢的,所以我將給出每種方法的速度與該方法的速度進行比較。
看起來只有 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)
它能做什么:
更多信息在這里。
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);
這就是使用簡單代碼增加值的方法。
益處:
缺點:
理論上,一旦你調用 get(),你就已經知道把()放在哪里,所以你不必再次搜索。 但是在哈希映射中搜索通常只需要很少的時間,您可以忽略這個性能問題。
但是如果你對這個問題非常認真,你是一個完美主義者,另一種方法是使用合並方法,這(可能)比之前的代碼片段更有效,因為你將(理論上)只搜索一次地圖:(雖然這段代碼乍一看並不明顯,它簡短而高效)
map.merge(key, 1, (a,b) -> a+b);
建議:在大多數情況下,您應該關心代碼可讀性而不是性能提升。 如果第一個代碼片段對您來說更容易理解,請使用它。 但是,如果您能夠理解第二個罰款,那么您也可以繼續學習!
作為我自己評論的后續行動:Trove 看起來是要走的路。 如果出於某種原因,你想堅持使用標准的JDK, ConcurrentMap和AtomicLong的可以使代碼一點點更好,但情況因人而異。
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;
在地圖上包含兩個潛在的開銷較大的操作,即containsKey
和get
。 前者執行的操作可能與后者非常相似,因此您要做兩次相同的工作!
如果您查看 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框架並改用其他東西,甚至有良好的性能原因嗎?
更新:我已經對幾個答案進行了測試。 見下文。
有幾種方法:
使用 Bag 算法,如 Google Collections 中包含的集合。
創建可以在 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 之類的東西來完成您的要求,否則確實沒有更簡潔的方法來完成您的要求。 我可以在一分鍾內試一試並更新。 順便說一句, Hashtable是Collections 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.