Microbenchmarking mit Java

1. Einleitung

Dieser kurze Artikel konzentriert sich auf JMH (das Java Microbenchmark Harness). Zunächst machen wir uns mit der API vertraut und lernen deren Grundlagen. Dann würden wir einige Best Practices sehen, die wir beim Schreiben von Mikrobenchmarks berücksichtigen sollten.

Einfach ausgedrückt, JMH kümmert sich um Dinge wie JVM-Aufwärm- und Codeoptimierungspfade, um das Benchmarking so einfach wie möglich zu gestalten.

2. Erste Schritte

Zunächst können wir mit Java 8 weiterarbeiten und einfach die Abhängigkeiten definieren:

 org.openjdk.jmh jmh-core 1.19   org.openjdk.jmh jmh-generator-annprocess 1.19 

Die neuesten Versionen des JMH Core und des JMH Annotation Processor finden Sie in Maven Central.

Erstellen Sie als Nächstes einen einfachen Benchmark mithilfe der Annotation @Benchmark (in einer beliebigen öffentlichen Klasse):

@Benchmark public void init() { // Do nothing }

Dann fügen wir die Hauptklasse hinzu, die den Benchmarking-Prozess startet:

public class BenchmarkRunner { public static void main(String[] args) throws Exception { org.openjdk.jmh.Main.main(args); } }

Wenn Sie jetzt BenchmarkRunner ausführen, wird unser wohl etwas nutzloser Benchmark ausgeführt. Nach Abschluss des Laufs wird eine Übersichtstabelle angezeigt:

# Run complete. Total time: 00:06:45 Benchmark Mode Cnt Score Error Units BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops/s

3. Arten von Benchmarks

JMH unterstützt einige mögliche Benchmarks: Durchsatz, AverageTime, SampleTime und SingleShotTime . Diese können über die Annotation @BenchmarkMode konfiguriert werden :

@Benchmark @BenchmarkMode(Mode.AverageTime) public void init() { // Do nothing }

Die resultierende Tabelle hat eine durchschnittliche Zeitmetrik (anstelle des Durchsatzes):

# Run complete. Total time: 00:00:40 Benchmark Mode Cnt Score Error Units BenchMark.init avgt 20 ≈ 10⁻⁹ s/op

4. Warmup und Ausführung konfigurieren

Durch die Verwendung der @Fork Anmerkung können wir festlegen, wie die Benchmark Ausführung geschieht: der Wert Parameter steuern , wie oft wird die Benchmark ausgeführt werden, und die Warm - up Parameter steuern , wie oft ein Benchmark , bevor die Ergebnisse lief trocken werden gesammelt, zum Beispiel ::

@Benchmark @Fork(value = 1, warmups = 2) @BenchmarkMode(Mode.Throughput) public void init() { // Do nothing }

Dies weist JMH an, zwei Aufwärmgabeln zu betreiben und die Ergebnisse zu verwerfen, bevor mit dem Echtzeit-Benchmarking fortgefahren wird.

Die Annotation @Warmup kann auch verwendet werden, um die Anzahl der Aufwärmiterationen zu steuern. Zum Beispiel teilt @Warmup (Iterationen = 5) JMH mit, dass fünf Aufwärmiterationen ausreichen, im Gegensatz zu den Standarditerationen 20.

5. Staat

Lassen Sie uns nun untersuchen, wie eine weniger triviale und indikativere Aufgabe des Benchmarking eines Hashing-Algorithmus unter Verwendung von State ausgeführt werden kann . Angenommen, wir möchten zusätzlichen Schutz vor Wörterbuchangriffen auf eine Kennwortdatenbank hinzufügen, indem wir das Kennwort einige hundert Mal hashen.

Wir können die Auswirkungen auf die Leistung erkunden durch Verwendung eines Staatsobjekt:

@State(Scope.Benchmark) public class ExecutionPlan { @Param({ "100", "200", "300", "500", "1000" }) public int iterations; public Hasher murmur3; public String password = "4v3rys3kur3p455w0rd"; @Setup(Level.Invocation) public void setUp() { murmur3 = Hashing.murmur3_128().newHasher(); } }

Unsere Benchmark-Methode sieht dann so aus:

@Fork(value = 1, warmups = 1) @Benchmark @BenchmarkMode(Mode.Throughput) public void benchMurmur3_128(ExecutionPlan plan) { for (int i = plan.iterations; i > 0; i--) { plan.murmur3.putString(plan.password, Charset.defaultCharset()); } plan.murmur3.hash(); }

Hier werden die Feld Iterationen werden mit den entsprechenden Werten aus dem aufgefüllt werden @param Anmerkung der JMH , wenn es um die Benchmark - Methode übergeben wird. Die mit @Setup annotierte Methode wird vor jedem Aufruf des Benchmarks aufgerufen und erstellt einen neuen Hasher, der die Isolation sicherstellt.

Wenn die Ausführung abgeschlossen ist, erhalten wir ein ähnliches Ergebnis wie das folgende:

# Run complete. Total time: 00:06:47 Benchmark (iterations) Mode Cnt Score Error Units BenchMark.benchMurmur3_128 100 thrpt 20 92463.622 ± 1672.227 ops/s BenchMark.benchMurmur3_128 200 thrpt 20 39737.532 ± 5294.200 ops/s BenchMark.benchMurmur3_128 300 thrpt 20 30381.144 ± 614.500 ops/s BenchMark.benchMurmur3_128 500 thrpt 20 18315.211 ± 222.534 ops/s BenchMark.benchMurmur3_128 1000 thrpt 20 8960.008 ± 658.524 ops/s

6. Dead Code Elimination

Beim Ausführen von Mikrobenchmarks ist es sehr wichtig, Optimierungen zu berücksichtigen . Andernfalls können sie die Benchmark-Ergebnisse sehr irreführend beeinflussen.

Um die Sache etwas konkreter zu machen, betrachten wir ein Beispiel:

@Benchmark @OutputTimeUnit(TimeUnit.NANOSECONDS) @BenchmarkMode(Mode.AverageTime) public void doNothing() { } @Benchmark @OutputTimeUnit(TimeUnit.NANOSECONDS) @BenchmarkMode(Mode.AverageTime) public void objectCreation() { new Object(); }

Wir erwarten, dass die Kosten für die Objektzuweisung mehr sind als gar nichts. Wenn wir jedoch die Benchmarks ausführen:

Benchmark Mode Cnt Score Error Units BenchMark.doNothing avgt 40 0.609 ± 0.006 ns/op BenchMark.objectCreation avgt 40 0.613 ± 0.007 ns/op

Anscheinend ist es fast kostenlos, einen Platz im TLAB zu finden, ein Objekt zu erstellen und zu initialisieren! Wenn wir uns diese Zahlen ansehen, sollten wir wissen, dass hier etwas nicht ganz stimmt.

Hier sind wir das Opfer der Beseitigung toten Codes . Compiler sind sehr gut darin, den redundanten Code zu optimieren. Genau das hat der JIT-Compiler hier getan.

Um diese Optimierung zu verhindern, sollten wir den Compiler irgendwie austricksen und ihn glauben lassen, dass der Code von einer anderen Komponente verwendet wird. Eine Möglichkeit, dies zu erreichen, besteht darin, das erstellte Objekt zurückzugeben:

@Benchmark @OutputTimeUnit(TimeUnit.NANOSECONDS) @BenchmarkMode(Mode.AverageTime) public Object pillarsOfCreation() { return new Object(); }

Wir können das Schwarze Loch auch verbrauchen lassen:

@Benchmark @OutputTimeUnit(TimeUnit.NANOSECONDS) @BenchmarkMode(Mode.AverageTime) public void blackHole(Blackhole blackhole) { blackhole.consume(new Object()); }

Wenn Blackhole das Objekt konsumiert, kann der JIT-Compiler davon überzeugt werden, die Optimierung zur Beseitigung von totem Code nicht anzuwenden . Wenn wir diese Benchmarks erneut durchführen, wären die Zahlen jedenfalls sinnvoller:

Benchmark Mode Cnt Score Error Units BenchMark.blackHole avgt 20 4.126 ± 0.173 ns/op BenchMark.doNothing avgt 20 0.639 ± 0.012 ns/op BenchMark.objectCreation avgt 20 0.635 ± 0.011 ns/op BenchMark.pillarsOfCreation avgt 20 4.061 ± 0.037 ns/op

7. Konstante Faltung

Betrachten wir noch ein weiteres Beispiel:

@Benchmark public double foldedLog() { int x = 8; return Math.log(x); }

Berechnungen, die auf Konstanten basieren, können unabhängig von der Anzahl der Ausführungen genau dieselbe Ausgabe zurückgeben. Daher besteht eine gute Chance, dass der JIT-Compiler den Logarithmus-Funktionsaufruf durch sein Ergebnis ersetzt:

@Benchmark public double foldedLog() { return 2.0794415416798357; }

Diese Form der Teilbewertung wird als konstante Faltung bezeichnet . In diesem Fall wird durch ständiges Falten der Aufruf von Math.log vollständig vermieden , was der springende Punkt des Benchmarks war.

Um ein konstantes Falten zu verhindern, können wir den konstanten Zustand in ein Zustandsobjekt einkapseln:

@State(Scope.Benchmark) public static class Log { public int x = 8; } @Benchmark public double log(Log input) { return Math.log(input.x); }

Wenn wir diese Benchmarks gegeneinander ausführen:

Benchmark Mode Cnt Score Error Units BenchMark.foldedLog thrpt 20 449313097.433 ± 11850214.900 ops/s BenchMark.log thrpt 20 35317997.064 ± 604370.461 ops/s

Anscheinend leistet der Log- Benchmark im Vergleich zum FoldedLog ernsthafte Arbeit , was sinnvoll ist.

8. Fazit

Dieses Tutorial konzentrierte sich auf das Micro-Benchmarking-Geschirr von Java und zeigte es.

Codebeispiele finden Sie wie immer auf GitHub.