Wie schreibe ich einen korrekten Mikro-Benchmark in Java?


870

Wie schreibt (und führt) man einen korrekten Mikro-Benchmark in Java aus?

Ich suche nach Codebeispielen und Kommentaren, die verschiedene Dinge veranschaulichen, über die ich nachdenken sollte.

Beispiel: Sollte der Benchmark Zeit / Iteration oder Iterationen / Zeit messen und warum?

Verwandte: Ist Stoppuhr-Benchmarking akzeptabel?


Siehe [diese Frage] [1] von vor einigen Minuten für einige verwandte Informationen. edit: sorry, das soll keine antwort sein. Ich hätte als Kommentar posten sollen. [1]: stackoverflow.com/questions/503877/…
Tiago

Nachdem ich geplant hatte, das Poster dieser Frage auf eine solche Frage zu verweisen, stellte ich fest, dass diese Frage nicht existiert. Hier ist es also, hoffentlich werden im Laufe der Zeit einige gute Tipps zusammengestellt.
John Nilsson

5
Java 9 bietet möglicherweise einige Funktionen für das Mikro-Benchmarking: openjdk.java.net/jeps/230
Raedwald,

1
@ Raedwald Ich denke, dass JEP darauf abzielt, dem JDK-Code einen Mikro-Benchmark hinzuzufügen, aber ich glaube nicht, dass jmh in das JDK aufgenommen wird ...
assylias

1
@ Raedwald Hallo aus der Zukunft. Es hat den Schnitt nicht geschafft .
Michael

Antworten:


787

Tipps zum Schreiben von Mikro-Benchmarks von den Entwicklern von Java HotSpot :

Regel 0: Lesen Sie ein seriöses Papier über JVMs und Mikro-Benchmarking. Ein guter ist Brian Goetz, 2005 . Erwarten Sie nicht zu viel von Mikro-Benchmarks. Sie messen nur einen begrenzten Bereich von JVM-Leistungsmerkmalen.

Regel 1: Schließen Sie immer eine Aufwärmphase ein, in der Ihr Testkernel vollständig ausgeführt wird, sodass alle Initialisierungen und Kompilierungen vor der Timing-Phase (n) ausgelöst werden. (In der Aufwärmphase sind weniger Iterationen in Ordnung. Als Faustregel gelten mehrere Zehntausend Iterationen der inneren Schleife.)

Regel 2: Immer lief mit -XX:+PrintCompilation, -verbose:gcetc., so dass Sie überprüfen können , dass der Compiler und andere Teile der JVM sind nicht unerwartet Arbeit während Taktphase zu tun.

Regel 2.1: Drucken Sie Nachrichten zu Beginn und am Ende der Timing- und Aufwärmphase, damit Sie überprüfen können, ob während der Timing-Phase keine Ausgabe von Regel 2 erfolgt.

Regel 3: Beachten Sie den Unterschied zwischen -clientund -server, OSR und regelmäßigen Zusammenstellungen. Das -XX:+PrintCompilationFlag meldet OSR-Kompilierungen mit einem At-Zeichen, um den nicht anfänglichen Einstiegspunkt zu kennzeichnen, zum Beispiel : Trouble$1::run @ 2 (41 bytes). Bevorzugen Sie Server gegenüber Client und regulär gegenüber OSR, wenn Sie die beste Leistung erzielen möchten.

Regel 4: Beachten Sie die Initialisierungseffekte. Drucken Sie während Ihrer Timing-Phase nicht zum ersten Mal, da beim Drucken Klassen geladen und initialisiert werden. Laden Sie keine neuen Klassen außerhalb der Aufwärmphase (oder der letzten Berichtsphase), es sei denn, Sie testen das Laden von Klassen speziell (und laden in diesem Fall nur die Testklassen). Regel 2 ist Ihre erste Verteidigungslinie gegen solche Effekte.

Regel 5: Achten Sie auf Deoptimierungs- und Neukompilierungseffekte. Nehmen Sie zum ersten Mal in der Timing-Phase keinen Codepfad, da der Compiler den Code möglicherweise verschmutzen und neu kompilieren kann, basierend auf einer früheren optimistischen Annahme, dass der Pfad überhaupt nicht verwendet werden würde. Regel 2 ist Ihre erste Verteidigungslinie gegen solche Effekte.

Regel 6: Verwenden Sie geeignete Tools, um die Gedanken des Compilers zu lesen, und lassen Sie sich von dem von ihm erzeugten Code überraschen. Überprüfen Sie den Code selbst, bevor Sie Theorien darüber aufstellen, was etwas schneller oder langsamer macht.

Regel 7: Reduzieren Sie das Rauschen bei Ihren Messungen. Führen Sie Ihren Benchmark auf einem leisen Computer aus und führen Sie ihn mehrmals aus, wobei Sie Ausreißer verwerfen. Verwenden Sie -Xbatchdiese Option, um den Compiler mit der Anwendung zu serialisieren, und erwägen Sie die Einstellung -XX:CICompilerCount=1, um zu verhindern, dass der Compiler parallel zu sich selbst ausgeführt wird. Versuchen Sie nach besten Kräften, den GC-Overhead zu reduzieren, setzen Sie Xmx(groß genug) gleich Xmsund verwenden Sie ihn, UseEpsilonGCfalls verfügbar.

Regel 8: Verwenden Sie eine Bibliothek für Ihren Benchmark, da diese wahrscheinlich effizienter ist und bereits zu diesem alleinigen Zweck getestet wurde. Wie JMH , Caliper oder Bill und Pauls ausgezeichnete UCSD-Benchmarks für Java .


5
Dies war auch ein interessanter Artikel: ibm.com/developerworks/java/library/j-jtp12214
John Nilsson

142
Verwenden Sie System.currentTimeMillis () niemals, es sei denn, Sie sind mit einer Genauigkeit von + oder - 15 ms einverstanden, was für die meisten OS + JVM-Kombinationen typisch ist. Verwenden Sie stattdessen System.nanoTime ().
Scott Carey


93
Es ist zu beachten, dass die Genauigkeit System.nanoTime()nicht garantiert werden kann als System.currentTimeMillis(). Es ist nur garantiert, dass es mindestens genauso genau ist. Es ist jedoch normalerweise wesentlich genauer.
Schwerkraft

41
Der Hauptgrund, warum man System.nanoTime()stattdessen verwenden muss, System.currentTimeMillis()ist, dass der erstere garantiert monoton zunimmt. Das Subtrahieren der zurückgegebenen Werte durch zwei currentTimeMillisAufrufe kann tatsächlich zu negativen Ergebnissen führen, möglicherweise weil die Systemzeit von einem NTP-Daemon angepasst wurde.
Waldheinz

239

Ich weiß, dass diese Frage als beantwortet markiert wurde, aber ich wollte zwei Bibliotheken erwähnen, die uns beim Schreiben von Mikro-Benchmarks helfen

Bremssattel von Google

Erste Schritte Tutorials

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

JMH von OpenJDK

Erste Schritte Tutorials

  1. Vermeiden von Benchmarking-Fallstricken in der JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/

37
+1 Es hätte als Regel 8 der akzeptierten Antwort hinzugefügt werden können: Regel 8: Da so viele Dinge schief gehen können, sollten Sie wahrscheinlich eine vorhandene Bibliothek verwenden, anstatt zu versuchen, dies selbst zu tun!
Assylias

8
@Pangaea jmh ist Caliper heutzutage wahrscheinlich überlegen. Siehe auch: groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/…
assylias

86

Wichtige Dinge für Java-Benchmarks sind:

  • Erwärmen Sie die JIT zuerst, indem Sie den Code mehrmals ausführen, bevor Sie ihn zeitlich festlegen
  • Stellen Sie sicher, dass Sie es lange genug ausführen, um die Ergebnisse in Sekunden oder (besser) zehn Sekunden messen zu können
  • Sie können zwar nicht System.gc()zwischen Iterationen aufrufen , es ist jedoch eine gute Idee, sie zwischen Tests auszuführen, damit jeder Test hoffentlich einen "sauberen" Speicherplatz zum Arbeiten erhält. (Ja, gc()ist eher ein Hinweis als eine Garantie, aber es ist sehr wahrscheinlich, dass sich meiner Erfahrung nach wirklich Müll ansammelt.)
  • Ich zeige gerne Iterationen und Zeit sowie eine Zeit- / Iterationsbewertung an, die so skaliert werden kann, dass der "beste" Algorithmus eine Bewertung von 1,0 erhält und andere relativ bewertet werden. Dies bedeutet, dass Sie alle Algorithmen über einen längeren Zeitraum ausführen können , wobei sowohl die Anzahl der Iterationen als auch die Zeit variiert werden, aber dennoch vergleichbare Ergebnisse erzielt werden.

Ich bin gerade dabei, über das Design eines Benchmarking-Frameworks in .NET zu bloggen. Ich habe ein bekam Paar von früheren Beiträgen der in der Lage sein, Ihnen einige Ideen zu geben - nicht alles wird angemessen sein, natürlich, aber ein Teil davon sein kann.


3
Minor nitpick: IMO "damit jeder Test" wird "sollte" sein, damit jeder Test "bekommt, da der erstere den Eindruck erweckt, dass durch das Aufrufen gc immer unbenutzter Speicherplatz frei wird.
Sanjay T. Sharma

@ SanjayT.Sharma: Nun, die Absicht ist, dass es tatsächlich tut. Obwohl dies nicht unbedingt garantiert ist, ist es tatsächlich ein ziemlich starker Hinweis. Wird bearbeitet, um klarer zu sein.
Jon Skeet

1
Ich bin nicht damit einverstanden, System.gc () aufzurufen. Es ist ein Hinweis, das ist alles. Nicht einmal "es wird hoffentlich etwas bewirken". Du solltest es niemals nennen. Das ist Programmierung, keine Kunst.
Gyorgyabraham

13
@gyabraham: Ja, es ist ein Hinweis - aber ich habe beobachtet, dass er normalerweise genommen wird. Wenn Sie also nicht gerne verwenden System.gc(), wie schlagen Sie vor, die Speicherbereinigung in einem Test aufgrund von Objekten zu minimieren, die in früheren Tests erstellt wurden? Ich bin pragmatisch, nicht dogmatisch.
Jon Skeet

9
@gyabraham: Ich weiß nicht, was du mit "großer Fallback" meinst. Können Sie noch einmal näher darauf eingehen - haben Sie einen Vorschlag, um bessere Ergebnisse zu erzielen? Ich habe ausdrücklich gesagt, dass es keine Garantie ist ...
Jon Skeet

48

jmh ist eine neue Erweiterung von OpenJDK und wurde von einigen Performance-Ingenieuren von Oracle geschrieben. Auf jeden Fall einen Blick wert.

Das jmh ist ein Java-Harness zum Erstellen, Ausführen und Analysieren von Nano- / Mikro- / Makro-Benchmarks, die in Java und anderen Sprachen für die JVM geschrieben wurden.

Sehr interessante Informationen, die in den Kommentaren der Beispieltests vergraben sind .

Siehe auch:


1
Siehe auch diesen Blog-Beitrag: psy-lob-saw.blogspot.com/2013/04/… für Details zu den ersten Schritten mit JMH.
Nitsan Wakart

Zu Ihrer Information , JEP 230: Microbenchmark Suite ist ein OpenJDK- Vorschlag, der auf diesem Java Microbenchmark Harness (JMH) -Projekt basiert . Hat den Schnitt für Java 9 nicht gemacht , kann aber später hinzugefügt werden.
Basil Bourque

23

Sollte der Benchmark Zeit / Iteration oder Iterationen / Zeit messen und warum?

Es hängt davon ab, was Sie testen möchten.

Wenn Sie an Latenz interessiert sind , verwenden Sie Zeit / Iteration und wenn Sie an Durchsatz interessiert sind , verwenden Sie Iterationen / Zeit.


16

Wenn Sie versuchen, zwei Algorithmen zu vergleichen, führen Sie jeweils mindestens zwei Benchmarks in abwechselnder Reihenfolge durch. dh:

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

Ich habe einige bemerkenswerte Unterschiede (manchmal 5-10%) in der Laufzeit des gleichen Algorithmus in verschiedenen Durchgängen festgestellt.

Stellen Sie außerdem sicher, dass n sehr groß ist, damit die Laufzeit jeder Schleife mindestens 10 Sekunden beträgt. Je mehr Iterationen, desto signifikanter die Zahlen in Ihrer Benchmark-Zeit und desto zuverlässiger sind diese Daten.


5
Eine natürliche Änderung der Reihenfolge beeinflusst natürlich die Laufzeit. JVM-Optimierungen und Caching-Effekte werden hier funktionieren. Besser ist es, die JVM-Optimierung aufzuwärmen, mehrere Läufe durchzuführen und jeden Test in einer anderen JVM zu bewerten.
Mnementh

15

Stellen Sie sicher, dass Sie Ergebnisse verwenden, die in Benchmark-Code berechnet werden. Andernfalls kann Ihr Code entfernt werden.


13

Es gibt viele mögliche Fallstricke beim Schreiben von Mikro-Benchmarks in Java.

Erstens: Sie müssen mit allen Arten von Ereignissen rechnen, die mehr oder weniger zufällig Zeit benötigen: Speicherbereinigung, Caching-Effekte (des Betriebssystems für Dateien und der CPU für Speicher), E / A usw.

Zweitens: Sie können der Genauigkeit der gemessenen Zeiten für sehr kurze Intervalle nicht vertrauen.

Drittens: Die JVM optimiert Ihren Code während der Ausführung. So werden verschiedene Läufe in derselben JVM-Instanz immer schneller.

Meine Empfehlungen: Lassen Sie Ihren Benchmark einige Sekunden laufen, das ist zuverlässiger als eine Laufzeit über Millisekunden. Erwärmen Sie die JVM (bedeutet, dass der Benchmark mindestens einmal ohne Messung ausgeführt wird, damit die JVM Optimierungen ausführen kann). Führen Sie Ihren Benchmark mehrmals (möglicherweise fünfmal) aus und nehmen Sie den Medianwert. Führen Sie jeden Micro-Benchmark in einer neuen JVM-Instanz aus (fordern Sie für jeden Benchmark neues Java an), da sonst die Optimierungseffekte der JVM die späteren laufenden Tests beeinflussen können. Führen Sie keine Dinge aus, die nicht in der Aufwärmphase ausgeführt werden (da dies das Laden und Neukompilieren von Klassen auslösen könnte).


8

Es sollte auch beachtet werden, dass es auch wichtig sein kann, die Ergebnisse des Mikro-Benchmarks zu analysieren, wenn verschiedene Implementierungen verglichen werden. Daher sollte ein Signifikanztest durchgeführt werden.

Dies liegt daran, dass die Implementierung Awährend der meisten Durchläufe des Benchmarks möglicherweise schneller ist als die Implementierung B. Kann Aaber auch einen höheren Spread aufweisen, sodass der gemessene Leistungsvorteil von Aim Vergleich zu nicht von Bedeutung ist B.

Daher ist es auch wichtig, einen Mikro-Benchmark korrekt zu schreiben und auszuführen, ihn aber auch korrekt zu analysieren.


8

Um den anderen ausgezeichneten Rat zu ergänzen, würde ich auch Folgendes beachten:

Bei einigen CPUs (z. B. Intel Core i5 mit TurboBoost) wirkt sich die Temperatur (und die Anzahl der derzeit verwendeten Kerne sowie deren prozentuale Auslastung) auf die Taktrate aus. Da CPUs dynamisch getaktet werden, kann dies Ihre Ergebnisse beeinflussen. Wenn Sie beispielsweise eine Single-Threaded-Anwendung haben, ist die maximale Taktrate (mit TurboBoost) höher als bei einer Anwendung mit allen Kernen. Dies kann daher den Vergleich der Einzel- und Multithread-Leistung auf einigen Systemen beeinträchtigen. Beachten Sie, dass die Temperatur und die Spannungen auch die Dauer der Turbofrequenz beeinflussen.

Vielleicht ein grundlegenderer Aspekt, über den Sie die direkte Kontrolle haben: Stellen Sie sicher, dass Sie das Richtige messen! Wenn Sie beispielsweise System.nanoTime()ein bestimmtes Codebit als Benchmark verwenden, platzieren Sie die Aufrufe der Zuweisung an Orten, die sinnvoll sind, um zu vermeiden, dass Dinge gemessen werden, an denen Sie nicht interessiert sind. Führen Sie beispielsweise Folgendes nicht aus:

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

Das Problem ist, dass Sie nicht sofort die Endzeit erhalten, wenn der Code fertig ist. Versuchen Sie stattdessen Folgendes:

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

Ja, es ist wichtig, keine unabhängigen Arbeiten innerhalb des zeitgesteuerten Bereichs auszuführen, aber Ihr erstes Beispiel ist immer noch in Ordnung. Es gibt nur einen Aufruf von println, keine separate Kopfzeile oder ähnliches und System.nanoTime()muss als erster Schritt beim Erstellen des Zeichenfolgenarguments für diesen Aufruf ausgewertet werden . Es gibt nichts, was ein Compiler mit dem ersten tun kann, was er mit dem zweiten nicht tun kann, und keiner ermutigt sie, zusätzliche Arbeit zu leisten, bevor sie eine Stoppzeit aufzeichnen.
Peter Cordes

7

http://opt.sourceforge.net/ Java Micro Benchmark - Steuerungsaufgaben, die erforderlich sind, um die vergleichenden Leistungsmerkmale des Computersystems auf verschiedenen Plattformen zu bestimmen. Kann verwendet werden, um Optimierungsentscheidungen zu treffen und verschiedene Java-Implementierungen zu vergleichen.


2
Scheint nur die JVM + -Hardware zu vergleichen, kein beliebiger Teil des Java-Codes.
Stefan L
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.