Ottimizzazione / alternativa delle prestazioni di Java HashMap


102

Voglio creare una grande HashMap ma le put()prestazioni non sono abbastanza buone. Qualche idea?

Altri suggerimenti sulla struttura dei dati sono i benvenuti, ma ho bisogno della funzione di ricerca di una mappa Java:

map.get(key)

Nel mio caso voglio creare una mappa con 26 milioni di voci. Utilizzando lo standard Java HashMap, la velocità di inserimento diventa insopportabilmente lenta dopo 2-3 milioni di inserimenti.

Inoltre, qualcuno sa se l'utilizzo di diverse distribuzioni di codice hash per le chiavi potrebbe aiutare?

Il mio metodo hashcode:

byte[] a = new byte[2];
byte[] b = new byte[3];
...

public int hashCode() {
    int hash = 503;
    hash = hash * 5381 + (a[0] + a[1]);
    hash = hash * 5381 + (b[0] + b[1] + b[2]);
    return hash;
}

Sto usando la proprietà associativa dell'addizione per assicurarmi che oggetti uguali abbiano lo stesso codice hash. Gli array sono byte con valori compresi tra 0 e 51. I valori vengono utilizzati solo una volta in entrambi gli array. Gli oggetti sono uguali se gli array a contengono gli stessi valori (in entrambi gli ordini) e lo stesso vale per l'array b. Quindi a = {0,1} b = {45,12,33} e a = {1,0} b = {33,45,12} sono uguali.

EDIT, alcune note:

  • Alcune persone hanno criticato l'utilizzo di una mappa hash o di un'altra struttura di dati per memorizzare 26 milioni di voci. Non riesco a capire perché questo possa sembrare strano. A me sembra un classico problema di strutture dati e algoritmi. Ho 26 milioni di elementi e voglio essere in grado di inserirli rapidamente e cercarli da una struttura dati: dammi la struttura dati e gli algoritmi.

  • L'impostazione della capacità iniziale della Java HashMap predefinita su 26 milioni riduce le prestazioni.

  • Alcune persone hanno suggerito di utilizzare i database, in alcune altre situazioni questa è sicuramente l'opzione intelligente. Ma sto davvero ponendo una domanda sulle strutture dati e sugli algoritmi, un database completo sarebbe eccessivo e molto più lento di una buona soluzione di struttura dati (dopotutto il database è solo software ma avrebbe comunicazione e forse sovraccarico del disco).


29
Se HashMap diventa lento, con ogni probabilità la tua funzione hash non è abbastanza buona.
Pascal Cuoq

12
medico, fa male quando faccio questo
skaffman

12
Questa è davvero una buona domanda; una bella dimostrazione del perché gli algoritmi di hashing sono importanti e degli effetti che possono avere sulle prestazioni
oxbow_lakes

12
La somma degli a ha un intervallo da 0 a 102 e la somma di b ha un intervallo da 0 a 153, quindi hai solo 15,606 valori hash possibili e una media di 1.666 chiavi con lo stesso hashCode. Dovresti cambiare il tuo codice hash in modo che il numero di codici hash possibili sia molto maggiore del numero di chiavi.
Peter Lawrey,

6
Ho determinato psichicamente che stai modellando il Texas Hold 'Em Poker ;-)
bacar

Risposte:


56

Come molte persone hanno sottolineato, il hashCode()metodo era da biasimare. Stava generando solo circa 20.000 codici per 26 milioni di oggetti distinti. Questa è una media di 1.300 oggetti per bucket hash = molto molto male. Tuttavia, se trasformo i due array in un numero in base 52, ho la garanzia di ottenere un codice hash univoco per ogni oggetto:

public int hashCode() {       
    // assume that both a and b are sorted       
    return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);
}

public static int powerOf52(byte b, int power) {
    int result = b;
    for (int i = 0; i < power; i++) {
        result *= 52;
    }
    return result;
}

Gli array vengono ordinati per garantire che questo metodo soddisfi il hashCode()contratto che gli oggetti uguali hanno lo stesso codice hash. Utilizzando il vecchio metodo, il numero medio di put al secondo su blocchi di 100.000 put, da 100.000 a 2.000.000 era:

168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083

L'uso del nuovo metodo offre:

337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25

Molto molto meglio. Il vecchio metodo si è esaurito molto rapidamente mentre il nuovo mantiene una buona produttività.


17
Suggerisco di non modificare gli array nel hashCodemetodo. Per convenzione, hashCodenon modifica lo stato dell'oggetto. Forse il costruttore sarebbe un posto migliore per ordinarli.
Michael Myers

Sono d'accordo che l'ordinamento degli array dovrebbe avvenire nel costruttore. Il codice mostrato non sembra mai impostare hashCode. Calcolo del codice può essere fatta più semplice come segue: int result = a[0]; result = result * 52 + a[1]; //etc.
rsp

Sono d'accordo che l'ordinamento nel costruttore e quindi il calcolo del codice hash come suggeriscono mmyers e rsp è migliore. Nel mio caso la mia soluzione è accettabile e volevo sottolineare il fatto che gli array devono essere ordinati per hashCode()funzionare.
Nash

3
Nota che potresti anche memorizzare nella cache il codice hash (e invalidarlo in modo appropriato se il tuo oggetto è modificabile).
NateS

1
Basta usare java.util.Arrays.hashCode () . È più semplice (nessun codice da scrivere e mantenere da soli), il suo calcolo è probabilmente più veloce (meno moltiplicazioni) e la distribuzione dei suoi codici hash sarà probabilmente più uniforme.
jcsahnwaldt Ripristina Monica il

18

Una cosa che noto nel tuo hashCode()metodo è che l'ordine degli elementi negli array a[]e b[]non importa. Quindi (a[]={1,2,3}, b[]={99,100})hash allo stesso valore di (a[]={3,1,2}, b[]={100,99}). In realtà tutte le chiavi k1e k2dove sum(k1.a)==sum(k2.a)e sum(k1.b)=sum(k2.b)risulteranno in collisioni. Suggerisco di assegnare un peso a ciascuna posizione dell'array:

hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);

dove, c0, c1e c3sono distinti costanti (è possibile utilizzare diverse costanti per bse necessario). Questo dovrebbe uniformare un po 'di più le cose.


Anche se dovrei anche aggiungere che non funzionerà per me perché voglio che la proprietà che gli array con gli stessi elementi in ordini diversi diano lo stesso codice hash.
nash

5
In tal caso, hai codici hash 52C2 + 52C3 (23426 secondo la mia calcolatrice) e una hashmap è lo strumento sbagliato per il lavoro.
kdgregory

In realtà questo aumenterebbe le prestazioni. Più collisioni eq meno voci nella tabella hash eq. meno lavoro da fare. Non è l'hash (che sembra a posto) né la tabella hash (che funziona alla grande) Scommetto che è sulla creazione dell'oggetto dove le prestazioni si stanno degradando.
OscarRyz

7
@Oscar - più collisioni equivalgono a più lavoro da fare, perché ora devi fare una ricerca lineare della catena hash. Se hai 26.000.000 di valori distinti per uguale () e 26.000 valori distinti per hashCode (), le catene di bucket avranno 1.000 oggetti ciascuna.
kdgregory

@ Nash0: Sembra che tu stia dicendo che vuoi che questi abbiano lo stesso hashCode ma allo stesso tempo non siano uguali (come definito dal metodo equals ()). Perché dovresti volerlo?
MAK

17

Per approfondire Pascal: capisci come funziona una HashMap? Hai un certo numero di slot nella tua tabella hash. Il valore hash per ogni chiave viene trovato e quindi mappato a una voce nella tabella. Se due valori hash vengono mappati alla stessa voce, una "collisione hash", HashMap crea un elenco collegato.

Le collisioni hash possono interrompere le prestazioni di una mappa hash. Nel caso estremo, se tutte le tue chiavi hanno lo stesso codice hash, o se hanno codici hash diversi ma mappano tutti allo stesso slot, la tua mappa hash si trasforma in un elenco collegato.

Quindi, se riscontri problemi di prestazioni, la prima cosa che controllo è: sto ricevendo una distribuzione casuale di codici hash? In caso contrario, è necessaria una funzione hash migliore. Ebbene, "migliore" in questo caso può significare "migliore per il mio particolare insieme di dati". Ad esempio, supponi di lavorare con le stringhe e di prendere la lunghezza della stringa per il valore hash. (Non come funziona String.hashCode di Java, ma sto solo inventando un semplice esempio.) Se le tue stringhe hanno lunghezze molto variabili, da 1 a 10.000, e sono distribuite in modo abbastanza uniforme su quell'intervallo, questo potrebbe essere un ottimo funzione hash. Ma se le tue stringhe sono tutte 1 o 2 caratteri, questa sarebbe una pessima funzione hash.

Modifica: dovrei aggiungere: ogni volta che aggiungi una nuova voce, HashMap controlla se si tratta di un duplicato. Quando si verifica una collisione di hash, deve confrontare la chiave in arrivo con ogni chiave mappata a quello slot. Quindi, nel caso peggiore in cui tutto si hash su un singolo slot, la seconda chiave viene confrontata con la prima chiave, la terza chiave viene confrontata con # 1 e # 2, la quarta chiave viene confrontata con # 1, # 2 e # 3 , ecc. Quando arrivi alla chiave numero 1 milione, hai fatto oltre un trilione di confronti.

@ Oscar: Umm, non vedo come sia un "non proprio". È più come un "lasciami chiarire". Ma sì, è vero che se fai una nuova voce con la stessa chiave di una voce esistente, questa sovrascrive la prima voce. Questo è ciò che intendevo quando ho parlato della ricerca di duplicati nell'ultimo paragrafo: ogni volta che una chiave esegue l'hashing nello stesso slot, HashMap deve controllare se è un duplicato di una chiave esistente o se si trovano nello stesso slot per coincidenza del funzione hash. Non so se questo sia il "punto intero" di una HashMap: direi che il "punto intero" è che puoi recuperare rapidamente gli elementi per chiave.

Ma comunque, ciò non influisce sul "punto intero" che stavo cercando di fare: quando hai due chiavi - sì, chiavi diverse, non la stessa chiave che viene mostrata di nuovo - quella mappa allo stesso slot nella tabella , HashMap crea un elenco collegato. Quindi, poiché deve controllare ogni nuova chiave per vedere se è effettivamente un duplicato di una chiave esistente, ogni tentativo di aggiungere una nuova voce che mappa a questo stesso slot deve inseguire l'elenco collegato esaminando ogni voce esistente per vedere se questo è un duplicato di una chiave vista in precedenza o se si tratta di una nuova chiave.

Aggiorna molto dopo il post originale

Ho appena ricevuto un voto positivo su questa risposta 6 anni dopo la pubblicazione, il che mi ha portato a rileggere la domanda.

La funzione hash data nella domanda non è un buon hash per 26 milioni di voci.

Somma a [0] + a [1] eb [0] + b [1] + b [2]. Dice che i valori di ciascun byte vanno da 0 a 51, in modo che dia solo (51 * 2 + 1) * (51 * 3 + 1) = 15.862 possibili valori hash. Con 26 milioni di voci, ciò significa una media di circa 1639 voci per valore hash. Si tratta di molte e molte collisioni, che richiedono molte, molte ricerche sequenziali tramite elenchi collegati.

L'OP dice che diversi ordini all'interno dell'array ae dell'array b dovrebbero essere considerati uguali, cioè [[1,2], [3,4,5]]. Uguale ([[2,1], [5,3,4] ]), quindi per adempiere al contratto devono avere codici hash uguali. Va bene. Tuttavia, ci sono molti più di 15.000 valori possibili. La sua seconda funzione hash proposta è molto migliore, offrendo una gamma più ampia.

Sebbene come ha commentato qualcun altro, sembra inappropriato per una funzione hash modificare altri dati. Avrebbe più senso "normalizzare" l'oggetto quando viene creato, o fare in modo che la funzione hash funzioni dalle copie degli array. Inoltre, l'utilizzo di un ciclo per calcolare le costanti ogni volta attraverso la funzione è inefficiente. Poiché ci sono solo quattro valori qui, avrei scritto entrambi

return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;

che farebbe sì che il compilatore esegua il calcolo una volta durante la compilazione; o avere 4 costanti statiche definite nella classe.

Inoltre, la prima bozza in una funzione hash ha diversi calcoli che non fanno nulla da aggiungere alla gamma di output. Nota che prima imposta hash = 503 che moltiplica per 5381 prima ancora di considerare i valori della classe. Quindi ... in effetti aggiunge 503 * 5381 a ogni valore. Cosa fa questo? L'aggiunta di una costante a ogni valore hash brucia semplicemente i cicli della CPU senza realizzare nulla di utile. Lezione qui: aggiungere complessità a una funzione hash non è l'obiettivo. L'obiettivo è ottenere un'ampia gamma di valori diversi, non solo aggiungere complessità per il bene della complessità.


3
Sì, una cattiva funzione hash comporterebbe questo tipo di comportamento. +1
Henning

Non proprio. L'elenco viene creato solo se l'hash è lo stesso, ma la chiave è diversa . Ad esempio, se una stringa fornisce un codice hash 2345 e un numero intero fornisce lo stesso codice hash 2345, il numero intero viene inserito nell'elenco perché String.equals( Integer )è false. Ma se hai la stessa classe (o almeno .equalsrestituisce true), viene utilizzata la stessa voce. Ad esempio new String("one")e `new String (" uno ") usato come chiavi, utilizzerà la stessa voce. In realtà questo è il punto INTERO di HashMap in primo luogo! Guarda tu stesso: pastebin.com/f20af40b9
OscarRyz

3
@Oscar: vedi la mia risposta allegata al mio post originale.
Jay,

So che questo è un thread molto vecchio, ma qui c'è un riferimento per il termine "collisione" in quanto si riferisce ai codici hash: link . Quando si sostituisce un valore in hashmap mettendo un altro valore con stessa chiave, è non chiama collisione
Tahir Akhtar

@Tahir esattamente. Forse il mio post era scritto male. Grazie per il chiarimento.
Jay

7

La mia prima idea è assicurarmi di inizializzare la tua HashMap in modo appropriato. Da JavaDocs per HashMap :

Un'istanza di HashMap ha due parametri che influiscono sulle sue prestazioni: capacità iniziale e fattore di carico. La capacità è il numero di bucket nella tabella hash e la capacità iniziale è semplicemente la capacità al momento della creazione della tabella hash. Il fattore di carico è una misura di quanto è consentito riempire la tabella hash prima che la sua capacità venga automaticamente aumentata. Quando il numero di voci nella tabella hash supera il prodotto del fattore di carico e la capacità corrente, la tabella hash viene modificata (ovvero, le strutture dati interne vengono ricostruite) in modo che la tabella hash abbia circa il doppio del numero di bucket.

Quindi, se stai iniziando con una HashMap troppo piccola, ogni volta che deve essere ridimensionata, tutti gli hash vengono ricalcolati ... che potrebbe essere quello che ti senti quando arrivi al punto di inserimento 2-3 milioni.


Non credo che vengano ricalcolati, mai. La dimensione della tabella viene aumentata, gli hash vengono mantenuti.
Henning

Hashmap fa solo un bit-saggio e per ogni voce: newIndex = storedHash & newLength;
Henning

4
Hanning: Forse una formulazione povera da parte di Delfuego, ma il punto è valido. Sì, i valori hash non vengono ricalcolati nel senso che l'output di hashCode () non viene ricalcolato. Ma quando la dimensione della tabella viene aumentata, tutte le chiavi devono essere reinserite nella tabella, ovvero il valore hash deve essere nuovamente hash per ottenere un nuovo numero di slot nella tabella.
Jay

Jay, sì - davvero povera di parole, e quello che hai detto. :)
delfuego

1
@delfuego e @ nash0: Sì, impostando la capacità iniziale uguale al numero di elementi diminuisce le prestazioni perché stai avendo tonnellate di milioni di collisioni e quindi stai usando solo una piccola quantità di quella capacità. Anche se si utilizzano tutte le voci disponibili, impostare la stessa capacità renderà le cose peggiori !, perché a causa del fattore di carico verrà richiesto più spazio. Dovrai usare initialcapactity = maxentries/loadcapacity(come 30 M, 0,95 per 26 milioni di voci) ma questo NON è il tuo caso, dal momento che stai avendo tutte quelle collisioni che stai usando solo circa 20k o meno.
OscarRyz

7

Suggerirei un approccio su tre fronti:

  1. Esegui Java con più memoria: java -Xmx256Mad esempio per eseguire con 256 Megabyte. Usane di più se necessario e hai molta RAM.

  2. Memorizza nella cache i valori hash calcolati come suggerito da un altro poster, quindi ogni oggetto calcola il suo valore hash solo una volta.

  3. Usa un algoritmo di hashing migliore. Quello che hai pubblicato restituirebbe lo stesso hash dove a = {0, 1} come se a = {1, 0}, a parità di tutte le altre.

Utilizza ciò che Java ti offre gratuitamente.

public int hashCode() {
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}

Sono abbastanza sicuro che questo ha molte meno possibilità di scontro rispetto al tuo metodo hashCode esistente, sebbene dipenda dalla natura esatta dei tuoi dati.


La RAM potrebbe essere troppo piccola per questo tipo di mappe e array, quindi sospettavo già un problema di limitazione della memoria.
ReneS

7

Entrare nella zona grigia di "on / off topic", ma è necessario eliminare la confusione riguardo al suggerimento di Oscar Reyes che più collisioni di hash siano una buona cosa perché riduce il numero di elementi nella HashMap. Potrei fraintendere quello che sta dicendo Oscar, ma non mi sembra di essere l'unico: kdgregory, delfuego, Nash0, e sembra che condividiamo tutti la stessa (mis) comprensione.

Se capisco cosa sta dicendo Oscar sulla stessa classe con lo stesso codice hash, sta proponendo che solo un'istanza di una classe con un dato codice hash verrà inserita in HashMap. Ad esempio, se ho un'istanza di SomeClass con un hashcode di 1 e una seconda istanza di SomeClass con un hashcode di 1, viene inserita solo un'istanza di SomeClass.

L'esempio di Java Pastebin su http://pastebin.com/f20af40b9 sembra indicare che quanto sopra riassume correttamente ciò che Oscar propone.

Indipendentemente da qualsiasi comprensione o malinteso, ciò che accade è che istanze diverse della stessa classe non vengono inserite una sola volta in HashMap se hanno lo stesso codice hash, non finché non viene determinato se le chiavi sono uguali o meno. Il contratto hashcode richiede che oggetti uguali abbiano lo stesso hashcode; tuttavia, non richiede che oggetti disuguali abbiano codici hash diversi (sebbene ciò possa essere desiderabile per altri motivi) [1].

Segue l'esempio pastebin.com/f20af40b9 (a cui Oscar si riferisce almeno due volte), ma leggermente modificato per utilizzare le asserzioni JUnit piuttosto che le righe di stampa. Questo esempio viene utilizzato per supportare la proposta che gli stessi codici hash causino collisioni e quando le classi sono le stesse viene creata solo una voce (ad esempio, solo una stringa in questo caso specifico):

@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
    String s = new String("ese");
    String ese = new String("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // AND equal
    assertTrue(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(2, map.size());

    assertEquals(2, map.get("ese"));
    assertEquals(3, map.get(some));

    assertTrue(s.equals(ese) && s.equals("ese"));
}

class SomeClass {
    public int hashCode() {
        return 100727;
    }
}

Tuttavia, il codice hash non è la storia completa. Ciò che l'esempio di pastebin trascura è il fatto che entrambi se esesono uguali: sono entrambi la stringa "ese". Pertanto, l'inserimento o il recupero del contenuto della mappa utilizzando so eseo "ese"come chiave sono tutti equivalenti perché s.equals(ese) && s.equals("ese").

Un secondo test dimostra che è errato concludere che codici hash identici sulla stessa classe sono la ragione per cui il valore chiave -> s -> 1viene sovrascritto da ese -> 2quando map.put(ese, 2)viene chiamato nel test uno. Nella seconda prova se esehanno ancora lo stesso codice hash (verificato da assertEquals(s.hashCode(), ese.hashCode());) E sono la stessa classe. Tuttavia, se esesono MyStringistanze in questo test, non Stringistanze Java - con l'unica differenza rilevante per questo test che è uguale: String s equals String esenel test uno sopra, mentre MyStrings s does not equal MyString esenel test due:

@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
    MyString s = new MyString("ese");
    MyString ese = new MyString("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // BUT not equal
    assertFalse(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(3, map.size());

    assertEquals(1, map.get(s));
    assertEquals(2, map.get(ese));
    assertEquals(3, map.get(some));
}

/**
 * NOTE: equals is not overridden so the default implementation is used
 * which means objects are only equal if they're the same instance, whereas
 * the actual Java String class compares the value of its contents.
 */
class MyString {
    String i;

    MyString(String i) {
        this.i = i;
    }

    @Override
    public int hashCode() {
        return 100727;
    }
}

Sulla base di un commento successivo, Oscar sembra invertire ciò che ha detto prima e riconosce l'importanza della parità. Tuttavia, sembra ancora che ciò che conta è la nozione di uguale, non la "stessa classe", non è chiara (enfasi mia):

"Non proprio. L'elenco viene creato solo se l'hash è lo stesso, ma la chiave è diversa. Ad esempio, se una stringa fornisce il codice hash 2345 e e Integer fornisce lo stesso codice hash 2345, il numero intero viene inserito nell'elenco perché String. equals (Integer) è falso. Ma se hai la stessa classe (o almeno .equals restituisce true), viene utilizzata la stessa voce. Ad esempio new String ("one") e `new String (" one ") usati come keys, utilizzerà la stessa voce. In realtà questo è il punto INTERO di HashMap in primo luogo! Guarda tu stesso: pastebin.com/f20af40b9 - Oscar Reyes "

rispetto ai commenti precedenti che affrontano esplicitamente l'importanza di una classe identica e dello stesso codice hash, senza menzione di uguali:

"@delfuego: guarda tu stesso: pastebin.com/f20af40b9 Quindi, in questa domanda viene utilizzata la stessa classe (aspetta un minuto, viene utilizzata la stessa classe, giusto?) Il che implica che quando viene utilizzato lo stesso hash viene utilizzata la stessa voce viene utilizzato e non c'è "lista" di voci. - Oscar Reyes "

o

"In realtà questo aumenterebbe le prestazioni. Più collisioni eq meno voci nell'equalizzazione della tabella hash. Meno lavoro da fare. Non è l'hash (che sembra a posto) né la tabella hash (che funziona alla grande) Scommetto che è sull'oggetto creazione in cui la performance è degradante. - Oscar Reyes "

o

"@kdgregory: Sì, ma solo se la collisione avviene con classi diverse, per la stessa classe (che è il caso) viene utilizzata la stessa voce. - Oscar Reyes"

Di nuovo, potrei fraintendere ciò che Oscar stava effettivamente cercando di dire. Tuttavia, i suoi commenti originali hanno causato tanta confusione che sembra prudente chiarire tutto con alcuni test espliciti, quindi non ci sono dubbi persistenti.


[1] - Da Effective Java, seconda edizione di Joshua Bloch:

  • Ogni volta che viene invocato sullo stesso oggetto più di una volta durante l'esecuzione di un'applicazione, il metodo hashCode deve restituire costantemente lo stesso numero intero, a condizione che non venga modificata alcuna informazione utilizzata in confronti uguali sull'oggetto. Questo numero intero non deve rimanere coerente da un'esecuzione di un'applicazione a un'altra esecuzione della stessa applicazione.

  • Se due oggetti sono uguali secondo il metodo equal s (Obj ect), la chiamata al metodo hashCode su ciascuno dei due oggetti deve produrre lo stesso risultato intero.

  • Non è necessario che se due oggetti sono disuguali secondo il metodo equal s (Object), la chiamata del metodo hashCode su ciascuno dei due oggetti deve produrre risultati interi distinti. Tuttavia, il programmatore deve essere consapevole del fatto che la produzione di risultati interi distinti per oggetti diversi può migliorare le prestazioni delle tabelle hash.


5

Se gli array nel tuo hashCode pubblicato sono byte, probabilmente ti ritroverai con molti duplicati.

a [0] + a [1] sarà sempre compreso tra 0 e 512. aggiungendo le b si otterrà sempre un numero compreso tra 0 e 768. Moltiplica quelli e ottieni un limite massimo di 400.000 combinazioni uniche, supponendo che i tuoi dati siano perfettamente distribuiti tra ogni possibile valore di ogni byte. Se i tuoi dati sono del tutto regolari, probabilmente avrai risultati molto meno unici di questo metodo.


4

HashMap ha una capacità iniziale e le prestazioni di HashMap dipendono molto da hashCode che produce gli oggetti sottostanti.

Prova a modificare entrambi.


4

Se le chiavi hanno uno schema, puoi dividere la mappa in mappe più piccole e avere una mappa indice.

Esempio: Chiavi: 1,2,3, .... n 28 mappe da 1 milione ciascuna. Mappa indice: 1-1.000.000 -> Mappa1 1.000.000-2.000.000 -> Mappa2

Quindi farai due ricerche ma il set di chiavi sarebbe 1.000.000 contro 28.000.000. Puoi farlo facilmente anche con i modelli di puntura.

Se le chiavi sono completamente casuali, questo non funzionerà


1
Anche se le chiavi sono casuali, puoi usare (key.hashCode ()% 28) per selezionare una mappa in cui memorizzare quel valore-chiave.
Juha Syrjälä

4

Se i due array di byte che menzioni sono la tua intera chiave, i valori sono compresi nell'intervallo 0-51, unici e l'ordine all'interno degli array aeb è insignificante, la mia matematica mi dice che ci sono solo circa 26 milioni di possibili permutazioni e che probabilmente stai cercando di riempire la mappa con i valori per tutte le chiavi possibili.

In questo caso, sia il riempimento che il recupero dei valori dall'archivio dati sarebbero ovviamente molto più veloci se si utilizza un array invece di una HashMap e lo si indicizza da 0 a 25989599.


Questa è un'ottima idea, e in effetti lo sto facendo per un altro problema di archiviazione dei dati con 1,2 miliardi di elementi. In questo caso volevo prendere la via più semplice e utilizzare una struttura dati
premade

4

Sono in ritardo qui, ma un paio di commenti sulle grandi mappe:

  1. Come discusso a lungo in altri post, con un buon hashCode (), 26 milioni di voci in una mappa non sono un grosso problema.
  2. Tuttavia, un problema potenzialmente nascosto qui è l'impatto GC delle mappe giganti.

Suppongo che queste mappe siano longeve. cioè li popoli e rimangono per tutta la durata dell'app. Suppongo anche che l'app stessa sia longeva, come un server di qualche tipo.

Ogni voce in una Java HashMap richiede tre oggetti: la chiave, il valore e la voce che li lega insieme. Quindi 26 milioni di voci nella mappa significano 26 milioni * 3 == 78 milioni di oggetti. Questo va bene finché non raggiungi un GC completo. Allora hai un problema di pausa nel mondo. Il GC esaminerà ciascuno degli oggetti 78M e determinerà che sono tutti vivi. 78M + oggetti sono solo molti oggetti da guardare. Se la tua app può tollerare pause occasionali lunghe (forse molti secondi), non ci sono problemi. Se stai cercando di ottenere garanzie di latenza potresti avere un grosso problema (ovviamente se vuoi garanzie di latenza, Java non è la piattaforma da scegliere :)) Se i valori nelle tue mappe cambiano rapidamente puoi finire con frequenti raccolte complete il che aggrava notevolmente il problema.

Non conosco una grande soluzione a questo problema. idee:

  • A volte è possibile regolare GC e le dimensioni dell'heap per impedire "principalmente" i GC completi.
  • Se i contenuti della tua mappa cambiano molto, potresti provare FastMap di Javolution : può raggruppare oggetti Entry, il che potrebbe ridurre la frequenza delle raccolte complete
  • Potresti creare la tua mappa impl e fare una gestione esplicita della memoria su byte [] (cioè scambiare cpu per una latenza più prevedibile serializzando milioni di oggetti in un singolo byte [] - ugh!)
  • Non utilizzare Java per questa parte: parla con una sorta di prevedibile DB in memoria su un socket
  • Spero che il nuovo raccoglitore G1 possa aiutare (si applica principalmente al caso ad alto tasso di abbandono)

Solo alcuni pensieri di qualcuno che ha trascorso molto tempo con le mappe giganti in Java.



3

Nel mio caso voglio creare una mappa con 26 milioni di voci. Utilizzando lo standard Java HashMap, la velocità di inserimento diventa insopportabilmente lenta dopo 2-3 milioni di inserimenti.

Dal mio esperimento (progetto studentesco nel 2009):

  • Ho costruito un albero rosso nero per 100.000 nodi da 1 a 100.000. Ci sono voluti 785,68 secondi (13 minuti). E non sono riuscito a creare RBTree per 1 milione di nodi (come i tuoi risultati con HashMap).
  • Utilizzando "Prime Tree", la struttura dei dati del mio algoritmo. Potrei costruire un albero / mappa per 10 milioni di nodi entro 21,29 secondi (RAM: 1,97 Gb). Il costo del valore-chiave di ricerca è O (1).

Nota: "Prime Tree" funziona meglio su "tasti continui" da 1 a 10 milioni. Per lavorare con chiavi come HashMap abbiamo bisogno di alcuni aggiustamenti minori.


Allora, cos'è #PrimeTree? In breve, è una struttura dati ad albero come Binary Tree, con i numeri dei rami sono numeri primi (invece di "2" -binary).


Potresti condividere qualche link o implementazione?
Benj



1

Hai considerato l'utilizzo di un database incorporato per farlo. Guarda Berkeley DB . È open-source, di proprietà di Oracle ora.

Memorizza tutto come coppia Chiave-> Valore, NON è un RDBMS. e mira ad essere veloce.


2
Berkeley DB non è neanche lontanamente abbastanza veloce per questo numero di voci a causa del sovraccarico di serializzazione / IO; non potrebbe mai essere più veloce di una hashmap e l'OP non si preoccupa della persistenza. Il tuo suggerimento non è buono.
oxbow_lakes

1

Per prima cosa dovresti controllare che stai usando Map correttamente, un buon metodo hashCode () per le chiavi, capacità iniziale per Map, corretta implementazione di Map ecc. Come descrivono molte altre risposte.

Quindi suggerirei di utilizzare un profiler per vedere cosa sta effettivamente accadendo e dove viene trascorso il tempo di esecuzione. Ad esempio, il metodo hashCode () viene eseguito miliardi di volte?

Se ciò non aiuta, che ne dici di usare qualcosa come EHCache o memcached ? Sì, sono prodotti per la memorizzazione nella cache, ma è possibile configurarli in modo che abbiano una capacità sufficiente e non rimuoveranno mai alcun valore dalla memoria cache.

Un'altra opzione potrebbe essere un motore di database più leggero di un RDBMS SQL completo. Qualcosa come Berkeley DB , forse.

Nota che personalmente non ho esperienza delle prestazioni di questi prodotti, ma potrebbe valere la pena provarli.


1

Potresti provare a memorizzare nella cache il codice hash calcolato nell'oggetto chiave.

Qualcosa come questo:

public int hashCode() {
  if(this.hashCode == null) {
     this.hashCode = computeHashCode();
  }
  return this.hashCode;
}

private int computeHashCode() {
   int hash = 503;
   hash = hash * 5381 + (a[0] + a[1]);
   hash = hash * 5381 + (b[0] + b[1] + b[2]);
   return hash;
}

Ovviamente devi stare attento a non modificare il contenuto della chiave dopo che hashCode è stato calcolato per la prima volta.

Modifica: sembra che la memorizzazione nella cache abbia valori di codice non è utile quando si aggiunge ogni chiave solo una volta a una mappa. In qualche altra situazione questo potrebbe essere utile.


Come sottolineato di seguito, non c'è ricalcolo dei codici hash degli oggetti in una HashMap quando viene ridimensionata, quindi questo non ti fa guadagnare nulla.
delfuego

1

Un altro poster ha già sottolineato che l'implementazione del codice hash comporterà molte collisioni a causa del modo in cui stai aggiungendo valori insieme. Sono disposto a esserlo, se guardi l'oggetto HashMap in un debugger, scoprirai che hai forse 200 valori hash distinti, con catene di bucket estremamente lunghe.

Se hai sempre valori nell'intervallo 0..51, ciascuno di questi valori richiederà 6 bit per essere rappresentato. Se hai sempre 5 valori, puoi creare un codice hash a 30 bit con spostamenti a sinistra e aggiunte:

    int code = a[0];
    code = (code << 6) + a[1];
    code = (code << 6) + b[0];
    code = (code << 6) + b[1];
    code = (code << 6) + b[2];
    return code;

Lo spostamento a sinistra è veloce, ma ti lascerà con codici hash che non sono distribuiti uniformemente (perché 6 bit implica un intervallo 0..63). Un'alternativa è moltiplicare l'hash per 51 e aggiungere ogni valore. Questo non sarà ancora perfettamente distribuito (ad esempio, {2,0} e {1,52} si scontreranno) e sarà più lento dello spostamento.

    int code = a[0];
    code *= 51 + a[1];
    code *= 51 + b[0];
    code *= 51 + b[1];
    code *= 51 + b[2];
    return code;

@kdgregory: ho risposto a proposito del "più collisioni implica più lavoro" da qualche altra parte :)
OscarRyz

1

Come sottolineato, l'implementazione del codice hash ha troppe collisioni e risolverlo dovrebbe portare a prestazioni decenti. Inoltre, la memorizzazione nella cache degli hashCodes e l'implementazione efficiente degli uguali aiuteranno.

Se hai bisogno di ottimizzare ulteriormente:

Secondo la tua descrizione, ci sono solo (52 * 51/2) * (52 * 51 * 50/6) = 29304600 chiavi diverse (di cui 26000000, ovvero circa il 90%, saranno presenti). Pertanto, è possibile progettare una funzione hash senza collisioni e utilizzare un semplice array anziché una hashmap per contenere i dati, riducendo il consumo di memoria e aumentando la velocità di ricerca:

T[] array = new T[Key.maxHashCode];

void put(Key k, T value) {
    array[k.hashCode()] = value;

T get(Key k) {
    return array[k.hashCode()];
}

(In generale, è impossibile progettare una funzione hash efficiente e priva di collisioni che si raggruppa bene, motivo per cui una HashMap tollera le collisioni, che comportano un certo sovraccarico)

Supponendo ae bsiano ordinati, potresti utilizzare la seguente funzione hash:

public int hashCode() {
    assert a[0] < a[1]; 
    int ahash = a[1] * a[1] / 2 
              + a[0];

    assert b[0] < b[1] && b[1] < b[2];

    int bhash = b[2] * b[2] * b[2] / 6
              + b[1] * b[1] / 2
              + b[0];
    return bhash * 52 * 52 / 2 + ahash;
}

static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;  

Penso che questo sia privo di collisioni. Dimostrare questo è lasciato come esercizio per il lettore incline alla matematica.


1

In Effective Java: Guida al linguaggio di programmazione (serie Java)

Nel Capitolo 3 puoi trovare buone regole da seguire quando si calcola hashCode ().

Appositamente:

Se il campo è un array, trattalo come se ogni elemento fosse un campo separato. Cioè, calcola un codice hash per ogni elemento significativo applicando queste regole in modo ricorsivo e combina questi valori al passaggio 2.b. Se ogni elemento in un campo array è significativo, è possibile utilizzare uno dei metodi Arrays.hashCode aggiunti nella versione 1.5.


0

Assegna una grande mappa all'inizio. Se sai che avrà 26 milioni di voci e ne hai la memoria, fai un file new HashMap(30000000).

Sei sicuro di avere memoria sufficiente per 26 milioni di voci con 26 milioni di chiavi e valori? Questo suona come un sacco di memoria per me. Sei sicuro che la raccolta dei rifiuti stia ancora andando bene al tuo segno di 2 o 3 milioni? Potrei immaginarlo come un collo di bottiglia.


2
Oh, un'altra cosa. I tuoi codici hash devono essere distribuiti uniformemente per evitare grandi elenchi collegati in singole posizioni nella mappa.
ReneS

0

Potresti provare due cose:

  • Fai in modo che il tuo hashCodemetodo restituisca qualcosa di più semplice ed efficace come un int consecutivo

  • Inizializza la mappa come:

    Map map = new HashMap( 30000000, .95f );

Queste due azioni ridurranno enormemente la quantità di rimaneggiamenti che la struttura sta facendo e sono abbastanza facili da testare credo.

Se non funziona, prendi in considerazione l'utilizzo di uno storage diverso come RDBMS.

MODIFICARE

È strano che l'impostazione della capacità iniziale riduca le prestazioni nel tuo caso.

Vedi dai javadoc :

Se la capacità iniziale è maggiore del numero massimo di voci diviso per il fattore di carico, non si verificheranno mai operazioni di rehash.

Ho fatto un microbeachmark (che non è affatto definitivo ma almeno dimostra questo punto)

$cat Huge*java
import java.util.*;
public class Huge {
    public static void main( String [] args ) {
        Map map = new HashMap( 30000000 , 0.95f );
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
import java.util.*;
public class Huge2 {
    public static void main( String [] args ) {
        Map map = new HashMap();
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
$time java -Xms2g -Xmx2g Huge

real    0m16.207s
user    0m14.761s
sys 0m1.377s
$time java -Xms2g -Xmx2g Huge2

real    0m21.781s
user    0m20.045s
sys 0m1.656s
$

Quindi, l'utilizzo della capacità iniziale scende da 21 a 16 secondi a causa del rehasing. Questo ci lascia con il tuo hashCodemetodo come "area di opportunità";)

MODIFICARE

Non è HashMap

Come nella tua ultima edizione.

Penso che dovresti davvero profilare la tua applicazione e vedere dove viene consumata la memoria / cpu.

Ho creato una classe che implementa la tua stessa hashCode

Quel codice hash dà milioni di collisioni, quindi le voci in HashMap vengono ridotte drasticamente.

Passo da 21, 16 nel mio test precedente a 10 e 8. Il motivo è perché l'hashCode provoca un numero elevato di collisioni e non stai memorizzando gli oggetti 26M che pensi ma un numero inferiore molto significativo (circa 20k direi) Quindi:

I problemi NON SONO L'HASHMAP è da qualche altra parte nel tuo codice.

È giunto il momento di ottenere un profiler e scoprire dove. Penso che sia sulla creazione dell'elemento o probabilmente stai scrivendo su disco o ricevendo dati dalla rete.

Ecco la mia implementazione della tua classe.

nota che non ho usato un intervallo da 0 a 51 come hai fatto tu, ma da -126 a 127 per i miei valori e ammetto ripetuto, questo perché ho fatto questo test prima di aggiornare la tua domanda

L'unica differenza è che la tua classe avrà più collisioni e quindi meno oggetti memorizzati nella mappa.

import java.util.*;
public class Item {

    private static byte w = Byte.MIN_VALUE;
    private static byte x = Byte.MIN_VALUE;
    private static byte y = Byte.MIN_VALUE;
    private static byte z = Byte.MIN_VALUE;

    // Just to avoid typing :) 
    private static final byte M = Byte.MAX_VALUE;
    private static final byte m = Byte.MIN_VALUE;


    private byte [] a = new byte[2];
    private byte [] b = new byte[3];

    public Item () {
        // make a different value for the bytes
        increment();
        a[0] = z;        a[1] = y;    
        b[0] = x;        b[1] = w;   b[2] = z;
    }

    private static void increment() {
        z++;
        if( z == M ) {
            z = m;
            y++;
        }
        if( y == M ) {
            y = m;
            x++;
        }
        if( x == M ) {
            x = m;
            w++;
        }
    }
    public String toString() {
        return "" + this.hashCode();
    }



    public int hashCode() {
        int hash = 503;
        hash = hash * 5381 + (a[0] + a[1]);
        hash = hash * 5381 + (b[0] + b[1] + b[2]);
        return hash;
    }
    // I don't realy care about this right now. 
    public boolean equals( Object other ) {
        return this.hashCode() == other.hashCode();
    }

    // print how many collisions do we have in 26M items.
    public static void main( String [] args ) {
        Set set = new HashSet();
        int collisions = 0;
        for ( int i = 0 ; i < 26000000 ; i++ ) {
            if( ! set.add( new Item() ) ) {
                collisions++;
            }
        }
        System.out.println( collisions );
    }
}

L'uso di questa classe ha la chiave per il programma precedente

 map.put( new Item() , i );

mi da:

real     0m11.188s
user     0m10.784s
sys 0m0.261s


real     0m9.348s
user     0m9.071s
sys  0m0.161s

3
Oscar, come sottolineato altrove sopra (in risposta ai tuoi commenti), sembra che tu stia assumendo che più collisioni siano BUONE; è molto NON buono. Una collisione significa che lo slot in un dato hash va da contenere una singola voce a contenere un elenco di voci, e questo elenco deve essere cercato / attraversato ogni volta che si accede allo slot.
delfuego

@delfuego: Non proprio, succede solo quando si verifica una collisione utilizzando classi diverse ma per la stessa classe viene utilizzata la stessa voce;)
OscarRyz

2
@Oscar - guarda la mia risposta con la risposta di MAK. HashMap mantiene un elenco collegato di voci in ogni bucket hash e percorre tale elenco chiamando equals () su ogni elemento. La classe dell'oggetto non ha nulla a che fare con esso (a parte un cortocircuito su uguale ()).
kdgregory

1
@Oscar - Leggendo la tua risposta sembra che tu stia assumendo che equals () restituirà true se gli hashcode sono gli stessi. Questo non fa parte del contratto uguale / hashcode. Se ho capito male, ignora questo commento.
kdgregory

1
Grazie mille per lo sforzo Oscar, ma penso che tu stia confondendo gli oggetti chiave a parità di condizioni rispetto allo stesso codice hash. Inoltre in uno dei tuoi collegamenti di codice stai usando stringhe uguali come chiave, ricorda che le stringhe in Java sono immutabili. Penso che entrambi abbiamo imparato molto
sull'hashing


0

Qualche tempo fa ho fatto un piccolo test con una lista contro una hashmap, la cosa divertente è stata scorrere l'elenco e trovare l'oggetto ha impiegato lo stesso tempo in millisecondi dell'uso della funzione hashmaps get ... solo un fyi. Oh sì, la memoria è un grosso problema quando si lavora con hashmap di quelle dimensioni.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.