O programování 04 - Java Microbenchmark Harness
Ve druhém díle jsem si prakticky ověřil, že používat pro (mikro) benchmark rozdíl času začátku a konce měřené operace není to pravé ořechové, výsledné časy se při opakování testu lišily o desítky procent a výjimkou nebyly ani násobné časy. Je celkem logické, že pokud je čas měřené operace v řádech milisekund, tak je přesnost měření v milisekundách nedostatečná.
Proto jsem hledal lepší řešení a jako první jsem našel Java Microbenchmark Harness, v uvedeném odkazu je i pěkně vysvětleno, co a proč je tak obtížné na Java mikro benchmarku. Bohužel jsem ani s použitím tutoriálů zde a zde dlouho nemohl testování rozchodit a ani stackoverflow moc nepomohl. Až po delším úsilí (=pár desítek minut) jsem pochopil, jak to má fungovat a implementoval jsem funkční verzi. Z výsledků jsem nejdříve nebyl moc moudrý, JMH totiž spouští celou řadu testů a měří různé charakteristiky (AverageTime, SampleTime, SingleShotTime, Throughput), ale kombinací intuice a dokumentace se dá celkem dobře pochopit, co který test znamená.
Principiálně je JMH o pár anotacích a následující příkladové konfiguraci:
final Options opt = new OptionsBuilder()
.include(ListPlusPlus.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(1)
.build();
Primárně jsem porovnával běžný cyklus foreach se stream().foreach(), stejně jako v mnou implementovaném benchmarku a výsledek je následující:
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
ListPlusPlus.foreach | thrpt | 5 | 8,070 | ± 2,046 | ops/ms |
ListPlusPlus.streamForEach | thrpt | 5 | 8,309 | ± 1,935 | ops/ms |
ListPlusPlus.foreach | avgt | 5 | 0,125 | ± 0,050 | ms/op |
ListPlusPlus.streamForEach | avgt | 5 | 0,124 | ± 0,028 | ms/op |
ListPlusPlus.foreach | sample | 40681 | 0,123 | ± 0,001 | ms/op |
ListPlusPlus.streamForEach | sample | 39544 | 0,127 | ± 0,003 | ms/op |
ListPlusPlus.foreach | ss | 5 | 1,059 | ± 2,028 | ms/op |
ListPlusPlus.streamForEach | ss | 5 | 0,422 | ± 0,677 | ms/op |
I tento test potvrdil, že stream().foreach() je výkonnější s výjimkou prvního spuštění. Stabilita měření však není ani v případě JMH zdaleka ideální, výsledek 1 ± 2 ms představuje docela velký výkyv a defakto se až tak neliší od naivní implementace založené na rozdílu času. Další nevýhodou JMH je to, že spuštění testu je opravdu pomalé - 5 warmapů a 5 iterací na měření mi v naivní implementaci trvalo pár vteřin ale v JMH to zabralo přes dvě minuty.
Spustil jsem ještě testy pro porovnání prvních pěti volání a opět nic nového pod sluncem, pouze potvrzení již konstatovaného závěru.
# Benchmark mode: Single shot invocation time # Benchmark: k8.chapter4.support.ListPlusPlus.streamForEach
# Run progress: 99,98% complete, ETA 00:00:00 # Fork: 1 of 1 # Warmup Iteration 1: 75,062 ms/op # Warmup Iteration 2: 0,926 ms/op # Warmup Iteration 3: 0,593 ms/op # Warmup Iteration 4: 0,717 ms/op # Warmup Iteration 5: 0,792 ms/op Iteration 1: 0,745 ms/op
# Benchmark mode: Single shot invocation time # Benchmark: k8.chapter4.support.ListPlusPlus.foreach
# Run progress: 99,97% complete, ETA 00:00:00 # Fork: 1 of 1 # Warmup Iteration 1: 3,075 ms/op # Warmup Iteration 2: 2,849 ms/op # Warmup Iteration 3: 1,956 ms/op # Warmup Iteration 4: 2,559 ms/op # Warmup Iteration 5: 2,547 ms/op Iteration 1: 2,181 ms/op
Řekl bych, že JMH nemá oproti naivní implementaci založené na rozdílu času velký přínos v časové stabilitě testů.
Mohl bych si sice vypomoci trochou statistiky (průměrná odchylka atd), ale problematika mikro benchmarků mě v tuto chvíli nepřipadá dostatečně zajímavá. Pokud by mě však zajímala, tak bych hledal buď jiný framework nebo bych zobecnil svůj naivní přístup a např. místo Date() bych zkusil použít přesnější Instant(). Nebo bych pro měření mohl použít aspekty, protože jak naivní implementace tak JMH znamenají zásahy do kódu, což se mi nelíbí, protože musím udržovat "dvě" varianty kódu, jednu testovací a druhou produkční.
- O programování 01 - Úvod
- O programování 02 - Efektivita funkcionálního přístupu v Java 8
- O programování 03 - Přehlednost funkcionálního zápisu v Java 8
- O programování 05 - Další varianty Javovské implementace foreach
- O programování 06 - Návrhové vzory - síla i slabina Javy
- O programování 07 - Funkce jako argument aneb od factory k lambdě v Javě
- O programování 08 - Jak v Javě předat metodu, která bude mít různé parametry
- O programování 09 - Pokročilé využití streamů a funkcionálního přístupu v Java 8
- O programování 10 - Java a inspirace v Ruby
- O programování 11 - Minimalistické programy a Java
- O programování 12 - Paralelní implementace násobení matic v Java - úvod