2015-07-23 24 views
5

Problema:Come posso valutare un'implementazione della tabella hash? (Utilizzo HashMap come riferimento)

  • devo confrontare 2 implementazioni tabella hash (ben sostanzialmente HashMap con un altro) e fare una conclusione ragionevole.

  • Non mi interessa l'accuratezza al 100% ma sto solo nella giusta direzione nella mia stima.

  • Sono interessato alla differenza non solo per operazione ma principalmente sulla tabella come un "intero".

  • Non ho un requisito rigoroso sulla velocità quindi se l'altra implementazione è ragionevolmente più lento posso accettare, ma ho fare aspettarsi/richiedono che l'utilizzo della memoria sia migliore (in quanto uno dei hashtables è supportato dalla tabella primitiva).

quello che ho fatto finora:

Inizialmente ho creato il mio "punto di riferimento" personalizzato con loop e molte chiamate al suggerimento per gc per avere un'idea della differenza, ma io sto leggendo online che l'utilizzo di uno strumento standard è più affidabile/appropriato.
Esempio del mio approccio (MapInterface è solo un wrapper così posso passare tra le implementazioni.):

int[] keys = new int[10000000]; 
String[] values = new String[10000000]; 
for(int i = 0; i < keys.length; ++i) { 
    keys[i] = i; 
    values[i] = "" + i; 
} 

if(operation.equals("put", keys, values)) { 
    runPutOperation(map); 
} 

public static long[] runOperation(MapInterface map, Integer[] keys, String[] values) { 
    long min = Long.MAX_VALUE; 
    long max = Long.MIN_VALUE; 
    long run = 0; 
    for(int i = 0; i < 10; ++i) { 
     long start = System.currentTimeMillis(); 
     for(int i = 0; i < keys.length; ++i) {   
      map.put(keys[i], values[i]); 
     } 
     long total = System.currentTimeMillis() - start; 
     System.out.println(total/1000d + " seconds");  
     if(total < min) { 
      min = time; 
     } 
     if(total > max) { 
      max = time; 
     } 
     run += time; 
     map = null; 
     map = createNewHashMap(); 
     hintsToGC();  
    } 
    return new long[] {min, max, run}; 
}  


public void hintsToGC() { 
    for(int i = 0; i < 20; ++i) { 
      System.out.print(". "); 
      System.gc();    
      try { 
       Thread.sleep(100); 
      } catch (InterruptedException e) {    
       e.printStackTrace(); 
      }   
     } 
} 


private HashMapInterface<String> createNewHashMap() { 
    if(jdk) { 
     return new JDKHashMapWrapper<String>(); 
    } 
    else { 
     return new AlternativeHashMapWrapper<String>(); 
    } 
} 



public class JDKHashMapWrapper implements HashMapInterface<String> { 
    HashMap<Integer, String> hashMap;   
    JDKHashMapWrapper() { 
     hashMap = new HashMap<Integer, String>(); 
    } 
    public String put(Integer key, String value) { 
     return hashMap.put(key, value); 
    } 
//etc 
} 

(Voglio testare put, get, contains e l'utilizzo di memoria)
posso essere sicuro da usando il mio approccio che posso ottenere misurazioni ragionevoli?
Se no quale sarebbe lo strumento più appropriato da usare e come?

Aggiornamento:
- I test anche con numeri casuali (anche ~ 10M numeri casuali) utilizzando SecureRandom.
- Quando la tabella hash ridimensiona stampo la dimensione logica della tabella hash/dimensione della tabella effettiva per ottenere il fattore di carico

Aggiornamento:
Per il mio caso specifico, dove sono interessati anche a interi cosa possono esserci delle insidie ​​con il mio approccio?

UPDATE dopo @ dimo414 commenta:

Bene al minimo la tabella hash come un "tutto" non è significativa

Voglio dire come la tabella hash si comporta sotto vari carichi sia a runtime e consumo di memoria.

Ogni struttura dati è un compromesso di diversi metodi

sono d'accordo.mio trade-off è una penalità di accesso accettabile per il miglioramento della memoria

è necessario identificare quali caratteristiche che ti interessa verificare

1) mettere (chiave, valore);
2) get (chiave, valore);
3) containsKey (chiave);
4) tutto quanto sopra quando si hanno molte voci nella tabella hash

+0

Una cosa che potresti fare sarebbe usare System.nanoTime() piuttosto che System.currentTimeMillis(). È più adatto per questo tipo di benchmarking. – bhspencer

+2

Credo che tu abbia visto http://stackoverflow.com/q/504103/113632? – dimo414

+0

@ dimo414: ho. 1) Suggerisce alcune opzioni JVM extra da utilizzare, quindi suppongo che il mio approccio con le opzioni JVM possa essere combinato per avere più confidenza 2) Ho controllato i framework nell'ultima regola. 'Bill e Paul's etc' fa più o meno lo stesso di quello che faccio io. Caliper è per me che è un utente inesperto e non molto esperto nel benchmarking di una black-box con documentazione non molto utile e dà apparentemente un micro benching per operazione. Non ho idea di come sarebbe stata testata la tabella hash. JHM, devo essere sincero, ho bisogno di leggere se può aiutarmi o meno – Cratylus

risposta

0

Stavo solo facendo qualcosa di simile a questo, e ho finito per utilizzare il profiler incorporato nel Netbeans IDE. È possibile ottenere informazioni molto dettagliate sull'utilizzo della CPU e della memoria. Originariamente avevo scritto tutto il mio codice in Eclipse, ma Netbeans ha una funzione di importazione per l'inserimento dei progetti Eclipse e non ha creato alcun problema, se questa è probabilmente anche la tua situazione.

Per i tempi, si potrebbe anche guardare la classe StopWatch in Apache Commons. E 'un modo molto più intuitivo di tempo di monitoraggio sulle operazioni mirate, ad esempio:

StopWatch myMapTimer = new StopWatch(); 
HashMap<Integer, Integer> hashMap = new HashMap<>(); 

myMapTimer.start(); 
for (int i = 0; i < numElements; i++) 
    hashMap.put(i, i); 
myMapTimer.stop(); 

System.out.println(myMapTimer.getTime()); // time will be in milliseconds 
+0

Esiste un altro vantaggio eccetto il codice più pulito che utilizza StopWatch? – Cratylus

+0

Non che io sappia, ma generalmente mi piace usare un'API stabilita, ridurre gli errori stupidi. Ci sono anche altre classi di StopWatch, in Guava e in Spring Framework. – aconkey

1

Alcuni considerazione chiave per l'utilizzo di tabelle hash è la dimensione della dotazione "secchi", la strategia di risoluzione di collisione, e la forma dei tuoi dati . In sostanza, una tabella hash prende la chiave fornita dall'applicazione e la blocca su un valore inferiore o uguale al numero di bucket allocati. Quando due valori chiave hash allo stesso bucket, l'implementazione deve risolvere la collisione e restituire il valore corretto. Ad esempio, si potrebbe avere una lista collegata ordinata per ciascun bucket e tale lista viene cercata.

Se i dati riscontrano numerose collisioni, le prestazioni ne risentiranno, poiché l'implementazione della tabella hash impiegherà troppo tempo a risolvere la collisione. D'altra parte, se si dispone di un numero molto elevato di bucket, si risolve il problema di collisione a scapito della memoria. Inoltre, l'implementazione HashMap integrata di Java eseguirà un "rehash" se il numero di voci diventerà maggiore di una certa quantità - immagino che questa sia un'operazione costosa che vale la pena di evitare.

Poiché i dati chiave sono i numeri interi positivi da 1 a 10 M, i dati del test sembrano buoni. Mi assicurerei inoltre che le diverse implementazioni delle tabelle hash fossero inizializzate con le stesse dimensioni del bucket per un determinato test, altrimenti non sarebbe un confronto equo. Infine, vorrei variare le dimensioni del bucket in un intervallo piuttosto significativo e rieseguire i test per vedere come le implementazioni hanno cambiato il loro comportamento.

+0

Punti validi. Forse dovrei aggiornare l'OP 1) Ho anche provato con numeri casuali (anche numeri casuali ~ 10M) usando SecureRandom. 2) Quando la tabella hash viene ridimensionata, stampo la dimensione logica della tabella hash/dimensione della tabella effettiva per ottenere il fattore di carico – Cratylus

+0

@Cratylus L'applicazione utilizzerà Integers come chiave per HashMap? – schtever

+0

I numeri interi Yest solo – Cratylus

1

Come ho capito, siete interessati sia al tempo di esecuzione delle operazioni che al consumo di memoria delle mappe nel test.

Inizierò con il consumo di memoria in quanto a queste cuciture non viene data alcuna risposta. Quello che propongo è di usare una piccola biblioteca chiamata Classmexer. L'ho usato personalmente quando ho bisogno di ottenere il consumo di memoria corretto al 100% di qualsiasi oggetto. Ha l'approccio agente java (perché è utilizzando l'API Strumentazione), il che significa che è necessario aggiungerlo come parametro per la JVM eseguendo i test:

-javaagent: [PATH_TO]/classmexer.jar 

L'uso del Classmexer è molto semplice. In qualsiasi punto del tempo è possibile ottenere il consumo di memoria in byte eseguendo:

MemoryUtil.deepMemoryUsageOf(mapIamInterestedIn, VisibilityFilter.ALL) 

Si noti che con filtro visibilità è possibile specificare se il calcolo di memoria dovrebbe essere fatto per l'oggetto (la nostra mappa) più tutti gli altri oggetti raggiungibili attraverso riferimentiQuesto è ciò che VisibilityFilter.ALL è per. Tuttavia, ciò significherebbe che la dimensione che si ottiene include tutti gli oggetti utilizzati per le chiavi e i valori. Pertanto, se si hanno 100 voci Integer/String, le dimensioni riportate includeranno anche quelle.

Per l'aspetto di temporizzazione, propongo lo strumento JMH, poiché questo strumento è realizzato per la micro-marcatura da banco. Ci sono molti esempi online, ad esempio this article ha esempi di test di mappe che possono guidarti piuttosto bene.

Nota che avrei dovuto fare attenzione quando si chiama memoria del Classmexer Util come essa possa interferire con i risultati in tempo se lo si chiama durante la misurazione del tempo. Inoltre, sono sicuro che ci sono molti altri strumenti simili a Classmexer, ma mi piace perché è piccolo e semplice.