Microbenchmarking med Java

1. Introduksjon

Denne raske artikkelen er fokusert på JMH (Java Microbenchmark Harness). Først blir vi kjent med API og lærer det grunnleggende. Så ville vi se noen gode fremgangsmåter som vi bør vurdere når vi skriver mikrobenchmarks.

Enkelt sagt, JMH tar seg av ting som JVM oppvarming og kodeoptimaliseringsveier, noe som gjør benchmarking så enkelt som mulig.

2. Komme i gang

For å komme i gang kan vi faktisk fortsette å jobbe med Java 8 og ganske enkelt definere avhengighetene:

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

De nyeste versjonene av JMH Core og JMH Annotation Processor finnes i Maven Central.

Deretter oppretter du en enkel referanse ved å bruke @Benchmark kommentar (i alle offentlige klasser):

@Benchmark public void init () {// Gjør ingenting}

Deretter legger vi til hovedklassen som starter referanseprosessen:

offentlig klasse BenchmarkRunner {offentlig statisk tomrom hoved (String [] args) kaster Unntak {org.openjdk.jmh.Main.main (args); }}

Kjører nå BenchmarkRunner vil utføre vår uten tvil noe ubrukelige referanseindeks. Når løpet er fullført, presenteres en oppsummeringstabell:

# Kjør komplett. Total tid: 00:06:45 Referansemodus Cnt Score feilenheter BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops / s

3. Typer av referanser

JMH støtter noen mulige referanser: Gjennomstrømning,Gjennomsnittstid,SampleTime, og SingleShotTime. Disse kan konfigureres via @BenchmarkMode kommentar:

@Benchmark @BenchmarkMode (Mode.AverageTime) public void init () {// Gjør ingenting}

Den resulterende tabellen vil ha en gjennomsnittlig tidsberegning (i stedet for gjennomstrømning):

# Kjør komplett. Total tid: 00:00:40 Referansemodus Cnt Score feilenheter BenchMark.init avgt 20 ≈ 10⁻⁹ s / op

4. Konfigurere oppvarming og utføring

Ved å bruke @Gaffel kommentar, vi kan sette opp hvordan referanseutførelsen skjer: verdi parameter styrer hvor mange ganger referanseverdien skal utføres, og varme opp parameter styrer hvor mange ganger en referanse vil tørke før resultatene samles, for eksempel:

@Benchmark @Fork (value = 1, warmups = 2) @BenchmarkMode (Mode.Throughput) public void init () {// Gjør ingenting}

Dette instruerer JMH om å kjøre to oppvarmingsgafler og kaste resultater før du går videre til virkelig tidsbestemt benchmarking.

Også, den @Varme opp merknader kan brukes til å kontrollere antall oppvarming. For eksempel, @Warmup (iterasjoner = 5) forteller JMH at fem oppvarmingsoppgaver vil være tilstrekkelig, i motsetning til standard 20.

5. Stat

La oss nå undersøke hvordan en mindre triviell og mer veiledende oppgave med å benchmarking av en hashingalgoritme kan utføres ved å bruke Stat. Anta at vi bestemmer oss for å legge til ekstra beskyttelse mot ordbokangrep på en passorddatabase ved å hashe passordet noen hundre ganger.

Vi kan utforske ytelseseffekten ved å bruke a Stat gjenstand:

@State (Scope.Benchmark) public class ExecutionPlan {@Param ({"100", "200", "300", "500", "1000"}) public it itations; offentlig Hasher murmur3; offentlig strengpassord = "4v3rys3kur3p455w0rd"; @Setup (Level.Invocation) public void setUp () {murmur3 = Hashing.murmur3_128 (). NewHasher (); }}

Vår målemetode vil da se ut:

@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.passord, Charset.defaultCharset ()); } plan.murmur3.hash (); }

Her, feltet iterasjoner vil bli fylt med passende verdier fra @Param merknad fra JMH når den blir overført til referansemetoden. De @Setup merket metode påkalles før hver påkalling av referanseindeksen og skaper en ny Hasher sikre isolasjon.

Når utførelsen er ferdig, får vi et resultat som ligner på det nedenfor:

# Kjør komplett. Total tid: 00:06:47 Referanse (iterasjoner) Modus Cnt Score Feilenheter 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.benchMurmur3128 638 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. Død kode eliminering

Når du kjører mikrobenker, er det veldig viktig å være oppmerksom på optimaliseringer. Ellers kan de påvirke referanseresultatene på en veldig misvisende måte.

For å gjøre saken litt mer konkret, la oss vurdere et eksempel:

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

Vi forventer at tildeling av objekter koster mer enn å gjøre noe i det hele tatt. Men hvis vi kjører referanseverdiene:

Referansemodus Cnt Score Error Units BenchMark.doIngenting avgt 40 0,609 ± 0,006 ns / op BenchMark.objectCreation avgt 40 0,613 ± 0,007 ns / op

Å finne et sted i TLAB, det er nesten gratis å lage og initialisere et objekt. Bare ved å se på disse tallene, bør vi vite at noe ikke helt tilføyer seg her.

Her er vi offer for eliminering av død kode. Kompilatorer er veldig flinke til å optimalisere den overflødige koden. Faktisk er det akkurat det JIT-kompilatoren gjorde her.

For å forhindre denne optimaliseringen, bør vi på en eller annen måte lure kompilatoren og få den til å tro at koden brukes av en annen komponent. En måte å oppnå dette på er å returnere det opprettede objektet:

@Benchmark @OutputTimeUnit (TimeUnit.NANOSECONDS) @BenchmarkMode (Mode.AverageTime) offentlige Object pillarsOfCreation () {returner nytt objekt (); }

Vi kan også la Svart hull konsumere det:

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

Å ha Svart hull konsumere objektet er en måte å overbevise JIT-kompilatoren om ikke å bruke optimalisering av dead code eliminering. Uansett, hvis vi kjører disse referanseverdiene igjen, ville tallene være mer fornuftige:

Benchmark Mode Cnt Score Error Units BenchMark.blackHole avgt 20 4.126 ± 0.173 ns / op BenchMark.doIngen 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. Konstant folding

La oss vurdere enda et eksempel:

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

Beregninger basert på konstanter kan gi nøyaktig samme utdata, uavhengig av antall henrettelser. Derfor er det en ganske god sjanse for at JIT-kompilatoren erstatter logaritmefunksjonsanropet med resultatet:

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

Denne formen for delvis evaluering kalles konstant folding. I dette tilfellet unngår konstant folding helt Math.log samtale, som var hele poenget med referanseindeksen.

For å forhindre konstant folding, kan vi kapsle inn den konstante tilstanden i et tilstandsobjekt:

@State (Scope.Benchmark) offentlig statisk klasse Logg {public int x = 8; } @Benchmark offentlig dobbeltlogg (Logginngang) {return Math.log (input.x); }

Hvis vi kjører disse målene mot hverandre:

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

Tilsynelatende, den Logg benchmark gjør noe seriøst arbeid i forhold til foldedLog, som er fornuftig.

8. Konklusjon

Denne opplæringen fokuserte på og viste Java's micro benchmarking sele.

Som alltid kan kodeeksempler finnes på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found