Come posso scrivere un micro-benchmark corretto in Java?

Come si scrive (e si esegue) un corretto micro-benchmark in Java?

Sto cercando qui esempi di codice e commenti che illustrano varie cose su cui riflettere.

Esempio: il benchmark dovrebbe misurare il tempo / iterazione o iterazioni / tempo e perché?

Correlato: il benchmark degli stopwatch è accettabile?

Suggerimenti per la scrittura di micro benchmark dai creatori di Java HotSpot :

Regola 0: leggi un documento affidabile su JVM e micro-benchmarking. Uno buono è Brian Goetz, 2005 . Non aspettarti troppo dai micro-benchmark; misurano solo una gamma limitata di caratteristiche di prestazione JVM.

Regola 1: includere sempre una fase di riscaldamento che esegue il kernel di test fino in fondo, abbastanza per triggersre tutte le inizializzazioni e le compilazioni prima della fase di sincronizzazione. (Meno iterazioni sono OK nella fase di riscaldamento. La regola generale è diverse decine di migliaia di iterazioni del ciclo interno.)

Regola 2: -XX:+PrintCompilation sempre con -XX:+PrintCompilation , -verbose:gc , ecc., Così puoi verificare che il compilatore e le altre parti della JVM non stiano facendo un lavoro inaspettato durante la fase di timing.

Regola 2.1: Stampa i messaggi all’inizio e alla fine delle fasi di timing e warmup, in modo da poter verificare che non ci sia output dalla Regola 2 durante la fase di timing.

Regola 3: Essere consapevoli della differenza tra -client e -server, e OSR e compilazioni regolari. Il flag -XX:+PrintCompilation riporta le compilazioni OSR con un segno di apertura per indicare il punto di ingresso non iniziale, ad esempio: Trouble$1::run @ 2 (41 bytes) . Preferisci server a client e regolare a OSR, se sei in cerca di prestazioni migliori.

Regola 4: essere consapevoli degli effetti di inizializzazione. Non stampare per la prima volta durante la fase di sincronizzazione, poiché la stampa carica e inizializza le classi. Non caricare nuove classi al di fuori della fase di riscaldamento (o fase di reporting finale), a meno che non si stia testando specificamente la class di caricamento (e in tal caso si caricano solo le classi di test). La regola 2 è la prima linea di difesa contro tali effetti.

Regola 5: essere consapevoli degli effetti di deottimizzazione e ricompilazione. Non prendere alcun percorso di codice per la prima volta nella fase di temporizzazione, perché il compilatore può spazzare via e ricompilare il codice, sulla base di una precedente ipotesi ottimistica che il percorso non sarebbe stato utilizzato affatto. La regola 2 è la prima linea di difesa contro tali effetti.

Regola 6: usare gli strumenti appropriati per leggere la mente del compilatore e aspettarsi di essere sorpresi dal codice che produce. Ispeziona il codice te stesso prima di formulare teorie su ciò che rende qualcosa più veloce o più lento.

Regola 7: riduce il rumore nelle tue misure. Esegui il tuo benchmark su una macchina silenziosa ed eseguilo più volte, scartando i valori anomali. Utilizzare -Xbatch per serializzare il compilatore con l’applicazione e prendere in considerazione l’impostazione -XX:CICompilerCount=1 per impedire al compilatore di essere eseguito in parallelo con se stesso. Xmx tuo meglio per ridurre il sovraccarico di GC, imposta Xmx (abbastanza grande) uguale a Xms e usa UseEpsilonGC se è disponibile.

Regola 8: utilizza una libreria per il tuo benchmark in quanto è probabilmente più efficiente ed è già stata debellata per questo solo scopo. Come JMH , Caliper o Bill e Paul’s Excellent UCSD Benchmarks per Java .

So che questa domanda è stata contrassegnata come risposta ma volevo menzionare due librerie che ci permettono di scrivere micro benchmark

Caliper di Google

Guida introduttiva

  1. http://codingjunkie.net/micro-benchmarking-with-caliper/
  2. http://vertexlabs.co.uk/blog/caliper

JMH di OpenJDK

Guida introduttiva

  1. Evitare i rischi di benchmarking sulla JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/

Le cose importanti per i benchmark Java sono:

  • Riscaldare prima il JIT eseguendo il codice più volte prima di cronometrarlo
  • Assicurati di eseguirlo abbastanza a lungo per poter misurare i risultati in secondi o (meglio) decine di secondi
  • Sebbene non sia ansible chiamare System.gc() tra le iterazioni, è una buona idea eseguirlo tra i test, in modo che ogni test possa avere uno spazio di memoria “pulito” con cui lavorare. (Sì, gc() è più un suggerimento che una garanzia, ma è molto probabile che sarà davvero spazzatura nella mia esperienza.)
  • Mi piace visualizzare le iterazioni e il tempo, e un punteggio di tempo / iterazione che può essere ridimensionato in modo tale che l’algoritmo “migliore” ottenga un punteggio di 1,0 e altri siano segnati in modo relativo. Ciò significa che è ansible eseguire tutti gli algoritmi per un tempo piuttosto lungo, variando il numero di iterazioni e il tempo, ma ottenendo comunque risultati comparabili.

Sono solo nel processo di blogging sulla progettazione di un framework di benchmarking in .NET. Ho un paio di post precedenti che potrebbero essere in grado di darti qualche idea – non tutto sarà appropriato, ovviamente, ma potrebbe esserlo.

jmh è una recente aggiunta a OpenJDK ed è stato scritto da alcuni ingegneri delle prestazioni di Oracle. Sicuramente vale la pena dare un’occhiata.

Il jmh è un harness Java per build, eseguire e analizzare benchmark nano / micro / macro scritti in Java e in altri linguaggi rivolti alla JVM.

Elementi di informazione molto interessanti seppelliti nei commenti dei test di esempio .

Guarda anche:

  • Evitare i rischi di benchmarking sulla JVM
  • Discussione sui principali punti di forza di jmh .

Il benchmark dovrebbe misurare il tempo / iterazione o iterazioni / tempo e perché?

Dipende da cosa stai provando a testare. Se ti interessa la latenza, usa il tempo / l’iterazione e se sei interessato al throughput usa le iterazioni / il tempo.

Assicurati di utilizzare in qualche modo risultati che sono calcolati in codice benchmark. Altrimenti il ​​tuo codice può essere ottimizzato.

Se stai cercando di confrontare due algoritmi, fai almeno due benchmark su ciascuno, alternando l’ordine. vale a dire:

 for(i=1..n) alg1(); for(i=1..n) alg2(); for(i=1..n) alg2(); for(i=1..n) alg1(); 

Ho trovato alcune differenze evidenti (5-10% a volte) nel runtime dello stesso algoritmo in diversi passaggi ..

Inoltre, assicurarsi che n sia molto grande, in modo che il tempo di esecuzione di ciascun ciclo sia di almeno 10 secondi circa. Più iterazioni, più cifre significative nel tuo tempo di riferimento e più affidabili sono i dati.

Ci sono molte possibili insidie ​​per scrivere micro-benchmark in Java.

Primo: devi calcolare con tutti i tipi di eventi che richiedono più tempo o meno a caso: Garbage Collection, effetti di memorizzazione nella cache (di SO per i file e di CPU per la memoria), IO ecc.

Secondo: non ci si può fidare della precisione dei tempi misurati per intervalli molto brevi.

Terzo: la JVM ottimizza il tuo codice durante l’esecuzione. Così diverse esecuzioni nella stessa istanza JVM diventeranno sempre più veloci.

I miei consigli: fai eseguire il tuo benchmark alcuni secondi, che è più affidabile di un runtime in millisecondi. Riscaldare la JVM (significa eseguire il benchmark almeno una volta senza misurare, che la JVM possa eseguire ottimizzazioni). E gestisci il tuo benchmark più volte (forse 5 volte) e prendi il valore mediano. Esegui ogni micro-benchmark in una nuova istanza JVM (chiama per ogni benchmark Java nuovo) altrimenti gli effetti di ottimizzazione della JVM possono influenzare i test successivi. Non eseguire cose che non vengono eseguite nella fase di riscaldamento (in quanto ciò potrebbe innescare il caricamento della class e la ricompilazione).

Va anche notato che potrebbe anche essere importante analizzare i risultati del micro benchmark quando si confrontano differenti implementazioni. Pertanto dovrebbe essere fatto un test di significatività .

Questo perché l’implementazione A potrebbe essere più veloce durante la maggior parte delle esecuzioni del benchmark rispetto all’implementazione B Ma A potrebbe anche avere uno spread più elevato, quindi il beneficio prestazionale misurato di A non sarà di alcun significato se confrontato con B

Quindi è anche importante scrivere ed eseguire correttamente un benchmark micro, ma anche analizzarlo correttamente.

http://opt.sourceforge.net/ Java Micro Benchmark: attività di controllo necessarie per determinare le caratteristiche comparative delle prestazioni del sistema informatico su piattaforms diverse. Può essere utilizzato per guidare le decisioni di ottimizzazione e per confrontare diverse implementazioni Java.

Per aggiungere all’altro consiglio eccellente, sarei consapevole di quanto segue:

Per alcune CPU (ad esempio la gamma Intel Core i5 con TurboBoost), la temperatura (e il numero di core attualmente utilizzati, nonché la loro percentuale di utilizzo) influisce sulla velocità di clock. Poiché le CPU hanno un clock dinamico, ciò può influire sui risultati. Ad esempio, se si dispone di un’applicazione a thread singolo, la velocità massima di clock (con TurboBoost) è superiore rispetto a un’applicazione che utilizza tutti i core. Questo può quindi interferire con il confronto delle prestazioni single e multi-thread su alcuni sistemi. Tieni presente che anche la temperatura e le volatilità influiscono sulla durata della frequenza Turbo.

Forse un aspetto di fondamentale importanza su cui hai il controllo diretto: assicurati di misurare la cosa giusta! Ad esempio, se stai utilizzando System.nanoTime() per eseguire il benchmark su un particolare bit di codice, metti le chiamate all’assegnazione in luoghi che hanno senso per evitare di misurare cose a cui non sei interessato. Ad esempio, non fare:

 long startTime = System.nanoTime(); //code here... System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds"); 

Il problema è che non stai ricevendo immediatamente l’ora di fine quando il codice è finito. Invece, prova quanto segue:

 final long endTime, startTime = System.nanoTime(); //code here... endTime = System.nanoTime(); System.out.println("Code took "+(endTime-startTime)+"nano seconds");