web-dev-qa-db-fra.com

Le moyen le plus efficace d’incrémenter une valeur de carte dans Java

J'espère que cette question n'est pas considérée comme trop fondamentale pour ce forum, mais nous verrons. Je me demande comment refactoriser du code pour obtenir de meilleures performances, qui s'exécute plusieurs fois.

Supposons que je crée une liste de fréquence Word, en utilisant une carte (probablement une 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, augmenter une telle valeur serait trivialement facile:

$map{$Word}++;

Mais en Java, c'est beaucoup plus compliqué. Voici comment 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 fonctionnalité de sélection automatique dans les nouvelles versions Java. Je me demande si vous pouvez suggérer un moyen plus efficace d’augmenter une telle valeur. Existe-t-il même de bonnes raisons de performance pour éviter le framework Collections et utiliser quelque chose d'autre?

Mise à jour: j'ai testé plusieurs des réponses. Voir ci-dessous.

338
gregory

Quelques résultats de test

J'ai eu beaucoup de bonnes réponses à cette question - merci les gens - alors j'ai décidé de faire quelques tests et de déterminer quelle méthode est réellement la plus rapide. Les cinq méthodes que j'ai testées sont les suivantes:

  • la méthode "ContainsKey" que j'ai présentée dans la question
  • la méthode "TestForNull" proposée par Aleksandar Dimitrov
  • la méthode "AtomicLong" proposée par Hank Gay
  • la méthode "Trove" proposée par jrudolph
  • la méthode "MutableInt" proposée par phax.myopenid.com

Méthode

Voici ce que j'ai fait ...

  1. créé cinq classes identiques, à l’exception des différences ci-dessous. Chaque classe devait effectuer une opération typique du scénario que j'ai présenté: ouvrir un fichier de 10 Mo et le lire, puis effectuer un décompte de fréquence de tous les jetons Word du fichier. Comme cela ne prenait en moyenne que 3 secondes, je l’ai demandé d’effectuer le comptage de fréquence (et non les E/S) 10 fois.
  2. chronométré la boucle de 10 itérations mais et non l'opération I/O et enregistre le temps total pris (en secondes d'horloge) en utilisant essentiellement la méthode de Ian Darwin dans le Java Cookbook .
  3. effectué les cinq tests en série, puis trois autres fois.
  4. en moyenne les quatre résultats pour chaque méthode.

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 vous donner la vitesse de chaque méthode par rapport à la vitesse de cette méthode.

  • ContainsKey: 30,654 secondes (ligne de base)
  • AtomicLong: 29,780 secondes (1,03 fois plus rapide)
  • TestForNull: 28,804 secondes (1,06 fois plus rapide)
  • Trove: 26.313 secondes (1.16 fois plus rapide)
  • MutableInt: 25,747 secondes (1,19 fois plus rapide)

Conclusions

Il semblerait que seules les méthodes MutableInt et Trove soient nettement plus rapides, en ce sens qu’elles donnent un gain de performances supérieur à 10%. Cependant, si le filetage pose problème, AtomicLong pourrait être plus attrayant que les autres (je ne suis pas vraiment sûr). J'ai également exécuté TestForNull avec les variables final, 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 quiconque avoir une bonne idée de la manière dont les méthodes MutableInt et Trove pourraient affecter l'utilisation de la mémoire.

Personnellement, je trouve la méthode MutableInt la plus intéressante, car elle ne nécessite pas le chargement de classes tierces. Donc, à moins que je découvre des problèmes avec cela, c'est la voie que je suis le plus susceptible d'aller.

Le code

Voici le code crucial de chaque méthode.

ContainsKey

import Java.util.HashMap;
import Java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
int count = freq.containsKey(Word) ? freq.get(Word) : 0;
freq.put(Word, count + 1);

TestForNull

import Java.util.HashMap;
import Java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
Integer count = freq.get(Word);
if (count == null) {
    freq.put(Word, 1);
}
else {
    freq.put(Word, count + 1);
}

AtomicLong

import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ConcurrentMap;
import Java.util.concurrent.atomic.AtomicLong;
...
final ConcurrentMap<String, AtomicLong> map = 
    new ConcurrentHashMap<String, AtomicLong>();
...
map.putIfAbsent(Word, new AtomicLong(0));
map.get(Word).incrementAndGet();

Trésor, richesse

import gnu.trove.TObjectIntHashMap;
...
TObjectIntHashMap<String> freq = new TObjectIntHashMap<String>();
...
freq.adjustOrPutValue(Word, 1, 1);

MutableInt

import Java.util.HashMap;
import Java.util.Map;
...
class MutableInt {
  int value = 1; // note that we start at 1 since we're counting
  public void increment () { ++value;      }
  public int  get ()       { return value; }
}
...
Map<String, MutableInt> freq = new HashMap<String, MutableInt>();
...
MutableInt count = freq.get(Word);
if (count == null) {
    freq.put(Word, new MutableInt());
}
else {
    count.increment();
}
348
gregory

OK, peut-être une vieille question, mais il y a un moyen plus court avec Java 8:

Map.merge(key, 1, Integer::sum)

Que fait-il: si la clé n’existe pas, mettez 1 comme valeur , sinon somme 1 à la valeur liée à la touche . Plus d'informations ici

190
LE GALL Benoît

Une petite 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 mieux):

                 time, ms
kolobokeCompile  18.8
koloboke         19.8
trove            20.8
fastutil         22.7
mutableInt       24.3
atomicInteger    25.3
Eclipse          26.9
hashMap          28.0
hppc             33.6
hppcRt           36.5

Résultats temps\espace: 

42
leventov

Google Guava est votre ami ...

... au moins dans certains cas. Ils ont cette belle AtomicLongMap . Surtout Nice parce que vous avez affaire à long en tant que valeur sur votre carte.

Par exemple.

AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(Word);

Également possible d'ajouter plus que 1 à la valeur:

map.getAndAdd(Word, 112L); 
33
H6.

@Hank Gay

Pour faire suite à mon propre commentaire (plutôt inutile): Trove semble être la voie à suivre. Si, pour une raison quelconque, vous vouliez vous en tenir au JDK standard, ConcurrentMap et AtomicLong peuvent transformer le code en un minuscule un peu plus agréable, bien que YMMV.

    final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.putIfAbsent("foo", new AtomicLong(0));
    map.get("foo").incrementAndGet();

laissera 1 comme valeur dans la carte pour foo. De manière réaliste, cette approche ne peut que le recommander.

31
Hank Gay

C'est toujours une bonne idée de regarder Google Collections Library pour ce genre de chose. Dans ce cas, un Multiset fera l'affaire:

Multiset bag = Multisets.newHashMultiset();
String Word = "foo";
bag.add(Word);
bag.add(Word);
System.out.println(bag.count(Word)); // Prints 2

Il existe des méthodes semblables à celles de la carte pour parcourir les clés/entrées, etc. En interne, l’implémentation utilise actuellement un HashMap<E, AtomicInteger>, vous éviterez ainsi des coûts de boxe.

25
Chris Nokleberg
Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0);
map.put(key, count + 1);

Et c'est comme cela que vous incrémentez une valeur avec un code simple.

Avantage:

  • Ne pas créer une autre classe pour mutable int
  • Petit code
  • Facile à comprendre
  • Aucune exception de pointeur nul

Une autre méthode consiste à utiliser la méthode de fusion, mais c'est trop pour simplement incrémenter une valeur.

map.merge(key, 1, (a,b) -> a+b);

Suggestion: dans la plupart des cas, la lisibilité du code doit être au centre de vos préoccupations.

21
off99555

Vous devez être conscient du fait que votre tentative initiale

int count = map.containsKey (Word)? map.get (Word): 0;

contient deux opérations potentiellement coûteuses sur une carte, à savoir containsKey et get. Le premier effectue une opération potentiellement assez similaire au second, vous faites donc le même travail deux fois!

Si vous examinez l'API pour la carte, les opérations get renvoient généralement null lorsque la carte ne contient pas l'élément demandé.

Notez que cela fera une solution comme

map.put (clé, map.get (clé) + 1);

dangereux, car il pourrait donner NullPointerExceptions. Vous devriez commencer par vérifier null.

Notez aussi, et ceci est très important, que HashMaps can contienne nulls par définition. Ainsi, tous les null renvoyés ne disent pas "il n’existe aucun élément de ce type". À cet égard, containsKey se comporte différemment de get en vous indiquant si il existe un tel élément. Reportez-vous à l'API pour plus de détails.

Cependant, dans votre cas, vous ne voudrez peut-être pas faire la distinction entre un null stocké et "noSuchElement". Si vous ne voulez pas autoriser null, vous préférerez peut-être un Hashtable. L'utilisation d'une bibliothèque de wrapper, comme cela avait déjà été proposé dans d'autres réponses, pourrait constituer une meilleure solution pour le traitement manuel, en fonction de la complexité de votre application.

Pour compléter la réponse (et j’avais oublié de le préciser au début, grâce à la fonction de modification!), La meilleure façon de le faire en mode natif est de get dans une variable final, vérifiez si null et put il retourne avec un 1. La variable doit être final car elle est immuable de toute façon. Le compilateur n'a peut-être pas besoin de cette indication, mais est plus clair de cette façon.

 HashMap final map = generateRandomHashMap (); 
 final Object clé = fetchSomeKey (); 
 Integer final i = map.get (clé); 
 if (i ! = null) {
 map.put (i + 1); 
} else {
 // faire quelque chose 
} 

Si vous ne voulez pas vous fier à la sélection automatique, vous devez plutôt indiquer quelque chose comme map.put(new Integer(1 + i.getValue()));.

21
Aleksandar Dimitrov

Une autre façon serait de créer un entier mutable:

class MutableInt {
  int value = 0;
  public void inc () { ++value; }
  public int get () { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt> ();
MutableInt value = map.get (key);
if (value == null) {
  value = new MutableInt ();
  map.put (key, value);
} else {
  value.inc ();
}

bien sûr, cela implique la création d'un objet supplémentaire, mais la surcharge liée à la création d'un Integer (même avec Integer.valueOf) ne devrait pas être si importante.

18
Philip Helger

Vous pouvez utiliser la méthode computeIfAbsent dans l'interface Map fournie dans Java 8 .

final Map<String,AtomicLong> map = new ConcurrentHashMap<>();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("B", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet(); //[A=2, B=1]

La méthode computeIfAbsent vérifie si la clé spécifiée est déjà associée à une valeur ou non? Si aucune valeur n'est associée, il tente de calculer sa valeur à l'aide de 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 null.

Sur une note parallèle, si vous avez une situation où plusieurs threads mettent à jour une somme commune, vous pouvez consulter LongAdder class.Un niveau de contention élevé, le débit attendu de cette classe est nettement supérieur à AtomicLong , au détriment d'une consommation d'espace plus importante.

10
i_am_zero

La rotation de la mémoire peut être un problème ici, car chaque mise en boîte d'un int supérieur ou égal à 128 provoque une allocation d'objet (voir Integer.valueOf (int)). Bien que le ramasse-miettes traite très efficacement les objets éphémères, 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 détenteur int à la place. Phax a déjà présenté du code pour cela. La voici à nouveau, avec deux modifications (la classe de titulaire est définie sur statique et la valeur initiale est définie sur 1):

static class MutableInt {
  int value = 1;
  void inc() { ++value; }
  int get() { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt>();
MutableInt value = map.get(key);
if (value == null) {
  value = new MutableInt();
  map.put(key, value);
} else {
  value.inc();
}

Si vous avez besoin de performances extrêmes, recherchez une implémentation de Map directement adaptée aux types de valeur primitifs. jrudolph mentionné GNU Trove .

A propos, un bon terme de recherche pour ce sujet est "histogramme".

7
volley

Au lieu d’appeler containsKey (), il est plus rapide d’appeler map.get et de vérifier si la valeur renvoyée est null ou non.

    Integer count = map.get(Word);
    if(count == null){
        count = 0;
    }
    map.put(Word, count + 1);
5
Glever

Il y a plusieurs approches:

  1. Utilisez un algorithme de type Bag comme les ensembles contenus dans Google Collections.

  2. Créez un conteneur modifiable que vous pouvez utiliser dans la carte:


    class My{
        String Word;
        int count;
    }

Et utilisez put ("Word", nouveau My ("Word")); Ensuite, vous pouvez vérifier s'il existe et incrémenter lors de l'ajout.

Évitez de lancer votre propre solution en utilisant des listes, car si vous effectuez une recherche et un tri innerloop, vos performances empestent. La première solution HashMap est en fait assez rapide, mais une solution correcte comme celle trouvée dans Google Collections est probablement meilleure.

Compter les mots à l'aide de Google Collections ressemble à ceci:



    HashMultiset s = new HashMultiset();
    s.add("Word");
    s.add("Word");
    System.out.println(""+s.count("Word") );

Utiliser HashMultiset est très élégant, car un algorithme de sac est exactement ce dont vous avez besoin pour compter les mots.

3
tovare

Collections Google HashMultiset:
- assez élégant à utiliser
- mais consomme du processeur et de la mémoire

Le mieux serait d'avoir une méthode comme celle-ci: Entry<K,V> getOrPut(K); (élégante et économique)

Une telle méthode calculera le hachage et l’indexation une seule fois, puis nous pourrons faire ce que nous voulons avec l’entrée (remplacer ou mettre à jour la valeur).

Plus élégant:
- prenez un HashSet<Entry>
- étendez-le de sorte que get(K) mette une nouvelle entrée si nécessaire
- L’entrée pourrait être votre propre objet.
-> (new MyHashSet()).get(k).increment();

3
the felis leo

Une variante de l'approche MutableInt qui pourrait être encore plus rapide, si l'on veut, consiste à utiliser un tableau int à un seul élément:

Map<String,int[]> map = new HashMap<String,int[]>();
...
int[] value = map.get(key);
if (value == null) 
  map.put(key, new int[]{1} );
else
  ++value[0];

Il serait intéressant de pouvoir relancer vos tests de performance avec cette variante. C'est peut-être le plus rapide.


Edit: Le motif ci-dessus a bien fonctionné pour moi, mais j’ai finalement décidé d’utiliser les collections de Trove afin de réduire la taille de la mémoire dans certaines très grandes cartes que je créais. En prime, c’était aussi plus rapide.

Une caractéristique vraiment intéressante est que la classe TObjectIntHashMap a un seul appel adjustOrPutValue qui, selon qu’il existe déjà une valeur à cette clé, met une valeur initiale ou incrémente la valeur existante. C'est parfait pour incrémenter:

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);
3

Je pense que votre solution serait la méthode standard, mais - comme vous l’avez indiqué vous-même - ce n’est probablement pas la méthode 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.

3
jrudolph

Êtes-vous sûr que c'est un goulot d'étranglement? Avez-vous effectué une analyse de performance?

Essayez d’utiliser le profileur NetBeans (gratuit et intégré à NB 6.1) pour examiner les points chauds.

Enfin, une mise à niveau de la machine virtuelle Java (par exemple de 1,5 à> 1,6) constitue souvent un booster de performance peu coûteux. Même une mise à niveau du numéro de version peut fournir de bonnes augmentations de performances. Si vous utilisez 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 Server Hotspot. Sur les machines Linux et Solaris, cela est détecté automatiquement.

3
John Wright

Assez simple, utilisez simplement la fonction intégrée dans Map.Java comme suit

map.put(key, map.getOrDefault(key, 0) + 1);
2
sudoz

"put" need "get" (pour éviter toute duplication de clé).
Alors faites directement un "put",
et s'il y avait une valeur précédente, faites une addition:

Map map = new HashMap ();

MutableInt newValue = new MutableInt (1); // default = inc
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.add(oldValue); // old + inc
}

Si le nombre commence à 0, ajoutez 1: (ou toute autre valeur ...)

Map map = new HashMap ();

MutableInt newValue = new MutableInt (0); // default
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.setValue(oldValue + 1); // old + inc
}

Avis: Ce code n'est pas thread-safe. Utilisez-le pour construire puis utilisez la carte, et non pour la mettre à jour simultanément.

Optimisation: Dans une boucle, conservez l'ancienne valeur pour devenir la nouvelle valeur de la prochaine boucle.

Map map = new HashMap ();
final int defaut = 0;
final int inc = 1;

MutableInt oldValue = new MutableInt (default);
while(true) {
  MutableInt newValue = oldValue;

  oldValue = map.put (key, newValue); // insert or...
  if (oldValue != null) {
    newValue.setValue(oldValue + inc); // ...update

    oldValue.setValue(default); // reuse
  } else
    oldValue = new MutableInt (default); // renew
  }
}
2
the felis leo

Si vous utilisez Eclipse Collections , vous pouvez utiliser un 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 un MutableObjectIntMap qui stocke les inits primitifs au lieu d'objets Counter. Cela réduit la surcharge de mémoire et améliore la vitesse d'exécution.

HashBag fournit l'API dont vous avez besoin puisqu'il s'agit d'un Collection qui vous permet également d'interroger le nombre d'occurrences d'un élément.

Voici un exemple tiré de Eclipse Collections Kata .

MutableBag<String> bag =
  HashBag.newBagWith("one", "two", "two", "three", "three", "three");

Assert.assertEquals(3, bag.occurrencesOf("three"));

bag.add("one");
Assert.assertEquals(2, bag.occurrencesOf("one"));

bag.addOccurrences("one", 4);
Assert.assertEquals(6, bag.occurrencesOf("one"));

Remarque: Je suis un partisan des collections Eclipse.

1
Craig P. Motlin

J'utiliserais Apache Collections Lazy Map (pour initialiser les valeurs à 0) et utiliserais MutableIntegers d'Apache Lang comme valeurs de cette carte.

Le coût le plus élevé est de devoir rechercher la carte deux fois dans votre méthode. Dans le mien, vous devez le faire juste une fois. Obtenez juste la valeur (elle sera initialisée si elle est absente) et incrémentez-la.

1
jb.

La TreeMap structure de données de la bibliothèque fonctionnelle Java possède une méthode update dans la dernière tête de ligne principale:

public TreeMap<K, V> update(final K k, final F<V, V> f)

Exemple d'utilisation:

import static fj.data.TreeMap.empty;
import static fj.function.Integers.add;
import static fj.pre.Ord.stringOrd;
import fj.data.TreeMap;

public class TreeMap_Update
  {public static void main(String[] a)
    {TreeMap<String, Integer> map = empty(stringOrd);
     map = map.set("foo", 1);
     map = map.update("foo", add.f(1));
     System.out.println(map.get("foo").some());}}

Ce programme affiche "2".

1
Apocalisp

Je ne sais pas dans quelle mesure il est efficace, mais le code ci-dessous fonctionne également. Vous devez définir un BiFunction au début. De plus, vous pouvez faire plus que simplement incrémenter avec cette méthode.

public static Map<String, Integer> strInt = new HashMap<String, Integer>();

public static void main(String[] args) {
    BiFunction<Integer, Integer, Integer> bi = (x,y) -> {
        if(x == null)
            return y;
        return x+y;
    };
    strInt.put("abc", 0);


    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abcd", 1, bi);

    System.out.println(strInt.get("abc"));
    System.out.println(strInt.get("abcd"));
}

la sortie est

3
1
1
MGoksu

Les divers wrappers primitifs, par exemple, Integer sont immuables. Il n’ya donc pas de moyen plus concis de faire ce que vous demandez à moins que ne soit possible. faites-le avec quelque chose comme AtomicLong . Je peux y aller dans une minute et mettre à jour. BTW, Hashtable fait partie du Collections Framework .

1
Hank Gay

@Vilmantas Baranauskas: En ce qui concerne cette réponse, je voudrais commenter si j'avais les points de rep, mais ce n'est pas le cas. Je voulais noter que la classe Counter définie à cet endroit n'est PAS thread-safe, car il ne suffit pas de synchroniser inc () sans synchroniser value (). Les autres threads appelant value () ne sont pas assurés de voir la valeur, sauf si une relation passe-avant a été établie avec la mise à jour.

1
Alex Miller