web-dev-qa-db-ger.com

Java: Iteration durch eine HashMap, die effizienter ist?

In Anbetracht des folgenden Codes mit zwei alternativen Möglichkeiten, ihn zu durchlaufen,
Gibt es einen Leistungsunterschied zwischen diesen beiden Methoden?

        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
        }

Ich bin geneigt zu denken, dass alt. #2 das effizientere Mittel ist, um die gesamte map zu durchlaufen (aber ich könnte mich irren)

56
bguiz

Ihre zweite Option ist definitiv effizienter, da Sie nur einmal nachschlagen, verglichen mit der Anzahl n der ersten Option.

Aber nichts bleibt besser, als es auszuprobieren, wenn Sie können. Also hier geht es -

(Nicht perfekt, aber gut genug, um Annahmen und auf meiner Maschine trotzdem zu überprüfen)

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");
}

ERGEBNISSE(Einige interessante)

Mit int mapSize = 5000; int strLength = 5;
Alt # 1 nahm 26 ms in Anspruch
Alt # 2 nahm 20 ms in Anspruch

Mit int mapSize = 50000; int strLength = 5;
Alt # 1 nahm 32 ms in Anspruch
Alt # 2 nahm 20 ms in Anspruch

Mit int mapSize = 50000; int strLength = 50;
Alt # 1 nahm 22 ms in Anspruch
Alt # 2 nahm 21 ms in Anspruch

Mit int mapSize = 50000; int strLength = 500;
Alt # 1 nahm 28 ms in Anspruch
Alt # 2 dauerte 23 ms

Mit int mapSize = 500000; int strLength = 5;
Alt # 1 nahm 92 ms in Anspruch
Alt # 2 nahm 57 ms in Anspruch

...und so weiter

57
Amol Katdare

Das zweite Snippet wird etwas schneller sein, da die Tasten nicht erneut gesucht werden müssen.

Alle HashMap-Iteratoren rufen die nextEntry-Methode auf, die einen Entry<K,V> zurückgibt. 

Ihr erstes Snippet verwirft den Wert aus dem Eintrag (in KeyIterator ) und sucht ihn dann erneut im Wörterbuch nach.

Ihr zweites Snippet verwendet den Schlüssel und den Wert direkt (von EntryIterator )

(Beide keySet() und entrySet() sind billige Anrufe)

10
SLaks

Letzteres ist effizienter als das erstere. Ein Tool wie FindBugs wird das Erstere kennzeichnen und Ihnen vorschlagen, das Letztere zu tun.

5
Jonas Kongslund

Karte:

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

Neben den 2 Optionen gibt es noch eine.

1) keySet () - Verwenden Sie diese Option, wenn Sie nur die Tasten verwenden müssen

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

2) entrySet () - benutze es, wenn du beides brauchst: Schlüssel & Werte

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

3) values ​​() - benutze es, wenn du nur das values brauchst

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

Im Allgemeinen wäre die zweite für eine HashMap etwas schneller. Es ist nur wirklich wichtig, wenn Sie viele Hash-Kollisionen haben, da der Aufruf get(key) langsamer wird als O(1) - er erhält O(k), wobei k die Anzahl der Einträge im selben Bucket ist (dh die Anzahl der Schlüssel mit demselben Hash-Code oder einem anderen Hash-Code, der immer noch demselben Bucket zugeordnet wird (dies hängt auch von der Kapazität, Größe und dem Lastfaktor der Karte ab).

Die Entry-iterating-Variante muss nicht nachschlagen, daher wird es hier etwas schneller.

Noch ein Hinweis: Wenn die Kapazität Ihrer Karte viel größer ist als die tatsächliche Größe und Sie häufig Iterationen verwenden, sollten Sie stattdessen LinkedHashMap verwenden. Es bietet O(size) statt O(size+capacity) Komplexität für eine vollständige Iteration (sowie eine vorhersagbare Iterationsreihenfolge). (Sie sollten immer noch messen, ob dies tatsächlich zu einer Verbesserung führt, da die Faktoren variieren können. LinkedHashMap hat einen größeren Aufwand für die Erstellung der Karte.)

2
Paŭlo Ebermann

bguiz

Ich denke (ich weiß es nicht), dass das Durchlaufen des EntrySets (Alternative 2) etwas effizienter ist, einfach weil es nicht jeden Key hash, um seinen Wert zu erhalten ... Nachdem man das gesagt hat, ist die Berechnung des Hashes ein O(1) Operation pro Eintrag, und deshalb sprechen wir NUR O(n) über die gesamte HashMap.... Beachten Sie jedoch, dass dies alles nur für HashMap gilt ... andere Implementierungen von Map können haben sehr unterschiedliche Leistungsmerkmale.

Ich denke, Sie würden es "drängen", den Leistungsunterschied tatsächlich zu bemerken. Wenn Sie besorgt sind, können Sie einen Testfall einrichten, um beide Iterationstechniken zu testen.

Wenn Sie kein echtes, gemeldetes Leistungsproblem haben, dann machen Sie sich wirklich keine Sorgen ... Ein paar Zeittakte hier und dort haben keinen Einfluss auf die allgemeine Verwendbarkeit Ihres Programms. 

Ich glaube, dass viele, viele andere Aspekte des Codes in der Regel wichtiger sind als die absolute Leistung. Natürlich sind einige Blöcke "leistungskritisch", und das ist bekannt, BEVOR es sogar geschrieben wird, wenn die Leistung allein getestet wird ... aber solche Fälle sind ziemlich selten. Generell ist es besser, sich auf das Schreiben von vollständigem, korrektem, flexiblem, überprüfbarem, wiederverwendbarem, lesbarem, wartbarem Code zu konzentrieren. Die Leistung kann später je nach Bedarf eingebaut werden. 

Version 0 sollte SO EINFACH WIE MÖGLICH sein, ohne "Optimierungen".

2
corlettk

Die effizienteste Methode (gemäß meinem Benchmark) ist die Verwendung der neuen HashMap.forEach() - Methode, die in Java 8 oder HashMap.entrySet().forEach() hinzugefügt wurde.

JMH-Benchmark:

@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();
}

Ergebnisse:

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

Wie Sie sehen, sind HashMap.forEach und HashMap.entrySet().forEach() am besten für große Karten geeignet und werden durch die for-Schleife der entrySet() für eine optimale Leistung auf kleinen Karten unterstützt. 

Der Grund dafür, dass die Schlüsselmethoden langsamer sind, liegt wahrscheinlich daran, dass sie den Wert für jeden Eintrag erneut suchen müssen, während die anderen Methoden nur ein Feld in einem Objekt lesen müssen, das sie bereits benötigen, um den Wert zu erhalten. Ich würde erwarten, dass die Iterator-Methoden langsamer sind, weil sie eine externe Iteration durchführen. Dies erfordert zwei Methodenaufrufe (hasNext und next) für jedes Element sowie das Speichern des Iterationsstatus im Iterator-Objekt, während die interne Iteration ausgeführt wird von forEach erfordert nur einen Methodenaufruf von accept.

Sie sollten auf Ihrer Zielhardware ein Profil mit Ihren Zieldaten erstellen und Ihre Zielaktion in den Schleifen durchführen, um ein genaueres Ergebnis zu erhalten.

1
Alex