Mikrobenchmarking s Javou

1. Úvod

Tento rýchly článok je zameraný na JMH (Java Microbenchmark Harness). Najprv sa oboznámime s API a osvojíme si jeho základy. Potom by sme videli niekoľko osvedčených postupov, ktoré by sme mali brať do úvahy pri písaní mikroznačiek.

Jednoducho povedané, JMH sa stará o veci, ako sú cesty zahrievania JVM a optimalizácie kódu, vďaka čomu je testovanie čo najjednoduchšie.

2. Začíname

Na začiatok môžeme skutočne pracovať s Java 8 a jednoducho definovať závislosti:

 org.openjdk.jmh jmh-core 1,19 org.openjdk.jmh jmh-generator-annprocess 1,19 

Najnovšie verzie JMH Core a JMH Annotation Processor nájdete v Maven Central.

Ďalej vytvorte jednoduchý benchmark využitím @Benchmark anotácia (v ktorejkoľvek verejnej triede):

@Benchmark public void init () {// Nerobiť nič}

Potom pridáme hlavnú triedu, ktorá spúšťa proces testovania:

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

Teraz beží BenchmarkRunner vykoná našu pravdepodobne trochu zbytočnú referenčnú hodnotu. Po dokončení behu sa zobrazí súhrnná tabuľka:

# Spustenie dokončené. Celkový čas: 00:06:45 Benchmark Mode Cnt Score Error Units BenchMark.init thpt 200 3099210741,962 ± 17510507,589 ops / s

3. Typy referenčných hodnôt

JMH podporuje niektoré možné referenčné hodnoty: Priepustnosť,Priemerný čas,Čas vzorkovaniaa SingleShotTime. Môžu byť konfigurované pomocou @BenchmarkMode anotácia:

@Benchmark @BenchmarkMode (Mode.AverageTime) public void init () {// Nerobiť nič}

Výsledná tabuľka bude mať priemernú časovú metriku (namiesto priepustnosti):

# Spustenie dokončené. Celkový čas: 00:00:40 Benchmark Mode Cnt Skóre Chyba Jednotky BenchMark.init priem. 20 ≈ 10⁻⁹ s / op

4. Konfigurácia zahrievania a spustenia

Použitím @Vidlička anotáciu, môžeme nastaviť, ako sa vykonávanie benchmarku deje: hodnotu parameter určuje, koľkokrát bude test vykonaný a zahrievanie parameter určuje, koľkokrát bude test bežať nasucho predtým, ako sa zhromaždia výsledky, napríklad:

@Benchmark @Fork (value = 1, warmups = 2) @BenchmarkMode (Mode.Throughput) public void init () {// Nerobiť nič}

Toto dáva pokyn spoločnosti JMH, aby spustila dve zahrievacie vidlice a zahodila výsledky pred prechodom na skutočne načasované testovanie.

Tiež @ Zahrievanie anotáciu možno použiť na riadenie počtu iterácií zahrievania. Napríklad, @ Zahrievanie (iterácie = 5) hovorí JMH, že bude stačiť päť zahrievacích iterácií, na rozdiel od predvolených 20.

5. Štát

Poďme teraz preskúmať, ako je možné vykonať menej triviálnu a indikatívnejšiu úlohu porovnávania hashovacieho algoritmu využitím Štát. Predpokladajme, že sa rozhodneme pridať ďalšiu ochranu pred slovníkovými útokmi na databázu hesiel tak, že heslo niekoľkokrát zahašujete.

Vplyv na výkon môžeme preskúmať pomocou a Štát objekt:

@State (Scope.Benchmark) verejná trieda ExecutionPlan {@Param ({"100", "200", "300", "500", "1000"}) verejné interácie; verejný hasher šelest3; public String password = "4v3rys3kur3p455w0rd"; @Setup (Level.Invocation) public void setUp () {murmur3 = Hashing.murmur3_128 (). NewHasher (); }}

Naša referenčná metóda potom bude vyzerať takto:

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

Tu, pole iterácie budú vyplnené príslušnými hodnotami z @Param poznámka JMH, keď sa odovzdáva referenčnej metóde. The @Nastaviť anotovaná metóda sa vyvolá pred každým vyvolaním referenčnej hodnoty a vytvorí novú Hasher zabezpečenie izolácie.

Po dokončení spustenia dostaneme výsledok podobný nasledujúcemu:

# Spustenie dokončené. Celkový čas: 00:06:47 Benchmark (iterácie) Režim Cnt Skóre Chyba Jednotky BenchMark.benchMurmur3_128 100 impulzov 20 92463,622 ± 1672,227 ops / s BenchMark.benchMurmur3_128 200 impulzov 20 39737,532 ± 5294,200 ops / s BenchMark.bench128,383 ops / s BenchMark.benchMurmur3_128 500 impulzov 20 18315.211 ± 222,534 ops / s BenchMark.benchMurmur3_128 1 000 impulzov 20 8960,008 ± 658,524 ops / s

6. Odstránenie mŕtveho kódu

Pri spustení minimálnych značiek je veľmi dôležité uvedomiť si optimalizáciu. V opačnom prípade môžu ovplyvniť výsledky porovnávacieho testu veľmi zavádzajúcim spôsobom.

Aby sme veci trochu konkretizovali, zvážme príklad:

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

Očakávame, že náklady na alokáciu objektov budú viac ako nič neurobiť. Ak však spustíme referenčné hodnoty:

Benchmark Mode Cnt Score Error Units BenchMark.do Nič priem. 40 0,609 ± 0,006 ns / op BenchMark.objectCreation priem. 40 0,613 ± 0,007 ns / op

Zjavné nájdenie miesta v TLAB, vytvorenie a inicializácia objektu je takmer zadarmo! Už pri pohľade na tieto čísla by sme mali vedieť, že tu sa niečo celkom nezhromažďuje.

Tu sme sa stali obeťou eliminácie mŕtveho kódu. Kompilátory veľmi dobre optimalizujú redundantný kód. V skutočnosti je to presne to, čo tu urobil kompilátor JIT.

Aby sme zabránili tejto optimalizácii, mali by sme kompilátor nejako oklamať a domyslieť si, že kód používa nejaká iná súčasť. Jedným zo spôsobov, ako to dosiahnuť, je vrátenie vytvoreného objektu:

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

Môžeme tiež nechať Čierna diera konzumovať:

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

Majúce Čierna diera konzumovať objekt je spôsob, ako presvedčiť kompilátor JIT, aby neaplikoval optimalizáciu eliminácie mŕtveho kódu. Ak by sme znova spustili tieto referenčné hodnoty, čísla by mali väčší zmysel:

Benchmark Mode Cnt Score Error Units BenchMark.blackHole priem. 20 4,126 ± 0,173 ns / op BenchMark.do nič priem. 20 0,639 ± 0,012 ns / op BenchMark.objectCreation priem. 20 0,635 ± 0,011 ns / op BenchMark.pillarsOfCreation priem. 20 4,061 ± 0,037

7. Neustále skladanie

Uvažujme ešte o jednom príklade:

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

Výpočty založené na konštantách môžu vrátiť úplne rovnaký výstup bez ohľadu na počet vykonaní. Existuje teda celkom dobrá šanca, že kompilátor JIT nahradí volanie funkcie logaritmu svojím výsledkom:

@Benchmark public double foldedLog () {návrat 2.0794415416798357; }

Táto forma čiastočného vyhodnotenia sa nazýva neustále skladanie. V takom prípade sa neustálemu skladaniu úplne vyhnete Math.log call, čo bol celý bod benchmarku.

Aby sme zabránili neustálemu skladaniu, môžeme zapuzdriť konštantný stav vo vnútri objektu stavu:

@State (Scope.Benchmark) verejná statická trieda Denník {public int x = 8; } @Benchmark public double log (Log input) {return Math.log (input.x); }

Ak porovnáme tieto kritériá navzájom:

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

Podľa všetkého log v porovnaní s foldedLog, čo je rozumné.

8. Záver

Tento výukový program sa zameral na a predstavil postroj Java pre mikro benchmarking.

Ako vždy, príklady kódov možno nájsť na GitHub.