[英]Memory Optimization of Java Collectors.toMap
我有一個將列表轉換為地圖的功能。 調用此函數后,映射的大小不會更改。 我正在嘗試在以下兩個實現之間做出決定:
Map<Long, Object> listToMap(List<Object> objs) {
/* Implementation One: */
Map<Long, Object> map = new HashMap<>(objs.size(), 1);
for (Object obj : objs) {
map.put(obj.getKey(), obj);
}
return map;
/* Implementation Two: */
return objs.stream().collect(Collectors.toMap(Object::getKey, obj -> obj));
}
在第一個實現中,我通過使用1的加載因子和列表的大小為所有元素分配了足夠的內存。 這確保不會執行調整大小操作。 然后,我遍歷列表並逐個添加元素。
在第二個實現中,我使用Java 8流來提高可讀性。
我的問題是:第二個實現是否會涉及HashMap的多個調整大小,還是已經過優化以分配足夠的內存?
第二個實現將涉及HashMap的多個調整大小。
我通過在調試器中運行它來確定這一點,並且每次調整哈希映射的大小時都會中斷。 首先,我調整了您發布的代碼,使其在我的系統上進行編譯:
import java.util.*;
import java.util.stream.*;
class Test {
public static void main(String[] args) {
List<Object> list = new ArrayList<Object>();
for(int i=0; i<100000; i++) {
list.add(new Integer(i));
}
new Test().listToMap(list);
}
Map<Integer, Object> listToMap(List<Object> objs) {
return objs.stream().collect(Collectors.toMap(Object::hashCode, obj -> obj));
}
}
然后我編譯它並在調試器中運行它直到它命中listToMap
:
$ javac Test.java && jdb Test
Initializing jdb ...
> stop in Test.listToMap
Deferring breakpoint Test.listToMap.
It will be set after the class is loaded.
> run
run Test
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Test.listToMap
Breakpoint hit: "thread=main", Test.listToMap(), line=14 bci=0
14 return objs.stream().collect(Collectors.toMap(Object::hashCode, obj -> obj));
main[1]
然后我在java.util.HashMap.resize
設置一個斷點並繼續:
main[1] stop in java.util.HashMap.resize
Set breakpoint java.util.HashMap.resize
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0
main[1]
並且在我感到無聊之前cont
下去:
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(),
line=678 bci=0
main[1] print size
size = 3073
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0
main[1] print size
size = 6145
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0
main[1] print size
size = 12289
所以是的:它肯定會不斷調整大小。
第二個實現是否涉及HashMap的多個調整大小,還是已經過優化以分配足夠的內存?
在你的代碼中,前者。 請參閱https://stackoverflow.com/a/51333961/139985
值得注意的是,對於您當前的實施:
collect
完成后,您仍然可能最終得到一個主哈希數組,該數組最多可達2倍。 “浪費”的內存可能在表中每個條目最多8個字節,但平均每個條目將是4個字節。 HashMap
最大的內存使用者。 除了用於表示鍵和值的空間外,每個條目大約消耗32個字節。 (以上數字假定為64位引用。)
作為替代方案,如果使用toMap()
的4參數重載 , toMap()
可以提供Supplier
來創建要填充的Map
。 這允許您執行以下操作:
HashMap
以避免調整大小,但不能太大。 Map
的(假設的)替代實現,每個條目使用的內存少於HashMap
。 K
和V
類型實現Map<K,V>
.... (例如,您可以使用GNU Trove庫中的TLongObjectHashMap
。) (在后兩種情況下,目標是找到一個Map
或“類似地圖”的類,它使用較少的內存(對於您的K
和V
類型)但仍具有適當的查找性能。)
總結其他人說的內容並添加一點,這是使用自定義Collector
執行此操作的方法。 但是,你應該記住兩件事:
從他的回答中繼續思考Stephen C ,在你發現它確實是你的應用程序中的性能瓶頸之前,你不應該真的擔心優化這些情況。 正如唐納德克努特所說,“過早優化是萬惡之源”。
正如shmosel在注釋中指出的那樣,如果所述Collector
以並行模式使用,則分配具有預定義大小的HashMap
的Collector
將過度分配。 因此,我建議的Collector
不支持並行收集。
話雖如此,您可以編寫以下通用Collector
:
public class ExtraCollectors {
public static <T, K, V> Collector<T, ?, HashMap<K, V>> toSizedMap(
Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends V> valueMapper, int size) {
return toSequentialMap(
() -> com.google.common.collect.Maps.newHashMapWithExpectedSize(size),
keyMapper, valueMapper, Collector.Characteristics.UNORDERED
);
}
public static <T, K, V, M extends Map<K, V>> Collector<T, ?, M> toSequentialMap(
Supplier<M> mapSupplier, Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper, Collector.Characteristics... characteristics) {
return Collector.of(
mapSupplier,
(map, element) -> map.merge(
keyMapper.apply(element), valueMapper.apply(element), ExtraCollectors::mergeUnsupported
),
ExtraCollectors::combineUnsupported,
characteristics
);
}
private static <T> T mergeUnsupported(T valueA, T valueB) {
throw new UnsupportedOperationException("This Collector does not support merging.");
}
private static <A> A combineUnsupported(A accumulatorA, A accumulatorB) {
throw new UnsupportedOperationException("This Collector does not support parallel streams.");
}
}
請注意,我使用了Guava的Maps.newHashMapWithExpectedSize,因此您可以獲得一個具有所需大小的HashMap
(它大致與Andreas在您對您的問題的評論中所解釋的一樣)。 如果您沒有對Guava的依賴(並且不想擁有),您可以將Maps.capacity方法復制到您的代碼庫中。
使用上面定義的ExtraCollectors.toSizedMap()
方法,您的轉換方法如下所示:
Map<Long, KeyedObject> listToMap(List<? extends KeyedObject> objs) {
return objs.stream().collect(ExtraCollectors.toSizedMap(KeyedObject::getKey, obj -> obj, objs.size()));
}
盡管如此,如果您真的想要最高性能(以可重用性為代價),您可以完全跳過Stream
API,並使用Maps.newHashMapWithExpectedSize
應用您的解決方案1以獲得HashMap
的大小。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.