簡體   English   中英

Java Collectors.toMap的內存優化

[英]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

值得注意的是,對於您當前的實施:

  1. 調整大小所消耗的大部分額外內存將在下一次GC運行時回收。
  2. collect完成后,您仍然可能最終得到一個主哈希數組,該數組最多可達2倍。 “浪費”的內存可能在表中每個條目最多8個字節,但平均每個條目將是4個字節。
  3. 即便如此,哈希條目節點將是HashMap最大的內存使用者。 除了用於表示鍵和值的空間外,每個條目大約消耗32個字節。

(以上數字假定為64位引用。)


作為替代方案,如果使用toMap()4參數重載toMap()可以提供Supplier來創建要填充的Map 這允許您執行以下操作:

  • 使用足夠大的初始容量來分配HashMap以避免調整大小,但不能太大。
  • 使用Map的(假設的)替代實現,每個條目使用的內存少於HashMap
  • 創建一個包裝器來填充類似地圖的對象,該對象不會為您的KV類型實現Map<K,V> .... (例如,您可以使用GNU Trove庫中的TLongObjectHashMap 。)

(在后兩種情況下,目標是找到一個Map或“類似地圖”的類,它使用較少的內存(對於您的KV類型)但仍具有適當的查找性能。)

總結其他人說的內容並添加一點,這是使用自定義Collector執行此操作的方法。 但是,你應該記住兩件事:

  1. 他的回答中繼續思考Stephen C ,在你發現它確實是你的應用程序中的性能瓶頸之前,你不應該真的擔心優化這些情況。 正如唐納德克努特所說,“過早優化是萬惡之源”。

  2. 正如shmosel在注釋中指出的那樣,如果所述Collector以並行模式使用,則分配具有預定義大小的HashMapCollector將過度分配。 因此,我建議的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.

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