Mikrobenchmarking s Javom

1. Uvod

Ovaj je kratki članak usredotočen na JMH (Java Microbenchmark Harness). Prvo se upoznajemo s API-jem i učimo njegove osnove. Tada bismo vidjeli nekoliko najboljih praksi koje bismo trebali uzeti u obzir prilikom pisanja mikroznaka.

Jednostavno rečeno, JMH brine o stvarima poput JVM-ovih putova zagrijavanja i optimizacije koda, čineći benčmarking što jednostavnijim.

2. Početak rada

Za početak zapravo možemo nastaviti raditi s Javom 8 i jednostavno definirati ovisnosti:

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

Najnovije verzije JMH Core i JMH Annotation Processor mogu se naći u Maven Central.

Zatim stvorite jednostavnu referentnu vrijednost pomoću @Benchmark napomena (u bilo kojem javnom razredu):

@Benchmark public void init () {// Ne radi ništa}

Zatim dodajemo glavnu klasu koja započinje postupak usporedne analize:

javna klasa BenchmarkRunner {public static void main (String [] args) baca iznimku {org.openjdk.jmh.Main.main (args); }}

Sad trči BenchmarkRunner izvršit će naše vjerojatno pomalo beskorisno mjerilo. Nakon završetka izvođenja prikazuje se sažeta tablica:

# Trčanje dovršeno. Ukupno vrijeme: 00:06:45 Benchmark Mode Cnt Rezultat Pogreške Jedinice BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops / s

3. Vrste mjerila

JMH podržava neka moguća mjerila: Propusnost,Prosječno vrijeme,Vrijeme uzorka, i SingleShotTime. To se može konfigurirati putem @BenchmarkMode napomena:

@Benchmark @BenchmarkMode (Mode.AverageTime) public void init () {// Ne radi ništa}

Rezultirajuća tablica imat će mjernu vrijednost prosječnog vremena (umjesto propusnosti):

# Trčanje dovršeno. Ukupno vrijeme: 00:00:40 Benchmark Mode Cnt Rezultat Jedinice pogrešaka BenchMark.init prosjek 20 ≈ 10⁻⁹ s / op

4. Konfiguriranje zagrijavanja i izvršavanja

Korištenjem @Fork napomena, možemo postaviti način na koji se izvršava referentna vrijednost: vrijednost parametar kontrolira koliko će se puta izvršiti referentna vrijednost, a zagrijati se parametar kontrolira koliko će se puta referentna vrijednost suho pokretati prije nego što se prikupe rezultati, na primjer:

@Benchmark @Fork (value = 1, warmups = 2) @BenchmarkMode (Mode.Throughput) public void init () {// Ne radi ništa}

Ovo nalaže JMH-u da pokrene dvije vilice za zagrijavanje i odbaci rezultate prije nego što pređe na stvarnu vremensku usporedbu.

Također, @Zagrijati se napomena se može koristiti za kontrolu broja ponavljanja zagrijavanja. Na primjer, @Warmup (iteracije = 5) kaže JMH-u da će biti dovoljno pet ponavljanja zagrijavanja, za razliku od zadanih 20.

5. Država

Istražimo sada kako se manje trivijalan i indikativniji zadatak benčmarkinga algoritma raspršivanja može izvesti korištenjem država. Pretpostavimo da smo odlučili dodati dodatnu zaštitu od napada rječnika na bazu podataka lozinki tako što ćemo lozinku raspršiti nekoliko stotina puta.

Učinak na izvedbu možemo istražiti pomoću a država objekt:

@ Itate javne klase ExecutionPlan {@Param ({"100", "200", "300", "500", "1000"}) javne klase; javni hasher žamor3; lozinka javnog niza = "4v3rys3kur3p455w0rd"; @Setup (Level.Invocation) javna praznina setUp () {murmur3 = Hashing.murmur3_128 (). NewHasher (); }}

Naša referentna metoda tada će izgledati ovako:

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

Evo, polje ponavljanja bit će popunjena odgovarajućim vrijednostima iz @Param anotacija od strane JMH kada se proslijedi na referentnu metodu. The @Postaviti anotirana metoda poziva se prije svakog pozivanja mjerila i stvara novu Mašina za mljevenje mesa osiguravanje izolacije.

Kada je izvršenje završeno, dobit ćemo rezultat sličan onome u nastavku:

# Trčanje dovršeno. Ukupno vrijeme: 00:06:47 Benchmark (iteracije) Način rada Cnt Rezultat Pogreške Jedinice BenchMark.benchMurmur3_128 100 thrpt 20 92463.622 ± 1672.227 ops / s BenchMark.benchMurmur3_128 200 thrpt 20 39737.532 ± 5294.200 ops / s BenchMarkrben12 628 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. Eliminacija mrtvog koda

Kada pokrećete mikroobilježja, vrlo je važno biti svjestan optimizacija. Inače, oni mogu utjecati na rezultate mjerenja na vrlo zavaravajući način.

Da stvar bude malo konkretnija, razmotrimo primjer:

@Benchmark @OutputTimeUnit (TimeUnit.NANOSECONDS) @BenchmarkMode (Mode.AverageTime) javna void neNothing () {} @Benchmark @OutputTimeUnit (TimeUnit.NANOSECONDS) @BenchmarkMode (Mode.AverageTime) (new void objectCreed); }

Očekujemo da dodjela predmeta košta više nego što se uopće ne radi. Međutim, ako pokrenemo mjerila:

BenchMark.doNišta prosj. 40 0,609 ± 0,006 ns / op BenchMark.objectCreation prosjek 40 0,613 ± 0,007 ns / op

Navodno je pronalaženje mjesta u TLAB-u, stvaranje i inicijalizacija objekta gotovo besplatno! Samo promatrajući ove brojke, trebali bismo znati da se ovdje nešto ne slaže.

Evo, mi smo žrtva uklanjanja mrtvog koda. Prevoditelji su vrlo dobri u optimizaciji suvišnog koda. Zapravo je to upravo ono što je ovdje radio JIT prevodilac.

Da bismo spriječili ovu optimizaciju, trebali bismo nekako prevariti kompajler i navesti ga da misli da kod koristi neka druga komponenta. Jedan od načina da se to postigne je samo vraćanje stvorenog objekta:

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

Također, možemo dopustiti Crna rupa konzumirajte ga:

@Benchmark @OutputTimeUnit (TimeUnit.NANOSECONDS) @BenchmarkMode (Mode.AverageTime) javna praznina blackHole (Blackhole blackhole) {blackhole.consume (novi objekt ()); }

Imati Crna rupa konzumiraj objekt je način da uvjeriš JIT kompajler da ne primjenjuje optimizaciju eliminacije mrtvog koda. Svejedno, ako ponovo pokrenemo teze, brojke bi imale više smisla:

BenchMark.blackHole avgt BenchMark.blackHole avg. BenchMark.doNothing avgt 20 0.639 ± 0.012 ns / op BenchMark.objectCreation avgt 20 0.635 ± 0.011 ns / op BenchMark.pillarsOfCreation avgt.

7. Stalno preklapanje

Razmotrimo još jedan primjer:

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

Izračuni na temelju konstanti mogu vratiti potpuno isti rezultat, bez obzira na broj izvršavanja. Stoga postoji prilično dobra šansa da će JIT kompajler svojim rezultatom zamijeniti poziv funkcije logaritma:

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

Ovaj oblik djelomičnog vrednovanja naziva se stalnim preklapanjem. U ovom slučaju, stalnim preklapanjem potpuno se izbjegava Matematika.log poziva, što je bila cijela poanta mjerila.

Da bismo spriječili neprestano presavijanje, možemo enkapsulirati konstantno stanje unutar objekta stanja:

@State (Scope.Benchmark) javni statični zapis klase {public int x = 8; } @Benchmark javni dvostruki zapisnik (unos dnevnika) {return Math.log (input.x); }

Ako ove mjerila pokrenemo jedni protiv drugih:

BenchMark.foldedLog thrpt 20 449313097.433 ± 11850214.900 ops / s BenchMark.log bencMark.log thrpt 20 35317997.064 ± 604370.461 ops / s

Očito je zapisnik benchmark radi ozbiljan posao u odnosu na foldedLog, što je razumno.

8. Zaključak

Ovaj se vodič usredotočio na i prikazao Javinu pojasu za mikro benchmarking.

Kao i uvijek, primjeri koda mogu se naći na GitHubu.