2013-04-02 8 views
32

Mentre lavoravo su un benchmark di memoria di alcune strutture di dati ad alto throughput, mi sono reso conto che potevo usare uno ImmutableMap con solo un piccolo refactoring.Guava ImmutableMap ha un accesso notevolmente più lento di HashMap

Pensando che questo sarebbe stato un miglioramento, l'ho inserito nel mix e sono stato sorpreso di scoprire che non solo era più lento di HashMap, in un ambiente a thread singolo sembra essere sempre più lento anche di ConcurrentHashMap!

si può vedere il punto di riferimento completo qui: https://bitbucket.org/snippets/dimo414/89K7G

La carne del test è piuttosto semplice, quanto tempo ci vuole per ottenere un gran numero di stringhe casuali che possono esistere nella mappa.

public static void timeAccess(Map<String,String> map) { 
    Random rnd = new Random(seed); 
    int foundCount = 0; 

    long start = System.nanoTime(); 

    for(int i = 0; i < loop; i++) { 
     String s = map.get(RndString.build(rnd)); 
     if(s != null) 
      foundCount++; 
    } 

    long stop = System.nanoTime() - start; 

    System.out.println("Found "+foundCount+" strings out of "+loop+" attempts - "+ 
     String.format("%.2f",100.0*foundCount/loop)+" success rate."); 
    System.out.println(map.getClass().getSimpleName()+" took "+ 
     String.format("%.4f", stop/1_000_000_000.0)+" seconds."); 
    System.out.println(); 
} 

E l'esecuzione di questo contro un HashMap, un ConcurrentHashMap, e un ImmutableMap, tutti contenenti gli stessi valori, costantemente ha mostrato un rallentamento drammatico quando si utilizza ImmutableMap - spesso verso l'alto di 15% più lento. Più sparsa è la mappa (ad esempio, più spesso è map.get() restituita nulla) maggiore è la disparità. Ecco il risultato di un'esecuzione di esempio:

Found 35312152 strings out of 100000000 attempts - 35.31 success rate. 
HashMap took 29.4538 seconds. 

Found 35312152 strings out of 100000000 attempts - 35.31 success rate. 
ConcurrentHashMap took 32.1465 seconds. 

Found 35312152 strings out of 100000000 attempts - 35.31 success rate. 
RegularImmutableMap took 37.9709 seconds. 

Si tratta di un problema documentato/previsto? Il Guava Docs indica che Immutable*** è più efficiente in termini di memoria, ma non dice nulla sulla velocità. Per rallentamenti di questa portata, sono propenso ad affrontare i costi della memoria ed evitare lo Immutable*** quando la velocità è un problema (e quando no ?!). Mi sto perdendo qualcosa?

Consulta anche: https://groups.google.com/forum/?fromgroups=#!topic/guava-discuss/I7yPpa5Hlpg

+9

I problemi menzionati in questo thread di mailing list si applicano definitivamente ai vostri benchmark. Inoltre, vedere Javadoc "ImmutableMap':" a differenza di HashMap, ImmutableMap non è ottimizzato per i tipi di elementi con implementazioni lente Object.equals (java.lang.Object) o Object.hashCode(). È possibile ottenere prestazioni migliori avendo il proprio elemento digita nella cache i propri codici hash e facendo uso dei valori memorizzati nella cache per cortocircuitare un algoritmo di equals lento. " Questo è certamente un problema con 'String'. Infine, l'implementazione di 'ImmutableMap' è in gran parte la stessa di' HashMap'. –

+0

Se lo desideri, puoi provare alcuni dei benchmark di Guava: https://code.google.com/p/guava-libraries/source/browse/guava-tests/benchmark/com/google/common/collect/MapBenchmark. java –

+0

Inoltre, "gestire i costi della memoria" potrebbe comportare un aumento significativo del carico del GC, che può rallentare il programma tanto quanto un'implementazione più lenta ma più compatta. Non c'è davvero alcun sostituto per la profilazione con la tua specifica applicazione reale. –

risposta

0

Alcune possibili ragioni:

  1. Potrebbe che dipendono dalla realizzazione di RndString.build()?

  2. e dare un'occhiata alla realizzazione get() di entrambe le mappe: com.google.common.collect.RegularImmutableMap.get (Object) java.util.HashMap.getEntry (Object) java.util. HashMap tenta di confrontare prima con "==". RegularImmutableMap non lo fa. Ciò potrebbe accelerare

  3. Potrebbe essere responsabile un diverso fattore di carico? Forse la RegularImmutableMap ha bisogno di più iterazioni per trovare la voce corretta.

+0

1. C'è un collegamento al codice completo nella mia domanda. 'RndString.build()' dovrebbe usare una memoria minima e usa sempre la stessa semente per l'accesso alla mappa. Certamente potrebbe causare qualche pregiudizio, ma non sono a conoscenza di come. – dimo414

+0

2. Buon punto, ma 'RegularImmutableMap.get() 'dice quanto segue:" Se avessimo fatto [an == check], avrebbe solo peggiorato le cose per i metodi [equals] più attenti alle prestazioni ". Poiché 'String.equals()' prima esegue un controllo '==', esso (presumibilmente) dovrebbe essere più veloce di '== && .equals()' – dimo414

+0

3. È certamente possibile (il valore predefinito di 'HashMap' è' .75' dove 'ImmutableMap' sceglie dinamicamente un valore non più grande di' 1.2'), ma anche cambiando il mio metodo 'buildMap()' per usare 'new HashMap <> (16,1.2f)' che dovrebbe essere * peggiore * di 'ImmutableMap' ' s implementazione, 'HashMap' era ancora notevolmente più veloce, anche se leggermente più lento del benchmark originale. Vale anche la pena di notare il commento in "RegularImmutableMap" che dice "L'indirizzamento chiuso tende a comportarsi bene anche con fattori di carico elevati ... [T] la tabella è ancora probabilmente relativamente sparsa (quindi manca rapidamente) mentre risparmia spazio." – dimo414

13

Come detto Louis Wasserman, ImmutableMap non è ottimizzato per oggetti con metodo lento equals. Penso che la differenza principale è qui:

HashMap:

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
    return e.value; 

ImmtubleMap:

if (key.equals(candidateKey)) { 
    return entry.getValue(); 

Come si può vedere, per verificare la presenza di collisioni, HashMap prima controllare gli hash. Ciò consente di rifiutare rapidamente i valori con diversi hash. Poiché String non esegue questo controllo nel suo metodo equals, questo rende HashMap più veloce.ImmutableMap non utilizza questa ottimizzazione perché renderebbe il test più lento quando equals è già ottimizzato.

+0

Ah, questo ha più senso, capisco cosa stava dicendo. Affascinante che String memorizza nella cache il valore hash in 'String.hashCode()' ma non trae vantaggio da quei dati in 'String.equals()' ... – dimo414

+1

Questo sicuramente sembra essere una gran parte del problema, aggiungendo un Il wrapper 'Holder' per la chiave che memorizza il codice hash dell'oggetto ha fatto cadere il tempo di ImmutableMap da ~' 35' secondi a ~ '32'. Eppure 'HashMap' eseguiva ancora misurabilmente più veloce a ~' 28 'secondi. Sembra che l'ipotesi di Guava che le chiavi siano/dovrebbero essere ben educate è piuttosto scarsa se 'String', la chiave di mappa più comune, non si adatta a tale ipotesi. – dimo414

+0

@DanTao ok, modificato. – WilQu

Problemi correlati