É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)
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
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)
Ce dernier est plus efficace que le premier. Un outil tel que FindBugs marquera le premier et vous suggérera de faire le dernier.
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.)
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".
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.