Microbenchmarking met Java

1. Inleiding

Dit korte artikel is gericht op JMH (de Java Microbenchmark Harness). Eerst maken we kennis met de API en leren we de basisprincipes ervan. Dan zouden we een paar best practices zien waarmee we rekening moeten houden bij het schrijven van microbenchmarks.

Simpel gezegd, JMH zorgt voor zaken als JVM-opwarmings- en code-optimalisatiepaden, waardoor benchmarking zo eenvoudig mogelijk wordt.

2. Aan de slag

Om aan de slag te gaan, kunnen we daadwerkelijk met Java 8 blijven werken en eenvoudig de afhankelijkheden definiëren:

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

De nieuwste versies van de JMH Core en JMH Annotation Processor zijn te vinden in Maven Central.

Maak vervolgens een eenvoudige benchmark door gebruik te maken van @Benchmark annotatie (in elke openbare klasse):

@Benchmark public void init () {// Niets doen}

Vervolgens voegen we de hoofdklasse toe die het benchmarkproces start:

openbare klasse BenchmarkRunner {openbare statische leegte hoofd (String [] args) gooit Uitzondering {org.openjdk.jmh.Main.main (args); }}

Nu rennen BenchmarkRunner zal onze aantoonbaar ietwat nutteloze benchmark uitvoeren. Zodra de run is voltooid, wordt een overzichtstabel weergegeven:

# Rennen voltooid. Totale tijd: 00:06:45 Benchmarkmodus Cnt Score Fouteenheden BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 bewerkingen / s

3. Soorten benchmarks

JMH ondersteunt enkele mogelijke benchmarks: Doorvoer,Gemiddelde tijd,Proeftijd, en SingleShotTime. Deze kunnen worden geconfigureerd via @BenchmarkMode annotatie:

@Benchmark @BenchmarkMode (Mode.AverageTime) public void init () {// Niets doen}

De resulterende tabel heeft een metrische gemiddelde tijd (in plaats van doorvoer):

# Rennen voltooid. Totale tijd: 00:00:40 Benchmarkmodus Cnt Score Fouteenheden BenchMark.init avgt 20 ≈ 10⁻⁹ s / op

4. Opwarming en uitvoering configureren

Door de @Vork annotatie kunnen we instellen hoe de uitvoering van de benchmark plaatsvindt: de waarde parameter bepaalt hoe vaak de benchmark wordt uitgevoerd, en de opwarmen parameter bepaalt hoe vaak een benchmark wordt getest voordat de resultaten worden verzameld, bijvoorbeeld:

@Benchmark @Fork (waarde = 1, opwarmingen = 2) @BenchmarkMode (Mode.Throughput) public void init () {// Niets doen}

Dit geeft JMH de opdracht om twee opwarmvorken uit te voeren en de resultaten te negeren voordat hij overgaat op real-getimede benchmarking.

Ook de @Opwarmen annotatie kan worden gebruikt om het aantal opwarmherhalingen te regelen. Bijvoorbeeld, @Warmup (iteraties = 5) vertelt JMH dat vijf opwarmings-iteraties voldoende zullen zijn, in tegenstelling tot de standaard 20.

5. Staat

Laten we nu eens kijken hoe een minder triviale en meer indicatieve taak om een ​​hash-algoritme te benchmarken kan worden uitgevoerd door gebruik te maken van Staat. Stel dat we besluiten om extra bescherming tegen woordenboekaanvallen toe te voegen aan een wachtwoorddatabase door het wachtwoord een paar honderd keer te hashen.

We kunnen de prestatie-impact onderzoeken door een Staat voorwerp:

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

Onze benchmarkmethode ziet er dan als volgt uit:

@Fork (waarde = 1, warmingups = 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, het veld iteraties wordt gevuld met de juiste waarden uit de @Param annotatie door de JMH wanneer deze wordt doorgegeven aan de benchmarkmethode. De @Opstelling geannoteerde methode wordt aangeroepen voor elke aanroep van de benchmark en creëert een nieuwe Hasher zorgen voor isolatie.

Wanneer de uitvoering is voltooid, krijgen we een resultaat dat lijkt op het onderstaande:

# Rennen voltooid. Totale tijd: 00:06:47 Benchmark (iteraties) Mode Cnt Score Error Units BenchMark.benchMurmur3_128 100 thrpt 20 92463.622 ± 1672.227 bewerkingen / s BenchMark.benchMurmur3_128 200 thrpt 20 39737.532 ± 5294.200 ops / s BenchMark.bench 30381.532 ops / s BenchMark.benchMurmur3_128 500 thrpt 20 18315.211 ± 222.534 ops / s BenchMark.benchMurmur3_128 1000 thrpt 20 8960.008 ± 658.524 bewerkingen / s

6. Verwijdering van dode codes

Bij het uitvoeren van microbenchmarks is het erg belangrijk om op de hoogte te zijn van optimalisaties. Anders kunnen ze de benchmarkresultaten op een zeer misleidende manier beïnvloeden.

Laten we, om de zaken wat concreter te maken, een voorbeeld bekijken:

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

We verwachten meer kosten voor objecttoewijzing dan helemaal niets doen. Als we echter de benchmarks uitvoeren:

Benchmarkmodus Cnt Score Fouteenheden BenchMark.doNothing gem. 40 0.609 ± 0.006 ns / op BenchMark.objectCreation gem. 40 0.613 ± 0.007 ns / op

Blijkbaar is het vinden van een plek in de TLAB, het maken en initialiseren van een object bijna gratis! Alleen al door naar deze cijfers te kijken, zouden we moeten weten dat iets hier niet helemaal klopt.

Hier zijn we het slachtoffer van het elimineren van dode codes. Compilers zijn erg goed in het weg optimaliseren van de overtollige code. Dat is eigenlijk precies wat de JIT-compiler hier deed.

Om deze optimalisatie te voorkomen, moeten we de compiler op de een of andere manier bedriegen en hem laten denken dat de code door een ander onderdeel wordt gebruikt. Een manier om dit te bereiken, is door het gemaakte object terug te sturen:

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

Ook kunnen we de Blackhole consumeren:

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

Hebben Blackhole consumeer het object is een manier om de JIT-compiler te overtuigen om de optimalisatie van de eliminatie van dode code niet toe te passen. Hoe dan ook, als we deze benchmarks opnieuw uitvoeren, zouden de cijfers logischer zijn:

Benchmarkmodus Cnt Score Fout Eenheden BenchMark.blackHole gemiddelde 20 4.126 ± 0.173 ns / op BenchMark.doNothing gemiddelde 20 0.639 ± 0.012 ns / op BenchMark.objectCreation gemiddelde 20 0.635 ± 0.011 ns / op BenchMark.pillarsOfCreation gemiddelde 20 4.061 ± 0.037 ns / op

7. Constant vouwen

Laten we nog een ander voorbeeld bekijken:

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

Berekeningen op basis van constanten kunnen exact dezelfde uitvoer retourneren, ongeacht het aantal uitvoeringen. Daarom is er een goede kans dat de JIT-compiler de logaritme-functieaanroep vervangt door het resultaat:

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

Deze vorm van gedeeltelijke evaluatie wordt constant vouwen genoemd. In dit geval vermijdt constant vouwen de Math.log call, dat was het hele punt van de benchmark.

Om constant vouwen te voorkomen, kunnen we de constante toestand in een toestandobject inkapselen:

@State (Scope.Benchmark) openbare statische klasse Log {public int x = 8; } @Benchmark openbaar dubbel logboek (Logboekinvoer) {retourneer Math.log (invoer.x); }

Als we deze benchmarks tegen elkaar uitvoeren:

Benchmarkmodus Cnt Score Fouteenheden BenchMark.foldedLog thrpt 20 449313097.433 ± 11850214.900 bewerkingen / s BenchMark.log thrpt 20 35317997.064 ± 604370.461 bewerkingen / s

Blijkbaar is de logboek benchmark doet serieus werk in vergelijking met de gevouwenLog, wat verstandig is.

8. Conclusie

Deze tutorial concentreerde zich op en toonde Java's micro-benchmarking-harnas.

Zoals altijd zijn codevoorbeelden te vinden op GitHub.