it-swarm.it

Il modo più efficiente per incrementare un valore di Map in Java

Spero che questa domanda non sia considerata troppo semplice per questo forum, ma vedremo. Mi sto chiedendo come rifattorizzare un codice per ottenere prestazioni migliori che vengono eseguite più volte.

Supponiamo che sto creando un elenco di frequenze di Word, utilizzando una mappa (probabilmente una HashMap), in cui ogni chiave è una stringa con la parola che viene contata e il valore è un numero intero che viene incrementato ogni volta che viene trovato un token della parola.

In Perl, incrementare tale valore sarebbe banalmente semplice:

$map{$Word}++;

Ma in Java, è molto più complicato. Ecco come lo sto facendo attualmente:

int count = map.containsKey(Word) ? map.get(Word) : 0;
map.put(Word, count + 1);

Che, naturalmente, si basa sulla funzionalità di autoboxing nelle nuove versioni di Java. Mi chiedo se è possibile suggerire un modo più efficiente di incrementare tale valore. Ci sono anche buone ragioni per le prestazioni per evitare il framework Collections e usare invece qualcos'altro?

Aggiornamento: ho fatto un test di molte delle risposte. Vedi sotto.

327
gregory

Alcuni risultati dei test

Ho ottenuto molte buone risposte a questa domanda - grazie gente - quindi ho deciso di eseguire alcuni test e capire quale metodo è effettivamente il più veloce. I cinque metodi che ho testato sono questi:

  • il metodo "ContainsKey" che ho presentato in la domanda
  • il metodo "TestForNull" suggerito da Aleksandar Dimitrov
  • il metodo "AtomicLong" suggerito da Hank Gay
  • il metodo "Trove" suggerito da jrudolph
  • il metodo "MutableInt" suggerito da phax.myopenid.com

Metodo

Ecco cosa ho fatto ...

  1. creato cinque classi che erano identiche tranne per le differenze mostrate di seguito. Ogni classe doveva eseguire un'operazione tipica dello scenario presentato: aprire un file da 10 MB e leggerlo, quindi eseguire un conteggio di frequenza di tutti i token di Word nel file. Poiché ciò ha richiesto una media di soli 3 secondi, ho dovuto eseguire il conteggio delle frequenze (non l'I/O) 10 volte.
  2. temporizzato il ciclo di 10 iterazioni ma non l'operazione I/O e registrato il tempo totale impiegato (in secondi) essenzialmente usando il metodo di Ian Darwin nel Ricettario Java .
  3. ha eseguito tutti e cinque i test in serie, e poi l'ha fatto un altro tre volte.
  4. ha calcolato la media dei quattro risultati per ciascun metodo.

Risultati

Presenterò prima i risultati e il codice seguente per coloro che sono interessati.

Il metodo ContainsKey era, come previsto, il più lento, quindi darò la velocità di ciascun metodo rispetto alla velocità di quel metodo.

  • ContainsKey: 30.654 secondi (linea di base)
  • AtomicLong: 29,780 secondi (1,03 volte più veloce)
  • TestForNull: 28,804 secondi (1,06 volte più veloce)
  • Trove: 26,313 secondi (1,16 volte più veloce)
  • MutableInt: 25.747 secondi (1,19 volte più veloce)

Conclusioni

Sembrerebbe che solo il metodo MutableInt e il metodo Trove siano significativamente più veloci, in quanto danno solo un incremento delle prestazioni superiore al 10%. Tuttavia, se il threading è un problema, AtomicLong potrebbe essere più attraente degli altri (non sono proprio sicuro). Ho anche eseguito TestForNull con variabili final, ma la differenza era trascurabile.

Si noti che non ho profilato l'utilizzo della memoria nei diversi scenari. Sarei felice di sapere da chiunque abbia una buona conoscenza di come i metodi MutableInt e Trove potrebbero influenzare l'utilizzo della memoria.

Personalmente, trovo che il metodo MutableInt sia il più attraente, dal momento che non richiede il caricamento di classi di terze parti. Quindi, a meno che non scopro dei problemi, è il modo in cui sono più propenso ad andare.

Il codice

Ecco il codice cruciale di ciascun metodo.

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

Trove

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

OK, potrebbe essere una vecchia domanda, ma c'è un modo più breve con Java 8:

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

Che cosa fa: if key non esiste, put 1 come valore, altrimenti sum 1 al valore collegato a chiave . Ulteriori informazioni qui

175
LE GALL Benoît

Una piccola ricerca nel 2016: https://github.com/leventov/Java-Word-count , benchmark codice sorgente

Migliori risultati per metodo (minore è meglio):

                 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

Tempo/spazio risultati: 

42
leventov

Google Guava è tuo amico ...

... almeno in alcuni casi. Hanno questo Nice AtomicLongMap . Particolarmente piacevole perché hai a che fare lungo come valore nella tua mappa.

Per esempio.

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

Inoltre è possibile aggiungere più di 1 al valore:

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

@Hank Gay

Come follow-up del mio (piuttosto inutile) commento: Trove sembra la strada da percorrere. Se, per qualsiasi motivo, si desidera mantenere il JDK standard, ConcurrentMap e AtomicLong può rendere il codice a tiny più bello, sebbene YMMV.

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

lascerà 1 come valore nella mappa per foo. Realisticamente, l'aumento della cordialità nel threading è tutto ciò che questo approccio deve raccomandare.

31
Hank Gay

È sempre una buona idea dare un'occhiata alla Google Collections Library per questo genere di cose. In questo caso un Multiset farà il trucco:

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

Esistono metodi di tipo Map per iterare su chiavi/voci, ecc. Internamente l'implementazione utilizza attualmente un HashMap<E, AtomicInteger>, quindi non si dovranno sostenere costi di boxe.

25
Chris Nokleberg

Dovresti essere consapevole del fatto che il tuo tentativo originale

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

contiene due operazioni potenzialmente costose su una mappa, vale a dire containsKeye getname__. Il primo esegue un'operazione potenzialmente abbastanza simile a quest'ultima, quindi stai facendo lo stesso lavoro due volte !

Se si guarda l'API per Map, le operazioni getsolitamente restituiscono nullquando la mappa non contiene l'elemento richiesto.

Si noti che questo renderà una soluzione simile

map.put (chiave, map.get (chiave) + 1);

pericoloso, dal momento che potrebbe produrre NullPointerExceptionname__s. Dovresti prima controllare nullname__.

Nota anche, e questo è molto importante, che HashMapname__s can contiene nullsper definizione. Quindi non tutti restituiti nulldice "non esiste un elemento di questo tipo". In questo senso, containsKeysi comporta in modo diverso da getin realtà dicendo se c'è un tale elemento. Fare riferimento all'API per i dettagli.

Per il tuo caso, tuttavia, potresti non voler distinguere tra nulle "noSuchElement". Se non vuoi permettere nullname__s potresti preferire un Hashtablename__. L'utilizzo di una libreria wrapper come già proposto in altre risposte potrebbe essere una soluzione migliore per il trattamento manuale, a seconda della complessità dell'applicazione.

Per completare la risposta (e ho dimenticato di inserirla all'inizio, grazie alla funzione di modifica!), Il modo migliore di farlo in modo nativo è getin una variabile finalname__, controlla nulle putdi nuovo con 1 . La variabile dovrebbe essere finalperché è comunque immutabile. Il compilatore potrebbe non aver bisogno di questo suggerimento, ma è più chiaro in questo modo.

 final HashMap map = generateRandomHashMap (); 
 final Chiave object = fetchSomeKey (); 
 finale Intero i = map.get (chiave); 
 if (i ! = null) {
 map.put (i + 1); 
} else {
 // fa qualcosa 
} 

Se non vuoi fare affidamento su autoboxing, dovresti invece dire qualcosa come map.put(new Integer(1 + i.getValue()));.

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

Ed è così che si incrementa un valore con un codice semplice.

Beneficiare:

  • Non creare un'altra classe per mutable int
  • Codice corto
  • Facile da capire
  • Nessuna eccezione del puntatore nullo

Un altro modo è utilizzare il metodo di unione, ma questo è troppo per l'incremento di un valore.

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

Suggerimento: dovresti preoccuparti della leggibilità del codice più del piccolo guadagno di prestazioni nella maggior parte delle volte.

20
off99555

Un altro modo sarebbe creare un intero mutabile:

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

ovviamente questo implica la creazione di un oggetto aggiuntivo ma il sovraccarico rispetto alla creazione di un intero (anche con Integer.valueOf) non dovrebbe essere così tanto.

18
Philip Helger

È possibile utilizzare il metodo computeIfAbsent nell'interfaccia Map fornita in 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]

Il metodo computeIfAbsent controlla se la chiave specificata è già associata a un valore o no? Se nessun valore associato tenta di calcolare il suo valore utilizzando la funzione di mappatura fornita. In ogni caso restituisce il valore corrente (esistente o calcolato) associato alla chiave specificata, oppure null se il valore calcolato è nullo.

Nota a margine se hai una situazione in cui più thread aggiornano una somma comune puoi dare un'occhiata a LongAdder class.In alto contention, throughput previsto di questa classe è significativamente più alta di AtomicLong, a spese di un maggiore consumo di spazio.

9
i_am_zero

La rotazione della memoria può essere un problema qui, dal momento che ogni boxing di un int maggiore o uguale a 128 causa un'allocazione dell'oggetto (vedi Integer.valueOf (int)). Sebbene il garbage collector si occupi in modo molto efficiente di oggetti di breve durata, le prestazioni risentiranno in una certa misura.

Se sai che il numero di incrementi effettuati sarà in gran parte superiore al numero di chiavi (= parole in questo caso), considera invece l'utilizzo di un titolare int. Phax ha già presentato il codice per questo. Eccolo di nuovo, con due modifiche (la classe del titolare è statica e il valore iniziale è impostato su 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();
}

Se hai bisogno di prestazioni estreme, cerca un'implementazione di mappa che sia direttamente adattata ai tipi di valore primitivi. jrudolph menzionato GNU Trove .

A proposito, un termine di ricerca valido per questo argomento è "istogramma".

7
volley

Invece di chiamare containsKey () è più veloce chiamare semplicemente map.get e controllare se il valore restituito è nullo o meno.

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

Ci sono un paio di approcci:

  1. Utilizza un algoritmo di Borsa come i set contenuti in Google Collections.

  2. Crea un contenitore mutevole che puoi utilizzare nella mappa:


    class My{
        String Word;
        int count;
    }

E usa put ("Word", new My ("Word")); Quindi puoi controllare se esiste e incrementare quando aggiungi.

Evita di far rotolare la tua soluzione usando gli elenchi, perché se ottieni ricerca interiore e ordinamento, le tue prestazioni faranno schifo. La prima soluzione di HashMap è in realtà abbastanza veloce, ma una versione corretta trovata in Google Collections è probabilmente migliore.

Contando le parole usando Google Collections, assomiglia a qualcosa del genere:



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

Usare HashMultiset è molto elegante, perché un algoritmo di borsa è proprio quello che ti serve quando contate le parole.

3
tovare

HashMultiset Google Collections:
- abbastanza elegante da usare
- ma consumano CPU e memoria

La cosa migliore sarebbe avere un metodo come: Entry<K,V> getOrPut(K); (elegante e a basso costo)

Tale metodo calcolerà hash e indice solo una volta, quindi potremo fare ciò che vogliamo con la voce (sostituire o aggiornare il valore).

Più elegante:
- prendi un HashSet<Entry>
- estendilo in modo che get(K) inserisca una nuova voce se necessario
- L'entrata potrebbe essere il tuo oggetto.
-> (new MyHashSet()).get(k).increment();

3
the felis leo

Una variante dell'approccio MutableInt che potrebbe essere anche più veloce, se un po 'un hack, è usare un array int a elemento singolo:

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];

Sarebbe interessante se fosse possibile rieseguire i test delle prestazioni con questa variazione. Potrebbe essere il più veloce.


Modifica: Il pattern sopra ha funzionato bene per me, ma alla fine ho cambiato le collezioni di Trove per ridurre le dimensioni della memoria in alcune mappe molto grandi che stavo creando - e come bonus era anche più veloce.

Una caratteristica davvero piacevole è che la classe TObjectIntHashMap ha una singola chiamata adjustOrPutValue che, a seconda che esista già un valore su quella chiave, metterà un valore iniziale o incrementerà il valore esistente. Questo è perfetto per l'incremento:

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

Penso che la tua soluzione sarebbe il modo standard, ma - come hai notato te stesso - probabilmente non è il modo più veloce possibile.

Puoi guardare GNU Trove . Quella è una biblioteca che contiene ogni sorta di collezioni veloci e primitive. Il tuo esempio userebbe un TObjectIntHashMap che ha un metodo adjustOrPutValue che fa esattamente quello che vuoi.

3
jrudolph

Sei sicuro che questo sia un collo di bottiglia? Hai fatto qualche analisi delle prestazioni?

Prova a utilizzare il profiler NetBeans (è gratuito e incorporato in NB 6.1) per esaminare gli hotspot.

Infine, un aggiornamento JVM (ad esempio da 1.5-> 1.6) è spesso un aumento delle prestazioni a basso costo. Anche un aggiornamento del numero di build può fornire un buon incremento delle prestazioni. Se si esegue su Windows e questa è un'applicazione di classe server, utilizzare -server sulla riga di comando per utilizzare la JVM di Server Hotspot. Su macchine Linux e Solaris questo viene rilevato automaticamente.

3
John Wright

Abbastanza semplice, usa la funzione built-in in Map.Java come segue

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

"put" ha bisogno di "get" (per garantire che non vi siano chiavi duplicate).
Quindi fai direttamente un "put",
e se c'era un valore precedente, quindi fare un'aggiunta:

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
}

Se il conteggio inizia da 0, quindi aggiungi 1: (o qualsiasi altro valore ...)

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
}

Avviso: Questo codice non è thread-safe. Usalo per costruire poi usa la mappa, non per aggiornarlo contemporaneamente.

Ottimizzazione: In un ciclo, mantenere il vecchio valore per diventare il nuovo valore del ciclo successivo.

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

Se stai usando Eclipse Collections , puoi usare un HashBag. Sarà l'approccio più efficiente in termini di utilizzo della memoria e sarà anche performante in termini di velocità di esecuzione.

HashBag è supportato da un MutableObjectIntMap che memorizza ints primitivi invece di oggetti Counter. Ciò riduce il sovraccarico della memoria e migliora la velocità di esecuzione.

HashBag fornisce l'API di cui avresti bisogno poiché è un Collection che ti consente anche di interrogare il numero di occorrenze di un oggetto.

Ecco un esempio dal 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"));

Nota: Sono un committer per le raccolte di Eclipse.

1
Craig P. Motlin

Vorrei utilizzare Apache Collections Lazy Map (per inizializzare i valori su 0) e utilizzare MutableIntegers da Apache Lang come valori in quella mappa.

Il costo maggiore consiste nel dover eseguire di nuovo la mappa due volte nel metodo. Nel mio devi farlo solo una volta. Ottieni il valore (verrà inizializzato se assente) e lo incrementerà.

1
jb.

Il funzionale Java della libreria TreeMap datastructure ha un metodo update nell'ultima testa del trunk:

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

Esempio di utilizzo:

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

Questo programma stampa "2".

1
Apocalisp

Non so quanto sia efficiente, ma funziona anche il codice seguente. Devi definire un BiFunctionall'inizio. Inoltre, puoi fare molto di più che incrementare con questo metodo.

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

l'output è

3
1
1
MGoksu

I vari wrapper primitivi, ad esempio, Integer sono immutabili, quindi non esiste un modo più conciso per fare ciò che stai chiedendo a meno che tu possa farlo con qualcosa di simile AtomicLong . Posso dare un andare in un minuto e aggiornare. BTW, Hashtable è una parte del Framework delle collezioni .

1
Hank Gay

@ Vilmantas Baranauskas: Per quanto riguarda questa risposta, vorrei commentare se ho avuto i punti di rep, ma non lo faccio. Volevo notare che la classe Counter definita NON è thread-safe in quanto non è sufficiente solo sincronizzare inc () senza sincronizzare value (). Gli altri thread che chiamano value () non sono garantiti per vedere il valore a meno che non sia stata stabilita una relazione di happen-before con l'aggiornamento.

1
Alex Miller