簡體   English   中英

Java:通過HashMap迭代,哪個效率更高?

[英]Java : Iteration through a HashMap, which is more efficient?

給定以下代碼,有兩種替代方法可以遍歷它,
這兩種方法之間有什么性能差異嗎?

        Map<String, Integer> map = new HashMap<String, Integer>();
        //populate map

        //alt. #1
        for (String key : map.keySet())
        {
            Integer value = map.get(key);
            //use key and value
        }

        //alt. #2
        for (Map.Entry<String, Integer> entry : map.entrySet())
        {
            String key = entry.getKey();
            Integer value = entry.getValue();
            //use key and value
        }

我傾向於認為alt. #2 alt. #2是遍歷整個map的更有效方法(但我可能是錯的)

您的第二個選項肯定更有效,因為與第一個選項中的 n 次相比,您只進行一次查找。

但是,沒有什么比盡可能嘗試更好的了。 所以這里 -

(不完美但足以驗證假設並在我的機器上)

public static void main(String args[]) {

    Map<String, Integer> map = new HashMap<String, Integer>();
    // populate map

    int mapSize = 500000;
    int strLength = 5;
    for(int i=0;i<mapSize;i++)
        map.put(RandomStringUtils.random(strLength), RandomUtils.nextInt());

    long start = System.currentTimeMillis();
    // alt. #1
    for (String key : map.keySet()) {
        Integer value = map.get(key);
        // use key and value
    }
    System.out.println("Alt #1 took "+(System.currentTimeMillis()-start)+" ms");

    start = System.currentTimeMillis();
    // alt. #2
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        String key = entry.getKey();
        Integer value = entry.getValue();
        // use key and value
    }
    System.out.println("Alt #2 took "+(System.currentTimeMillis()-start)+" ms");
}

結果(一些有趣的)

使用int mapSize = 5000; int strLength = 5; int mapSize = 5000; int strLength = 5;
Alt #1 耗時 26 毫秒
Alt #2 耗時 20 毫秒

int mapSize = 50000; int strLength = 5; int mapSize = 50000; int strLength = 5;
Alt #1 耗時 32 毫秒
Alt #2 耗時 20 毫秒

int mapSize = 50000; int strLength = 50; int mapSize = 50000; int strLength = 50;
Alt #1 耗時 22 毫秒
Alt #2 耗時 21 毫秒

int mapSize = 50000; int strLength = 500; int mapSize = 50000; int strLength = 500;
Alt #1 耗時 28 毫秒
Alt #2 耗時 23 毫秒

int mapSize = 500000; int strLength = 5; int mapSize = 500000; int strLength = 5;
Alt #1 耗時 92 毫秒
Alt #2 耗時 57 毫秒

...等等

第二個片段會稍微快一些,因為它不需要重新查找密鑰。

所有HashMap迭代器都調用nextEntry方法,該方法返回一個Entry<K,V>

您的第一個片段丟棄條目中的值(在KeyIterator中),然后在字典中再次查找它。

您的第二個片段直接使用鍵和值(來自EntryIterator

keySet()entrySet()都是廉價調用)

Map:

Map<String, Integer> map = new HashMap<String, Integer>();

除了 2 個選項之外,還有一個選項。

1) keySet() - 如果您需要使用,請使用它

for ( String k : map.keySet() ) {
    ...
}

2) entrySet() - 如果你需要兩者都使用它:鍵和值

for ( Map.Entry<String, Integer> entry : map.entrySet() ) {
    String k = entry.getKey();
    Integer v = entry.getValue();
    ...
}

3) values() - 如果你需要就使用它

for ( Integer v : map.values() ) {
    ...
}

后者比前者效率更高。 FindBugs之類的工具實際上會標記前者並建議您執行后者。

布吉茲,

我認為(我不知道)迭代EntrySet(替代方案2)效率略高,僅僅是因為它不是hash每個鍵以獲得它的價值......話雖如此,計算hash是一個O (1) 每個條目的操作,因此我們只在整個HashMap上談論 O(n) ...但請注意,所有這些僅適用於HashMap ... Map的其他實現可能具有不同的性能特征。

我確實認為你會“推動它”來真正注意到性能上的差異。 如果您擔心,那么為什么不設置一個測試用例來計時這兩種迭代技術呢?

如果您沒有真正的、報告的性能問題,那么您真的擔心的不是很多……這里和那里的幾個時鍾滴答聲不會影響程序的整體可用性。

我相信代碼的許多其他方面通常比直接性能更重要。 當然,有些塊是“性能關鍵的”,這在編寫之前就已經知道了,更不用說性能測試了……但這種情況相當罕見。 作為一種通用方法,最好專注於編寫完整、正確、靈活、可測試、可重用、可讀、可維護的代碼……性能可以在以后根據需要構建。

版本 0 應該盡可能簡單,沒有任何“優化”。

一般來說,對於 HashMap,第二個會快一些。 僅當您有很多 hash 沖突時才真正重要,因為此后get(key)調用變得比O(1)慢 - 它得到O(k) ,其中k是同一存儲桶中的條目數(即數字具有相同 hash 代碼或不同 hash 代碼的密鑰仍然映射到同一個存儲桶 - 這也取決於 map 的容量、大小和負載因子)

入口迭代變體不必進行查找,因此它在這里變得更快一些。

另一個注意事項:如果您的 map 的容量比實際大小大很多並且您經常使用迭代,您可以考慮使用 LinkedHashMap 代替。 它為完整的迭代(以及可預測的迭代順序)提供O(size)而不是O(size+capacity)復雜度。 (您仍然應該衡量這是否真的帶來了改進,因為這些因素可能會有所不同。LinkedHashMap 創建 map 的開銷更大。)

最有效的方法(根據我的基准)是使用在 Java 8 或HashMap.entrySet().forEach()中添加的新HashMap.forEach()方法。

JMH 基准:

@Param({"50", "500", "5000", "50000", "500000"})
int limit;
HashMap<String, Integer> m = new HashMap<>();
public Test() {
}
@Setup(Level.Trial)
public void setup(){
    m = new HashMap<>(m);
    for(int i = 0; i < limit; i++){
        m.put(i + "", i);
    }
}
int i;
@Benchmark
public int forEach(Blackhole b){
    i = 0;
    m.forEach((k, v) -> { i += k.length() + v; });
    return i;
}
@Benchmark
public int keys(Blackhole b){
    i = 0;
    for(String key : m.keySet()){ i += key.length() + m.get(key); }
    return i;
}
@Benchmark
public int entries(Blackhole b){
    i = 0;
    for (Map.Entry<String, Integer> entry : m.entrySet()){ i += entry.getKey().length() + entry.getValue(); }
    return i;
}
@Benchmark
public int keysForEach(Blackhole b){
    i = 0;
    m.keySet().forEach(key -> { i += key.length() + m.get(key); });
    return i;
}
@Benchmark
public int entriesForEach(Blackhole b){
    i = 0;
    m.entrySet().forEach(entry -> { i += entry.getKey().length() + entry.getValue(); });
    return i;
}
public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
            .include(Test.class.getSimpleName())
            .forks(1)
            .warmupIterations(25)
            .measurementIterations(25)
            .measurementTime(TimeValue.milliseconds(1000))
            .warmupTime(TimeValue.milliseconds(1000))
            .timeUnit(TimeUnit.MICROSECONDS)
            .mode(Mode.AverageTime)
            .build();
    new Runner(opt).run();
}

結果:

Benchmark            (limit)  Mode  Cnt      Score    Error  Units
Test.entries              50  avgt   25      0.282 ±  0.037  us/op
Test.entries             500  avgt   25      2.792 ±  0.080  us/op
Test.entries            5000  avgt   25     29.986 ±  0.256  us/op
Test.entries           50000  avgt   25   1070.218 ±  5.230  us/op
Test.entries          500000  avgt   25   8625.096 ± 24.621  us/op
Test.entriesForEach       50  avgt   25      0.261 ±  0.008  us/op
Test.entriesForEach      500  avgt   25      2.891 ±  0.007  us/op
Test.entriesForEach     5000  avgt   25     31.667 ±  1.404  us/op
Test.entriesForEach    50000  avgt   25    664.416 ±  6.149  us/op
Test.entriesForEach   500000  avgt   25   5337.642 ± 91.186  us/op
Test.forEach              50  avgt   25      0.286 ±  0.001  us/op
Test.forEach             500  avgt   25      2.847 ±  0.009  us/op
Test.forEach            5000  avgt   25     30.923 ±  0.140  us/op
Test.forEach           50000  avgt   25    670.322 ±  7.532  us/op
Test.forEach          500000  avgt   25   5450.093 ± 62.384  us/op
Test.keys                 50  avgt   25      0.453 ±  0.003  us/op
Test.keys                500  avgt   25      5.045 ±  0.060  us/op
Test.keys               5000  avgt   25     58.485 ±  3.687  us/op
Test.keys              50000  avgt   25   1504.207 ± 87.955  us/op
Test.keys             500000  avgt   25  10452.425 ± 28.641  us/op
Test.keysForEach          50  avgt   25      0.567 ±  0.025  us/op
Test.keysForEach         500  avgt   25      5.743 ±  0.054  us/op
Test.keysForEach        5000  avgt   25     61.234 ±  0.171  us/op
Test.keysForEach       50000  avgt   25   1142.416 ±  3.494  us/op
Test.keysForEach      500000  avgt   25   8622.734 ± 40.842  us/op

如您所見, HashMap.forEachHashMap.entrySet().forEach()在大地圖上表現最佳,並通過entrySet()上的 for 循環加入,在小地圖上表現最佳。

鍵方法較慢的原因可能是因為它們必須為每個條目再次查找值,而其他方法只需要讀取 object 中的字段,它們已經必須獲取值。 我希望迭代器方法更慢的原因是它們正在進行外部迭代,這需要對每個元素進行兩次方法調用( hasNextnext ),並將迭代 state 存儲在迭代器 object 中,而內部迭代完成通過forEach只需要一個方法調用就可以accept

您應該使用目標數據在目標硬件上進行概要分析,並在循環中執行目標操作以獲得更准確的結果。

暫無
暫無

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

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