簡體   English   中英

使用標准 Java HashMap(與 Trove THashMap 相比)會導致非 HashMap 代碼運行速度較慢

[英]Using standard Java HashMap (compared to Trove THashMap) causes non-HashMap code to run slower

我使用 HashMap 緩存通過遞歸算法計算的大約 200 萬個值。 我使用 Collections Framework 中的HashMap<Integer, Double>或 Trove 庫中的TIntDoubleHashMap ,由boolean useTrove變量控制,如下面的代碼所示。

我確實希望 Trove 庫更快,因為它避免了自動裝箱等。實際上, THashMapput()get()調用需要大約 300 毫秒才能運行(總共),而HashMap<>需要大約 500 毫秒HashMap<> .

現在,使用THashMap時我的整體程序運行時間約為 2.8 THashMap ,使用HashMap<>時約為 6.7 THashMap 這種差異不能僅用put()get()調用增加的運行時間來解釋。

  1. 我懷疑使用HashMap<>大幅增加運行時間是由於這種實現的內存效率非常低,因為每個 int/double 都需要裝箱到一個對象中,而這種增加的內存使用會導致程序其他部分的緩存未命中。 這個解釋有意義嗎,我怎么能確認/拒絕這個假設?

  2. 一般來說,我如何探索此類場景的算法優化? 分析算法並不容易指出HashMap<>是罪魁禍首,至少在僅考慮 CPU 時間的情況下。 是否只是提前知道需要為內存飢渴的程序優先使用內存的問題?

完整代碼如下。

import java.util.HashMap;
import gnu.trove.map.hash.TIntDoubleHashMap;

class RuntimeStopWatch {
    long elapsedTime;
    long startTime;
    RuntimeStopWatch() { reset(); }
    void reset() { elapsedTime = 0; }
    void start() { startTime = System.nanoTime(); }
    void stop() {
        long endTime = System.nanoTime();
        elapsedTime += (endTime - startTime);
        startTime = endTime;
    }
    void printElapsedTime(String prefix) {
        System.out.format(prefix + "%dms\n", elapsedTime / 1000000);
    }
}

public class HashMapBehaviour {

    static RuntimeStopWatch programTime = new RuntimeStopWatch();
    static RuntimeStopWatch hashMapTime = new RuntimeStopWatch();
    static HashMap<Integer, Double> javaHashMapCache;
    static TIntDoubleHashMap troveHashMapCache;
    static boolean useTrove;

    public static void main(String[] args) {
//        useTrove = true;
        useTrove = false;

        javaHashMapCache = new HashMap<>();
        troveHashMapCache = new TIntDoubleHashMap();

        programTime.start();
        recursiveFunction(29, 29, 178956970);
        programTime.stop();

        programTime.printElapsedTime("Program: ");
        hashMapTime.printElapsedTime("Hashmap: ");
    }


    static double recursiveFunction(int n, int k, int bitString) {
        if (k == 0) return 0.0;
        if (useTrove) {
            hashMapTime.start();
            if (troveHashMapCache.containsKey(bitString | (1 << n))) return troveHashMapCache.get(bitString | (1 << n));
            hashMapTime.stop();
        } else {
            hashMapTime.start();
            if (javaHashMapCache.containsKey(bitString | (1 << n))) return javaHashMapCache.get(bitString | (1 << n));
            hashMapTime.stop();
        }
        double result = 0.0;
        for (int i = 0; i < (n >> 1); i++) {
            double play1 = recursiveFunction(n - 1, k - 1, stripSingleBit(bitString, i));
            double play2 = recursiveFunction(n - 1, k - 1, stripSingleBit(bitString, n - i - 1));
            result += Math.max(play1, play2);
        }
        if (useTrove) {
            hashMapTime.start();
            troveHashMapCache.put(bitString | (1 << n), result);
            hashMapTime.stop();
        } else {
            hashMapTime.start();
            javaHashMapCache.put(bitString | (1 << n), result);
            hashMapTime.stop();
        }
        return result;
    }

    static int stripSingleBit(int bitString, int bitIndex) {
        return ((bitString >> (bitIndex + 1)) << bitIndex) | (bitString & ((1 << bitIndex) - 1));
    }
}

Trove 的一件大事是您需要預先確定集合的大小。 因為在 T*Maps 中存儲是基於單數組的,如果不能預先確定集合的大小,將會導致大量的數組創建和復制。 HashMap 沒有這個問題,因為它使用鏈接對象。

因此,總結:嘗試使用new TIntDoubleHashMap(<expected_size>)調整您的集合大小

在更大的范圍內,考慮您要優化的目標。 Trove 可以在整體內存使用和有時性能方面最有效。 然而,巨大的性能提升並不是來自超級時髦的散列算法,而是因為使用了更少的臨時對象(用於裝箱),因此可以減少 GC 壓力。 這對您是否重要完全取決於您的應用程序。 此外,負載因子允許您以查找速度為代價來權衡數組中的數據“密度”。 因此,調整可能很有用。 如果您在查找時遇到很多沖突並希望獲得更好的性能或希望以犧牲性能為代價最大化內存,請調整系數。

如果您有內存需要消耗並且只想要查找性能,那么 HashMap 很難被擊敗……尤其是如果地圖的內容是靜態的。 JVM 非常擅長優化掉臨時對象,所以不要太快忽略這一點。 (過早優化等...)

請記住,這種微型基准測試也不一定是真實世界性能的重要指標。 它錯過了諸如 GC 壓力和 JIT 編譯之類的東西。 JMH 之類的工具可以幫助編寫更具代表性的測試。

暫無
暫無

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

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