web-dev-qa-db-fra.com

Java: Itération à travers une HashMap, qui est plus efficace

Étant donné le code suivant, avec deux manières alternatives de le parcourir,
Existe-t-il une différence de performance entre ces deux méthodes?

        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
        }

J'ai tendance à penser que alt. #2 est le moyen le plus efficace de parcourir l'ensemble map (mais je peux me tromper)

56
bguiz

Votre deuxième option est nettement plus efficace puisque vous effectuez une recherche une seule fois par rapport à n fois dans la première option.

Mais rien ne colle mieux que d'essayer quand vous le pouvez. Alors voilà -

(Pas parfait mais assez bon pour vérifier les hypothèses et sur ma machine quand même)

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

R&EACUTE;SULTATS(Quelques intéressants)

Avec int mapSize = 5000; int strLength = 5;
Alt # 1 a pris 26 ms
Alt # 2 a pris 20 ms

Avec int mapSize = 50000; int strLength = 5;
Alt # 1 a pris 32 ms
Alt # 2 a pris 20 ms

Avec int mapSize = 50000; int strLength = 50;
Alt # 1 a pris 22 ms
Alt # 2 a pris 21 ms

Avec int mapSize = 50000; int strLength = 500;
Alt # 1 a pris 28 ms
Alt # 2 a pris 23 ms

Avec int mapSize = 500000; int strLength = 5;
Alt # 1 a pris 92 ms
Alt # 2 a pris 57 ms

...etc

57
Amol Katdare

Le deuxième extrait sera légèrement plus rapide, car il n'aura pas besoin de rechercher les clés.

Tous les itérateurs HashMap appellent la méthode nextEntry , qui renvoie un Entry<K,V>

Votre premier extrait supprime la valeur de l'entrée (dans KeyIterator ), puis le recherche à nouveau dans le dictionnaire.

Votre deuxième extrait utilise directement la clé et la valeur (de EntryIterator )

(Les deux keySet() et entrySet() sont des appels économiques)

10
SLaks

Ce dernier est plus efficace que le premier. Un outil tel que FindBugs marquera le premier et vous suggérera de faire le dernier.

5
Jonas Kongslund

En général, le second serait un peu plus rapide pour un HashMap. Cela n’aura vraiment d’importance que si vous avez beaucoup de collisions de hachage. Depuis lors, l’appel get(key) devient plus lent que O(1) - il devient O(k) avec k étant le nombre d’entrées dans le même compartiment (c'est-à-dire le nombre de clés avec le même code de hachage ou un code de hachage différent qui est toujours mappé vers le même seau - cela dépend également de la capacité, de la taille et du facteur de charge de la carte).

La variante Entry-iterating n'a pas à effectuer la recherche, elle est donc un peu plus rapide ici.

Autre remarque: si la capacité de votre carte est beaucoup plus grande que la taille réelle et que vous utilisez beaucoup d'itérations, vous pouvez envisager d'utiliser LinkedHashMap à la place. Il fournit O(size) à la place O(size+capacity) une complexité pour une itération complète (ainsi qu'un ordre d'itération prévisible). (Vous devez quand même mesurer si cela donne vraiment une amélioration, car les facteurs peuvent varier. LinkedHashMap a une charge plus importante pour la création de la carte.)

2
Paŭlo Ebermann

bguiz,

Je pense (je ne sais pas) qu'itérer EntrySet (variante 2) est légèrement plus efficace, tout simplement parce que chaque clé n'est pas hachée pour obtenir sa valeur ... Cela dit, le calcul du hachage est un O(1) opération par entrée, et par conséquent, nous ne parlons QUE O(n) de l'ensemble de la variable HashMap... mais notez que tout cela s'applique à HashMap uniquement ... d'autres implémentations de Map peuvent avoir des caractéristiques de performance TRES différentes.

Je pense que vous "insisteriez" pour AVISER la différence de performances. Si vous êtes concerné, pourquoi ne pas configurer un cas de test pour chronométrer les deux techniques d'itération?

Si vous ne rencontrez pas de problème de performance réel ni signalé, vous vous inquiétez vraiment de peu… Quelques coups d'horloge ici et là n'affecteront pas la facilité d'utilisation globale de votre programme. 

Je pense que de nombreux autres aspects du code sont généralement plus importants que la performance pure et simple. Bien sûr, certains blocs sont «critiques en termes de performances», et cela est connu AVANT que cela soit même écrit, que les performances soient testées de manière autonome… mais de tels cas sont assez rares. En règle générale, il est préférable de se concentrer sur l'écriture de code complet, correct, flexible, testable, réutilisable, lisible, maintenable ... Les performances peuvent être intégrées ultérieurement, si besoin est. 

La version 0 devrait être aussi simple que possible, sans aucune "optimisation".

2
corlettk

Les méthodes les plus efficaces (selon mon critère de référence) consistent à utiliser la nouvelle méthode HashMap.forEach() ajoutée à Java 8 ou HashMap.entrySet().forEach().

Indice de référence 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();
}

Résultats:

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

Comme vous pouvez le constater, HashMap.forEach et HashMap.entrySet().forEach() donnent les meilleurs résultats pour les grandes cartes et sont reliés par la boucle for de la entrySet() pour obtenir les meilleures performances sur les petites cartes. 

La raison pour laquelle les méthodes de clé sont plus lentes est probablement due au fait qu'elles doivent à nouveau rechercher la valeur pour chaque entrée, alors que les autres méthodes doivent simplement lire un champ dans un objet dont elles disposent déjà pour obtenir la valeur. La raison pour laquelle je m'attendrais à ce que les méthodes d'itérateur soient plus lentes est qu'elles effectuent une itération externe, ce qui nécessite deux appels de méthode (hasNext et next) pour chaque élément, tout en stockant l'état de l'itération dans l'objet itérateur by forEach nécessite un seul appel de méthode à accept.

Vous devez profiler sur votre matériel cible vos données cible et effectuer votre action cible dans les boucles pour obtenir un résultat plus précis.

1
Alex