images/logo.png
SHWB na blogspot  | Uživatel: Nepřihlášen

O programování 02 - Efektivita funkcionálního přístupu v Java 8

V prvním díle jsem vysvětlil záměr této série článků a nyní je na řadě první část a to (naivní) porovnání výkonnosti běžného cyklu foreach a funkcionálního přístupu v Javě 8. Toto porovnání jsem vyzkoušel na následující jednoduché úloze: Mám seznam objektů a chci ho zkopírovat do druhého seznamu, přičemž jednotlivé prvky modifikuji. Jinými slovy, provedu iteraci přes prvky vstupního seznamu a do výstupního seznamu vložím změněné hodnoty. Abych celý problém zjednodušil co nejvíce, budu pracovat s ArrayListem Integerů a změna bude spočívat v přičtení jedničky.

Nejdříve deklaruji vstupní a výstupní seznamy:



List<Integer> inputList = new ArrayList<>(); List<Integer> outputList = new ArrayList<>();

Poté do vstupního seznamu inputList vložím hodnoty 1 až 10 000, tuto část kódu neuvádím, pro vlastní účely článku není podstatná.

Přímočará implementace uvedené úlohy s využitím běžného cyklu foreach vypadá takto:



for (Integer value : inputList) { outputList.add(++value); }

Pro seznam 10 000 Integerů trvá takto zapsaný cyklus cca 5 milisekund a pro milion prvků zhruba 100 milisekund.

NetBeansy mi radí, abych cyklus přepsal s využitím streamu následujícím způsobem:



inputList.stream().forEach((value) -> { outputList.add(++value); });

Takto přepsaný cyklus trvá pro 10 000 Integerů cca 120 milisekund a pro milion 150 milisekund. Což vede k závěru, že časová výkonnost streamu je sice "skoro" konstantní (logaritmická složitost je asi přesnější), ale pro běžné případy je o dva řády horší než běžný foreach.

Takový výsledek jsem opravdu nečekal a tak jsem hledal na internetu, zda jsem něco nepřehlédl. Našel jsem však více lidí s podobným výsledkem a zajímavé byly reakce na toto pozorování. Jako první argument se uvádělo, že takhle (zjistit Date před a po a odečíst) se mikrobenchmarky nedělají. Druhý argument byl, že JVM potřebuje nějaký prostor na warm up a teprve jako třetí v pořadí následovalo vysvětlení, proč se tak děje. Vysvětlení celkem chápu, jde třeba i o to, že obyčejný (a jednoúčelový) for cyklus se optimalizuje nějakých 15 let, kdežto stream ani ne dva roky a že vše je o přístupu k objektům a jejich konstrukci. A teprve po těchto třech argumentech následuje konstatování, že to tak prostě je.

Pod vlivem diskusí jsem se rozhodl implementovat warm up před vlastním měřením, cyklus spustit opakovaně a počítat průměrnou hodnotu, minima a maxima. V takovém případě je výsledek mnohem lepší, po warm upu, a to stačí jedno až dvě spuštění, je implementace využívající streamy postupně čím dál rychlejší, takže celý výkonnostní problém je pouze u prvního spuštění.

Pokud mám popsat implementaci detailněji, tak se cyklus pro N prvkové pole spustí WARMUP krát a teprve poté probíhá vlastní měření výkonnosti. A to tak, že se cyklus spustí 1000x a z tohoto tisíce průběhů se vypíše minimum, maximum a průměr na jeden cyklus. Výsledky v milisekundách uvádí následující tabulky.

WARMUP = 10

N100100010000100000
standard forEachavg: 0,017
min: 0
max: 3
avg 0,086
min: 0
max: 9
avg: 0,223
min: 0
max: 4
avg 2,276
min: 1
max: 16
stream().forEach()avg: 0,018
min: 0
max: 4
avg: 0,049
min: 0
max: 2
avg: 0,254
min: 0
max: 10
avg: 1,819
min: 1
max: 23

WARMUP = 100

N100100010000100000
standard forEachavg: 0,009
min: 0
max: 1
avg: 0,064
min: 0
max: 10
avg: 0,199
min: 0
max: 4
avg: 2,023
min: 1
max: 16
stream().forEach()avg: 0,006
min: 0
max: 1
avg: 0,049
min: 0
max: 5
avg: 0,190
min: 0
max: 4
avg: 1,700
min: 0
max: 11

Dále mě zajímalo, jaký je časový průběh jednotlivých iterací v rámci warm upu, tedy kolik času zabere prvních n-průběhů cyklem. Výsledky jsou opět uvedeny v tabulce:

N = 10 000foreachstream N = 100 000foreachstream
07109 038131
131 1118
241 263
331 343
431 452
531 543
621 642
721 743
810 843
910 9162
1051 1025
1120 1113
1210 1222
1310 1322
1410 1422
1510 1522
1610 1622
1711 1722
1810 1822
1921 1922
2010 20108
2111 2112
2211 2222
2310 2322
2410 2422
2500 2522

Takže, znovu opakuji a vyvracím prvotní dojem. Po warm upu je stream postupně čím dál rychlejší a jeho časový průběh je stabilnější, jen to první spuštění je řádově horší.

Během výše uvedených pokusů jsem si ověřil, že takhle se mikro benchmarky opravdu dělat nedají, časy se při opětovném spuštění lišily od desítek procent až ke dvojnásobku.

V dalších dílech budu hledat způsob, jak udělat mikrobenchmarky spolehlivější, ale ještě předtím se podívám na hlavní argument pro funkcionální zápis a tím je přehlednost kódu.

29.11.2016
Přidat názor:
Vyhrazuji si právo libovolný komentář smazat bez udání důvodu. Kritika mi nevadí, ale chci omezit anonymní výkřiky, které nemají s tématem nic společného.
V textu je možné používat HTML tagy a tuto zjednodušenou MarkDown syntaxi
Jméno
Text
Postřehy:
10.11.2019: Arduino 02 - Co koupit?
Když jsem psal první díl o Arduinu a elektrotechnice, tak jsem si myslel, že na letošní Vánoce budu vybírat nějaký vhodný set. Na podzim to sice vypadalo, že nebudu mít čas se něčemu takovému věnovat, teď to zase vypadá, že by chvíle času zbýt mohla. A tak se zatím zabývám alespoň rešerší možností. Napadají mě totiž otázky jako: originální deska nebo klon, jaká verze desky, jen základní deska a 
extravaganza.controverso@seznam.cz: Zdravím, krásný a informacemi nabitý blog. Musím pochválit. Plánuji rozjet undergroundový zin, co by se týkal black matalu, ambientu, satanismu, left hand
Poslední diskuse Postřehy
O programování 06 - Návrhové vzory - síla i slabina Javy
P.S. samozrejme "Context" mel byt "Client" .. To jsem jen narazil na nejak divne pojmenovany diagram.. (Context je samozrejme trosku neco jineho...)
...
David | 25.02.2017
O programování 06 - Návrhové vzory - síla i slabina Javy
To k cemu jsi dosel (tedy implementace LooperRunner + ILoopMethod), tak je ta prava Strategy by GoF :) To co je tam dulezite je ze Context (LooperRunner) je oddeleny od Strategy (ILoopMethod),
...
David | 25.02.2017
O programování 03 - Přehlednost funkcionálního zápisu v Java 8
Máš pravdu, to map je tam zbytečné. Odněkud jsem to opsal a nezkontroloval. Tím ovšem trochu padá pointa celého článku.
...
Saha | 14.12.2016
O programování 03 - Přehlednost funkcionálního zápisu v Java 8
Jen takova otazka k tomu druhemu prikladu:
Proc tam tu cast s "map" ktera de facto s prvky toho streamu nic nedela? Nestacilo by
list.stream().reduce(0, Integer::sum);
?
Ja teda moc
...
David P. | 13.12.2016
Paleo na půl - 01 - První tři dny bez mléka
Kvalitní hořké čokolády jsou bez mléka.. :) (a to i ty méně "kvalitní"). Mléko bývá součástí jen těch "sladkých".
...
David | 04.05.2015
Statistiky
Aktualizováno: 10.11.2019
Počet článků/fotek: 1377/13846
(C) Saha - 1990 - 2019 - Verze 1.3.32 - 11.05.2019 - Generated by SHREC 2.216
Veškeré zde uvedené materiály vyjadřují pouze moje soukromé názory (s výjimkou knihy návštěv a diskusí, kam může přispívat kdokoliv), a pokud s nimi někdo nesouhlasí, tak je to jeho problém, nikoliv můj.