O programování 05 - Další varianty Javovské implementace foreach
V rámci předchozích dílů jsem zkoumal dvě varianty implementace foreach cyklu v Javě, konkrétně šlo o kopírování seznamu spojené s modifikací jednotlivých prvků. Porovnával jsem standardní cyklus foreach a jeho přímočarou funkcionální verzi z pohledu výkonnosti a přehlednosti. Jenom pro připomenutí, šlo o následující konstrukce pro foreach:
for (Integer value: inputList) {
outputList.add(++value);
}
resp. streamová varianta:
inputList.stream().forEach((value) -> {
outputList.add(++value);
});
Při prvním pohledu na streamovou variantu se mi nelíbí, že jde o metodu a ne koncept jazyka, což jsem rozebíral ve třetím díle.
Protože metod na objektu mohu napsat (teoreticky) kolik chci, tak máme ve standardní knihovně k dispozici další dvě varianty pro průchod všech prvků seznamu - přímo metodu listu list.forEach() a paralelní přístup přes streamy - list.parallelStream().forEach(). A s trochou snahy mohu napsat vlastní implementaci optimalizovanou pro mé potřeby. To je na jednu stranu super, ale jde to proti myšlence Javy jako vysokoúrovňového jazyka, který by se neměl zabývat podobnými detaily jako výběr konkrétní implementace.
Vraťme se však ke standardním metodám list.forEach() a list.parallelStream().forEach(). Zápis zkoumaného algoritmu s využitím těchto metod vypadá následujícím způsobem:
inputList.forEach((integer) -> {
outputList.add(++integer);
});
inputList.parallelStream().forEach((integer) -> {
outputList.add(++integer);
});
Nabízí se samozřejmě otázka, jaký je mezi těmito čtyřmi implementacemi rozdíl. Foreach jako standardní a univerzální cyklus je jasný a ani parallelStream() mi nedělá problém - výpočet se spustí paralelně, tedy ve více vláknech (a na více jádrech). To může značně urychlit výpočet za cenu většího vytížení procesoru, ovšem pouze pro paralelizovatelné úlohy, což zdaleka nejsou všechny. Bohužel nelze omezit počet paralelních procesů, což by se hodilo, protože nevhodným použitím parallelStream() lze snadno zahltit systém, příkladů je plný stackoverflow.
Ale co forEach() a stream().forEach? Na první pohled je na objektu Stream definováno mnohem více (funkcionálních) metod :). Ale teď vážně - podle dokuemntace je základní rozdíl v tom, že streamové forEach() je "terminal operation" a vstupním parametrem je "a non-interfering action to perform on the elements" zatímco listový foreach() má "action to perform on the elements" a je u něj garantované pořadí (podle iterátoru) provádění akcí. Klíčové je však "non-interfering action" související se změnou původních objektů, příslušnou diskusi ze stackowerflow shrnutou v Non-interference exact meaning in Java 8 streams se mi nechce překládat ani komentovat, myslím, že je jasná.
Každopádně pro zajímavost uvádím výsledky benchamrku pro všechny čtyři implementace.
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
ListPlusPlus.forEach | thrpt | 5 | 6,725 | ± 2,165 | ops/ms |
ListPlusPlus.parallelStream | thrpt | 5 | 3,880 | ± 0,202 | ops/ms |
ListPlusPlus.standarForEach | thrpt | 5 | 8,374 | ± 3,957 | ops/ms |
ListPlusPlus.streamForEach | thrpt | 5 | 8,567 | ± 5,949 | ops/ms |
ListPlusPlus.forEach | avgt | 5 | 0,121 | ± 0,059 | ms/op |
ListPlusPlus.parallelStream | avgt | 5 | 0,244 | ± 0,026 | ms/op |
ListPlusPlus.standarForEach | avgt | 5 | 0,109 | ± 0,018 | ms/op |
ListPlusPlus.streamForEach | avgt | 5 | 0,120 | ± 0,075 | ms/op |
ListPlusPlus.forEach | sample | 38527 | 0,130 | ± 0,001 | ms/op |
ListPlusPlus.parallelStream | sample | 19683 | 0,254 | ± 0,003 | ms/op |
ListPlusPlus.standarForEach | sample | 41471 | 0,120 | ± 0,003 | ms/op |
ListPlusPlus.streamForEach | sample | 39646 | 0,126 | ± 0,001 | ms/op |
ListPlusPlus.forEach | ss | 5 | 0,513 | ± 0,784 | ms/op |
ListPlusPlus.parallelStream | ss | 5 | 0,607 | ± 0,285 | ms/op |
ListPlusPlus.standarForEach | ss | 5 | 1,430 | ± 3,066 | ms/op |
ListPlusPlus.streamForEach | ss | 5 | 0,467 | ± 0,894 | ms/op |
V tomto konkrétním případě nevidím žádný přínos paralelního přístupu, zřejmě není tento problém paralelizovatelný a navíc byl test spuštěn v jednom threadu, ale ani pět threadů situaci nějak dramaticky nezměnilo.
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
ListPlusPlus.forEach | thrpt | 25 | 8,137 | ± 0,781 | ops/ms |
ListPlusPlus.parallelStream | thrpt | 25 | 3,784 | ± 0,127 | ops/ms |
ListPlusPlus.standarForEach | thrpt | 25 | 9,810 | ± 1,187 | ops/ms |
ListPlusPlus.streamForEach | thrpt | 25 | 9,236 | ± 0,298 | ops/ms |
ListPlusPlus.forEach | avgt | 25 | 0,115 | ± 0,011 | ms/op |
ListPlusPlus.parallelStream | avgt | 25 | 0,254 | ± 0,005 | ms/op |
ListPlusPlus.standarForEach | avgt | 25 | 0,093 | ± 0,005 | ms/op |
ListPlusPlus.streamForEach | avgt | 25 | 0,127 | ± 0,012 | ms/op |
ListPlusPlus.forEach | sample | 186409 | 0,134 | ± 0,001 | ms/op |
ListPlusPlus.parallelStream | sample | 95608 | 0,262 | ± 0,001 | ms/op |
ListPlusPlus.standarForEach | sample | 215093 | 0,116 | ± 0,001 | ms/op |
ListPlusPlus.streamForEach | sample | 235593 | 0,106 | ± 0,001 | ms/op |
ListPlusPlus.forEach | ss | 25 | 0,461 | ± 0,149 | ms/op |
ListPlusPlus.parallelStream | ss | 25 | 0,710 | ± 0,143 | ms/op |
ListPlusPlus.standarForEach | ss | 25 | 1,033 | ± 0,463 | ms/op |
ListPlusPlus.streamForEach | ss | 25 | 0,423 | ± 0,144 | ms/op |
Pro ověření přínosu paralelního přístupu s využitím parallelStream() zkusím někdy příště najít vhodnější úlohu - třeba násobení matic :).
- 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í 04 - Java Microbenchmark Harness
- 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