J'espère que cette question n'est pas considérée comme trop basique pour ce forum, mais nous verrons. Je me demande comment refactoriser du code pour de meilleures performances qui s'exécutent plusieurs fois.
Supposons que je crée une liste de fréquence de mots, en utilisant une carte (probablement un HashMap), où chaque clé est une chaîne avec le mot qui est compté et la valeur est un entier qui est incrémenté chaque fois qu'un jeton du mot est trouvé.
En Perl, incrémenter une telle valeur serait trivialement facile:
$map{$word}++;
Mais en Java, c'est beaucoup plus compliqué. Voici la façon dont je le fais actuellement:
int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);
Ce qui, bien sûr, repose sur la fonction de mise en boîte automatique dans les nouvelles versions de Java. Je me demande si vous pouvez suggérer un moyen plus efficace d'augmenter une telle valeur. Y a-t-il même de bonnes raisons de performance pour éviter le framework Collections et utiliser quelque chose d'autre à la place?
Mise à jour: j'ai fait un test de plusieurs des réponses. Voir ci-dessous.
la source
Réponses:
Quelques résultats de tests
J'ai obtenu beaucoup de bonnes réponses à cette question - merci les gens - j'ai donc décidé d'exécuter des tests et de déterminer la méthode la plus rapide. Les cinq méthodes que j'ai testées sont les suivantes:
Méthode
Voici ce que j'ai fait ...
Résultats
Je vais d'abord présenter les résultats et le code ci-dessous pour ceux qui sont intéressés.
La méthode ContainsKey était, comme prévu, la plus lente, je vais donc donner la vitesse de chaque méthode par rapport à la vitesse de cette méthode.
Conclusions
Il semblerait que seules la méthode MutableInt et la méthode Trove soient significativement plus rapides, en ce qu'elles seules donnent une amélioration des performances de plus de 10%. Cependant, si le filetage est un problème, AtomicLong pourrait être plus attrayant que les autres (je ne suis pas vraiment sûr). J'ai également exécuté TestForNull avec des
final
variables, mais la différence était négligeable.Notez que je n'ai pas profilé l'utilisation de la mémoire dans les différents scénarios. Je serais heureux d'entendre tous ceux qui ont une bonne idée de la façon dont les méthodes MutableInt et Trove seraient susceptibles d'affecter l'utilisation de la mémoire.
Personnellement, je trouve la méthode MutableInt la plus intéressante, car elle ne nécessite aucun chargement de classes tierces. Donc, sauf si je découvre des problèmes, c'est la voie que je suis le plus susceptible d'aller.
Le code
Voici le code crucial de chaque méthode.
ContainsKey
TestForNull
AtomicLong
Trove
MutableInt
la source
HashMap
, pas uneConcurrentHashMap
, pour une comparaison égale. Ce devrait également être unAtomicInteger
, et non unAtomicLong
, pour une comparaison égale. --- De plus, anint[1]
serait une simple version intégrée deMutableInt
, ne nécessitant pas de nouvelle classe.freq.compute(word, (key, count) -> count == null ? 1 : count + 1)
? En interne, il fait une recherche moins hachée quecontainsKey
, il serait intéressant de voir comment il se compare aux autres, en raison de la lambda.Maintenant, il existe un moyen plus court avec Java 8
Map::merge
.Ce qu'il fait:
Plus d'informations ici .
la source
map.merge(key, 1, (a, b) -> a + b);
faitInteger::sum
comme BiFunction, et n'aimait pas que @russter réponde comme il était écrit. Cela a fonctionné pour moiMap.merge(key, 1, { a, b -> a + b})
Un peu de recherche en 2016: https://github.com/leventov/java-word-count , code source de référence
Meilleurs résultats par méthode (plus petit est meilleur):
Résultats temps \ espace:
la source
Google Guava est votre ami ...
... au moins dans certains cas. Ils ont cette belle AtomicLongMap . Particulièrement agréable parce que vous avez affaire à longue que la valeur de votre carte.
Par exemple
Il est également possible d'ajouter plus de 1 à la valeur:
la source
AtomicLongMap#getAndAdd
prend une primitivelong
et non la classe wrapper; ça ne sert à riennew Long()
. EtAtomicLongMap
est un type paramétré; vous auriez dû le déclarer commeAtomicLongMap<String>
.@Hank Gay
Pour faire suite à mon propre commentaire (plutôt inutile): Trove ressemble à la voie à suivre. Si, pour une raison quelconque, vous vouliez coller avec le JDK standard, ConcurrentMap et AtomicLong peuvent rendre le code un petit de plus agréable bits, bien que YMMV.
laissera
1
la valeur dans la carte pourfoo
. De façon réaliste, une convivialité accrue pour le filetage est tout ce que cette approche doit le recommander.la source
Et c'est ainsi que vous incrémentez une valeur avec du code simple.
Avantage:
Inconvénient:
Théoriquement, une fois que vous appelez get (), vous savez déjà où mettre (), vous ne devriez donc pas avoir à chercher à nouveau. Mais la recherche dans la carte de hachage prend généralement très peu de temps pour ignorer ce problème de performances.
Mais si vous êtes très sérieux à propos de ce problème, vous êtes un perfectionniste, une autre façon est d'utiliser la méthode de fusion, c'est (probablement) plus efficace que l'extrait de code précédent car vous ne chercherez (théoriquement) la carte qu'une seule fois: (bien que ce code n'est pas évident à première vue, il est court et performant)
Suggestion: la plupart du temps, vous devriez vous soucier de la lisibilité du code plutôt que de peu de gain de performances. Si le premier extrait de code est plus facile à comprendre, utilisez-le. Mais si vous êtes en mesure de bien comprendre le 2e, alors vous pouvez aussi y aller!
la source
C'est toujours une bonne idée de consulter la bibliothèque Google Collections pour ce genre de chose. Dans ce cas, un multiset fera l'affaire:
Il existe des méthodes de type carte pour itérer sur les clés / entrées, etc. En interne, l'implémentation utilise actuellement un
HashMap<E, AtomicInteger>
, donc vous n'encourrez pas de frais de boxe.la source
count()
méthode sur un multiset s'exécute-t-elle en temps O (1) ou O (n) (pire cas)? Les documents ne sont pas clairs sur ce point.Vous devez être conscient du fait que votre tentative initiale
contient deux opérations potentiellement coûteuses sur une carte, à savoir
containsKey
etget
. Le premier effectue une opération potentiellement assez similaire au second, vous effectuez donc deux fois le même travail !Si vous regardez l'API pour Map, les
get
opérations retournent généralementnull
lorsque la map ne contient pas l'élément demandé.Notez que cela fera une solution comme
dangereux, car il pourrait entraîner l'
NullPointerException
al. Vous devriez vérifier pour unenull
première.Notez également , et c'est très important, que
HashMap
s peut contenirnulls
par définition. Donc, tous les retours nenull
disent pas "il n'y a pas un tel élément". À cet égard,containsKey
se comporte différemment deget
vous dire si existe un tel élément. Reportez-vous à l'API pour plus de détails.Pour votre cas, cependant, vous ne voudrez peut-être pas faire la distinction entre un
null
"noSuchElement" stocké. Si vous ne voulez pas autoriser,null
vous préférerezHashtable
. L'utilisation d'une bibliothèque d'encapsuleurs comme cela a déjà été proposé dans d'autres réponses pourrait être une meilleure solution au traitement manuel, selon la complexité de votre application.Pour compléter la réponse (et j'ai oublié de le mettre dans un premier temps, grâce à la fonction d'édition!), La meilleure façon de le faire en mode natif, est de placer
get
dans unefinal
variable, de vérifiernull
et deput
le réintégrer avec un1
. La variable devrait êtrefinal
parce qu'elle est immuable de toute façon. Le compilateur n'a peut-être pas besoin de cet indice, mais c'est plus clair de cette façon.Si vous ne voulez pas vous fier à la mise en boîte automatique, vous devriez dire quelque chose comme à la
map.put(new Integer(1 + i.getValue()));
place.la source
Une autre façon serait de créer un entier mutable:
bien sûr, cela implique de créer un objet supplémentaire, mais la surcharge par rapport à la création d'un entier (même avec Integer.valueOf) ne devrait pas être tellement.
la source
Vous pouvez utiliser la méthode computeIfAbsent dans l'
Map
interface fournie dans Java 8 .La méthode
computeIfAbsent
vérifie si la clé spécifiée est déjà associée à une valeur ou non? S'il n'y a pas de valeur associée, il tente de calculer sa valeur en utilisant la fonction de mappage donnée. Dans tous les cas, il renvoie la valeur actuelle (existante ou calculée) associée à la clé spécifiée, ou null si la valeur calculée est nulle.Sur une note latérale si vous avez une situation où plusieurs threads mettent à jour une somme commune, vous pouvez jeter un œil à LongAdder LongAdder.En cas de forte contention, le débit attendu de cette classe est nettement supérieur à
AtomicLong
, au détriment d'une consommation d'espace plus élevée.la source
La rotation de la mémoire peut être un problème ici, car chaque boxing d'un int supérieur ou égal à 128 provoque une allocation d'objet (voir Integer.valueOf (int)). Bien que le garbage collector traite très efficacement les objets à vie courte, les performances en souffriront dans une certaine mesure.
Si vous savez que le nombre d'incréments effectués sera largement supérieur au nombre de clés (= mots dans ce cas), envisagez d'utiliser un titulaire int à la place. Phax a déjà présenté le code pour cela. Le voici à nouveau, avec deux changements (classe de support rendue statique et valeur initiale définie sur 1):
Si vous avez besoin de performances extrêmes, recherchez une implémentation de carte directement adaptée aux types de valeur primitifs. jrudolph a mentionné GNU Trove .
Soit dit en passant, un bon terme de recherche pour ce sujet est "histogramme".
la source
Au lieu d'appeler containsKey (), il est plus rapide d'appeler map.get et de vérifier si la valeur retournée est nulle ou non.
la source
Êtes-vous sûr qu'il s'agit d'un goulot d'étranglement? Avez-vous effectué une analyse des performances?
Essayez d'utiliser le profileur NetBeans (gratuit et intégré à NB 6.1) pour examiner les hotspots.
Enfin, une mise à niveau JVM (disons de 1.5 à> 1.6) est souvent un booster de performances bon marché. Même une mise à niveau du numéro de build peut fournir de bonnes améliorations de performances. Si vous exécutez sous Windows et qu'il s'agit d'une application de classe serveur, utilisez -server sur la ligne de commande pour utiliser la machine virtuelle Java Hotspot Server. Sur les machines Linux et Solaris, cela est détecté automatiquement.
la source
Il existe quelques approches:
Utilisez un alorithme de sac comme les ensembles contenus dans Google Collections.
Créez un conteneur modifiable que vous pouvez utiliser dans la carte:
Et utilisez put ("word", new My ("Word")); Ensuite, vous pouvez vérifier s'il existe et incrémenter lors de l'ajout.
Évitez de rouler votre propre solution à l'aide de listes, car si vous effectuez une recherche et un tri dans la boucle interne, vos performances seront nulles. La première solution HashMap est en fait assez rapide, mais une bonne comme celle trouvée dans Google Collections est probablement meilleure.
Compter les mots à l'aide de Google Collections ressemble à ceci:
L'utilisation de HashMultiset est assez élégante, car un algorithme de sac est exactement ce dont vous avez besoin pour compter les mots.
la source
Je pense que votre solution serait la voie standard, mais - comme vous l'avez noté vous-même - ce n'est probablement pas la voie la plus rapide possible.
Vous pouvez regarder GNU Trove . C'est une bibliothèque qui contient toutes sortes de collections primitives rapides. Votre exemple utiliserait un TObjectIntHashMap qui a une méthode adjustOrPutValue qui fait exactement ce que vous voulez.
la source
Une variante de l'approche MutableInt qui pourrait être encore plus rapide, si elle est un peu un hack, consiste à utiliser un tableau int à un seul élément:
Il serait intéressant que vous puissiez réexécuter vos tests de performances avec cette variante. Ce pourrait être le plus rapide.
Edit: Le modèle ci-dessus a bien fonctionné pour moi, mais j'ai finalement changé pour utiliser les collections de Trove pour réduire la taille de la mémoire dans certaines très grandes cartes que je créais - et en bonus, il était également plus rapide.
Une fonctionnalité vraiment intéressante est que la
TObjectIntHashMap
classe a un seuladjustOrPutValue
appel qui, selon qu'il existe déjà une valeur à cette clé, mettra une valeur initiale ou incrémentera la valeur existante. C'est parfait pour incrémenter:la source
Google Collections HashMultiset:
- assez élégant à utiliser
- mais consomme du CPU et de la mémoire
Le mieux serait d'avoir une méthode comme:
Entry<K,V> getOrPut(K);
(élégante et à faible coût)Une telle méthode ne calculera le hachage et l'index qu'une seule fois, puis nous pourrions faire ce que nous voulons avec l'entrée (remplacer ou mettre à jour la valeur).
Plus élégant:
- prenez un
HashSet<Entry>
- prolongez-le afin de
get(K)
mettre une nouvelle entrée si nécessaire- l'entrée pourrait être votre propre objet.
->
(new MyHashSet()).get(k).increment();
la source
Assez simple, utilisez simplement la fonction intégrée
Map.java
comme suitla source
++
... OMG, c'est si simple. @siegi++
ne fonctionne nulle part dans cette expression car une variable est nécessaire comme opérande mais il y a juste des valeurs. Votre ajout d'+ 1
œuvres cependant. Maintenant, votre solution est la même que dans la réponse off99555s ."put" nécessite "get" (pour garantir l'absence de clé en double).
Donc, faites directement un "put",
et s'il y avait une valeur précédente, faites un ajout:
Si le décompte commence à 0, ajoutez 1: (ou toute autre valeur ...)
Remarque: ce code n'est pas thread-safe. Utilisez-le pour créer puis utilisez la carte, pas pour la mettre à jour simultanément.
Optimisation: dans une boucle, conservez l'ancienne valeur pour devenir la nouvelle valeur de la boucle suivante.
la source
Les différents wrappers primitifs, par exemple,
Integer
sont immuables, il n'y a donc vraiment pas de manière plus concise de faire ce que vous demandez, sauf si vous pouvez le faire avec quelque chose comme AtomicLong . Je peux essayer ça dans une minute et mettre à jour. BTW, Hashtable fait partie du cadre des collections .la source
J'utiliserais Apache Collections Lazy Map (pour initialiser les valeurs à 0) et utiliser MutableIntegers d'Apache Lang comme valeurs dans cette carte.
Le plus gros coût est d'avoir à traquer la carte deux fois dans votre méthode. Dans le mien, vous ne devez le faire qu'une seule fois. Obtenez simplement la valeur (elle sera initialisée si elle est absente) et incrémentez-la.
la source
La structure de données de la bibliothèque Java fonctionnelle
TreeMap
a uneupdate
méthode dans la dernière tête de tronc:Exemple d'utilisation:
Ce programme imprime "2".
la source
@Vilmantas Baranauskas: En ce qui concerne cette réponse, je commenterais si j'avais les points de représentant, mais je n'en ai pas. Je voulais noter que la classe Counter définie il n'y a PAS de thread-safe car il ne suffit pas de synchroniser inc () sans synchroniser value (). Les autres threads appelant value () ne sont pas garantis pour voir la valeur sauf si une relation se produit avant a été établie avec la mise à jour.
la source
Je ne sais pas à quel point c'est efficace, mais le code ci-dessous fonctionne aussi. Vous devez définir un
BiFunction
au début. De plus, vous pouvez faire plus que simplement incrémenter avec cette méthode.la sortie est
la source
Si vous utilisez des collections Eclipse , vous pouvez utiliser a
HashBag
. Ce sera l'approche la plus efficace en termes d'utilisation de la mémoire et elle fonctionnera également bien en termes de vitesse d'exécution.HashBag
est soutenu par unMutableObjectIntMap
qui stocke les ints primitifs au lieu desCounter
objets. Cela réduit la surcharge de la mémoire et améliore la vitesse d'exécution.HashBag
fournit l'API dont vous auriez besoin car c'est unCollection
vous permet également de rechercher le nombre d'occurrences d'un élément.Voici un exemple tiré des collections Eclipse Kata .
Remarque: je suis un committer pour les collections Eclipse.
la source
Je suggère d'utiliser Java 8 Map :: compute (). Il considère également le cas où une clé n'existe pas.
la source
mymap.merge(key, 1, Integer::sum)
?Étant donné que de nombreuses personnes recherchent des réponses sur Groovy dans les rubriques Java, voici comment procéder dans Groovy:
la source
La manière simple et facile dans java 8 est la suivante:
la source
J'espère que je comprends bien votre question, je viens de Java depuis Python afin que je puisse comprendre votre lutte.
si tu as
vous feriez
J'espère que cela t'aides!
la source