Microbenchmarking med Java

1. Introduktion

Denne hurtige artikel er fokuseret på JMH (Java Microbenchmark Harness). Først lærer vi API'et og lærer dets grundlæggende. Så ville vi se et par bedste fremgangsmåder, som vi bør overveje, når vi skriver mikrobenchmarks.

Kort sagt, JMH tager sig af ting som JVM-opvarmnings- og kodeoptimeringsstier, hvilket gør benchmarking så enkel som muligt.

2. Kom godt i gang

For at komme i gang kan vi faktisk fortsætte med at arbejde med Java 8 og blot definere afhængighederne:

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

De nyeste versioner af JMH Core og JMH Annotation Processor findes i Maven Central.

Opret derefter et simpelt benchmark ved at bruge @Benchmark kommentar (i enhver offentlig klasse):

@Benchmark public void init () {// Gør intet}

Derefter tilføjer vi hovedklassen, der starter benchmarking-processen:

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

Kører nu BenchmarkRunner vil udføre vores uden tvivl noget ubrugelige benchmark. Når kørslen er afsluttet, præsenteres en oversigtstabel:

# Kør komplet. Samlet tid: 00:06:45 Benchmark Mode Cnt Score Fejlenheder BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops / s

3. Typer af benchmarks

JMH understøtter nogle mulige benchmarks: Gennemløb,Gennemsnitlig tid,SampleTimeog SingleShotTime. Disse kan konfigureres via @BenchmarkMode kommentar:

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

Den resulterende tabel har en gennemsnitlig tidsmåling (i stedet for kapacitet):

# Kør komplet. Samlet tid: 00:00:40 Benchmark Mode Cnt Score Fejlenheder BenchMark.init avgt 20 ≈ 10⁻⁹ s / op

4. Konfiguration af opvarmning og udførelse

Ved hjælp af @Gaffel kommentar, vi kan indstille, hvordan benchmark-udførelsen sker: værdi parameter styrer hvor mange gange benchmarket udføres, og opvarmning parameter styrer, hvor mange gange et benchmark tørrer, før resultaterne indsamles, for eksempel:

@Benchmark @Fork (værdi = 1, warmups = 2) @BenchmarkMode (Mode.Throughput) offentlig ugyldig init () {// Gør intet}

Dette instruerer JMH om at køre to opvarmningsgafler og kassere resultater, inden de går videre til ægte tidsbestemt benchmarking.

Også den @Opvarmning annotering kan bruges til at kontrollere antallet af opvarmnings-iterationer. For eksempel, @Warmup (iterationer = 5) fortæller JMH, at fem opvarmnings-iterationer er tilstrækkelige i modsætning til standard 20.

5. Stat

Lad os nu undersøge, hvordan en mindre triviel og mere vejledende opgave med benchmarking af en hashingalgoritme kan udføres ved hjælp af Stat. Antag, at vi beslutter at tilføje ekstra beskyttelse mod ordbogangreb på en adgangskodedatabase ved at hashe adgangskoden et par hundrede gange.

Vi kan undersøge effektpåvirkningen ved hjælp af en Stat objekt:

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

Vores benchmarkmetode vil så se ud:

@Fork (værdi = 1, opvarmning = 1) @Benchmark @BenchmarkMode (Mode.Throughput) offentlig ugyldig benchMurmur3_128 (ExecutionPlan plan) {for (int i = plan.iterations; i> 0; i--) {plan.murmur3. putString (plan.password, Charset.defaultCharset ()); } plan.murmur3.hash (); }

Her, marken gentagelser vil blive befolket med passende værdier fra @Param kommentar fra JMH, når den overføres til benchmarkmetoden. Det @Opsætning annoteret metode påberåbes før hver påkaldelse af benchmark og skaber en ny Hasher sikre isolation.

Når udførelsen er færdig, får vi et resultat svarende til nedenstående:

# Kør komplet. Samlet tid: 00:06:47 Benchmark (iterationer) Mode Cnt Score Fejlenheder 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 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 kører mikrobenchmarks, er det meget vigtigt at være opmærksom på optimeringer. Ellers kan de påvirke benchmarkresultaterne på en meget vildledende måde.

For at gøre sagen lidt mere konkret, lad os overveje 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 objektallokering koster mere end slet ikke at gøre noget. Men hvis vi kører benchmarks:

Benchmark Mode Cnt Score Fejlenheder BenchMark.do Intet gennemsnit 40 0,609 ± 0,006 ns / op BenchMark.objectCreation avgt 40 0,613 ± 0,007 ns / op

Det er næsten gratis at finde et sted i TLAB, at oprette og initialisere et objekt! Bare ved at se på disse tal, skal vi vide, at noget ikke helt tilføjes her.

Her er vi offer for eliminering af død kode. Compilere er meget gode til at optimere den overflødige kode. Faktisk er det præcis, hvad JIT-kompilatoren gjorde her.

For at forhindre denne optimering skal vi på en eller anden måde narre compileren og få den til at tro, at koden bruges af en anden komponent. En måde at opnå dette på er bare at returnere det oprettede objekt:

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

Vi kan også lade Sort hul forbruge det:

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

At have Sort hul forbruge objektet er en måde at overbevise JIT-kompilatoren om ikke at anvende optimering af dead code eliminering. Under alle omstændigheder, hvis vi kører disse benchmarks igen, ville tallene give mere mening:

Benchmark Mode Cnt Score Error Units BenchMark.blackHole avgt 20 4.126 ± 0.173 ns / op BenchMark.do Intet gennemsnit 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 foldning

Lad os overveje endnu et eksempel:

@Benchmark offentlig dobbeltfoldet log () {int x = 8; returner Math.log (x); }

Beregninger baseret på konstanter returnerer nøjagtigt den samme output uanset antallet af henrettelser. Derfor er der en ret god chance for, at JIT-kompilatoren erstatter logaritmefunktionsopkaldet med sit resultat:

@Benchmark offentlig dobbeltfoldet log () {return 2.0794415416798357; }

Denne form for delvis evaluering kaldes konstant foldning. I dette tilfælde undgår konstant foldning helt Math.log opkald, som var hele punktet i benchmarket.

For at forhindre konstant foldning kan vi indkapsle den konstante tilstand inde i et tilstandsobjekt:

@State (Scope.Benchmark) offentlig statisk klasse Log {public int x = 8; } @Benchmark offentlig dobbeltlog (Logindgang) {returner Math.log (input.x); }

Hvis vi kører disse benchmarks mod hinanden:

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

Tilsyneladende log benchmark udfører noget seriøst arbejde i forhold til foldetLog, hvilket er fornuftigt.

8. Konklusion

Denne vejledning fokuserede på og fremviste Java's micro benchmarking seletøj.

Som altid kan kodeeksempler findes på GitHub.


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