Jednostavan način da ubrzamo for i foreach petlje je koristeći paralelno programiranje odnosno Parallel.For i Parallel.ForEach. Tako će se pojedine iteracije petlje izvršavati paralelno u više threadova, a pri tome ne trebamo sami brinuti o threadovima. Koliko ubrzanje ćemo postići ovisi o C# kodu unutar tijela petlje. Korištenjem Parallel.For i Parallel.ForEach možemo i usporiti C# kod u slučaju kada u tijelu petlje imamo jednostavan kod koji se u pojedinoj iteraciji jako brzo izvodi. 

Čemu služe Parallel.For i Parallel.ForEach? 

Da bi na jednostavan način ubrzali for i foreach petlje u C# možemo upotrijebiti petlje koje automatski izvode naš kod paralelno. Kao što i njihova imena sugeriraju Parallel.For i Parallel.ForEach su verzije for i foreach petlji koje omogućuju da se sadržaj petlje pokreće u nekoliko paralelnih threadova. Pri tome jednostavno koristimo ove paralelne petlje bez da ručno brinemo o threadovima u kojima se kod izvodi. Time se zamjena običnih for i foreach petlji s njihovim paralelnim verzijama može napraviti brzo i jednostavno.  

Parallel.For  

Parallel.For ima cijeli niz preopterećenih (overloaded) verzija. Osnovni način korištenja je jednostavno navesti rubne vrijednost kontrolne varijable petlje i napisati blok koda koji će se izvoditi u petlji: 

Parallel.For for petlja

Drugi parametar predstavlja gornju granicu koja nije uključena u niz vrijednosti koje poprima kontrolne varijable petlje. Drugim riječima, zadnja vrijednost koju kontrolna varijabla loopVariable ima u petlji je toExclusive – 1 te u idućem koraku kada loopVariable == toExclusive ne ulazi se u tijelo petlje. 

Ako usporedimo ovu petlju s običnom for petljom vidimo da ispred toExclusive je znak manje, a ne znak manje-ili-jednako. 

Parallel.For obicna for petlja

Dodatne mogućnosti koje donose overloaded verzije metode Parallel.For iste su kao i u slučaju overloaded verzija metode Parallel.ForEach.

Parallel.ForEach 

Parallel.ForEach isto tako ima cijeli niz preopterećenih (overloaded) verzija. Osnovni način korištenja u ovom slučaju je navesti kolekciju elemenata, varijablu koja će biti aktualni element kolekcije u pojedinoj iteraciji petlje i napisati blok koda koji će se izvoditi u petlji: 

Parallel.ForEach for petlja

Parallel.ForEach, kao i Parallel.For, ima dodatne mogućnosti koje koristimo pomoću overloaded verzija Parallel.ForEach petlje. 

Dodatne mogućnosti Parallel.For i Parallel.ForEach 

Dobro je znati da postoje dodatne mogućnosti jer će se u praksi pojaviti situacije kada ćete trebati te mogućnosti. 

Overloaded verzije Parallel.For i Parallel.ForEach donose mogućnosti definiranja opcija izvođenja korištenjem objekta ParallelOptions klase. Radi se o definiranju tokena za prekid (CancellationToken), definiranje maksimalnog broja paralelnih instanci koje izvode tijelo petlje te definiranje schedulera koji se koristi pomoću objekta TaskScheduler klase. 

Uz to overloaded verzije nude mogućnost korištenja objekta ParallelLoopState klase za interakciju između pojedinih instanci paralelne petlje.  

Paralelne petlje kroz primjer C# koda 

Za primjer aplikacije u kojoj ćemo koristiti Parallel.For i Parallel.ForEach uzeti ću aplikaciju koja analizira objave na društvenim mrežama. Od svih analiza koje bi takva aplikacija radila za potrebe ovog blog posta fokusirati ću se na samo dvije analize: 

a) u koliko objava su korisnici stavili više puta isti hashtag

b) koliko objava su kratke objave s manje od 200 znakova

Ovdje je bitno da će se u jednom slučaju duže analizirati pojedina objava, a u drugom slučaju će se površno analizirati ta ista objava. U prvom slučaju ćemo pozivati metodu koja koristi uobičajeni izraz da pronađemo dva ili više ista hashtaga. To će trajati dosta duže od jednostavne usporedbe duljine posta s 200 u drugom slučaju. 

Za provjeru koliko je neka optimizacija utjecala na brzinu koda koristi se BenchmarkDotNet . I ovdje će se koristiti taj command-line alat koji će izmjeriti vremena izvođenja, obraditi rezultate mjerenja i prikazati ih tablično. 

Za uspoređivanje obične verzije petlje s paralelnom verzijom napisati ću dva benchmarka – jedan za običnu petlju, a drugi za paralelnu petlju. Svaki benchmark je C# metoda označena atributom [Benchmark]. Pri tome možemo i definirati koji benchmark je referentna točka prilikom usporedbe s drugim benchmarkom. Da bi neki od benchmarka označili kao referentnu točku koristimo parametar (Baseline = true) u [Benchmark] atributu. U rezultatima koje prikaže BenchmarkDotNet referentni benchmark, označen atributom [Benchmark(Baseline = true)], imati će u stupcu Ratio vrijednost 1.0, a drugi benchmark ako se izvede brže od referentnog benchmarka imati će vrijednost Ratio manju od 1.0. Cilj je dobiti Ratio manji od 1.0 jer je tada kod optimiziran. 

Obična for petlja – početni kod koji ćemo optimizirati 

Kreće se od obične for petlje koju će se zatim pretvoriti u paralelnu petlju to jest od benchmarka u kojemu mjerimo vrijeme izvođenja naše prve analize u kojoj određujemo koliko objava koristi više puta isti hashtag. Označiti ćemo ovaj benchmark kao referentni benchmark pomoću odgovarajućeg BenchmarkDotNet atributa [Benchmark(Baseline = true)] : 

Sljedeću metodu, koja određuje postoje li u nekom tekstu dva ili više ista hashtaga, koristiti ću i za Parallel.For i Parallel.ForEach primjere: 

Obicna for petlja za Parallel.For i Parallel.ForEach

Parallel.For – primjer ubrzanja koda 

Parallel.For petlja omogućuje da zamijenimo običnu for petlju iz prethodnog poglavlja i optimiziramo C# kod. 

Parallel.For ubrzanje koda 1

Pogledajmo rezultate ubrzanja koje smo izmjerili koristeći BenchmarkDotNet i to u slučaju različitih veličina kolekcije podataka – kolekcije objava kroz koju petlja prolazi. Sada, kao i u svim idućim primjerima, koristimo tri različite veličine kolekcija: 100, 1 000 i 1 000 000 elemenata. 

Parallel.For petlja je 4 do 5 puta brža od obične for petlje što vidimo iz mjerenje vremena izvođenja C# koda (Ratio 0.19 do 0.25): 

Parallel.For ubrzanje koda 2

Ubrzanje C# koda ovisi o broju elemenata u kolekciji kroz koju prolazimo s petljom. To je očekivano uzme li se u obzir da je u slučaju manjeg broja elemenata veći učinak overheada koji nastaje prilikom automatskog pokretanja koda u nekoliko paralelnih threadova 

Obična foreach petlja – početni kod koji ćemo optimizirati 

I u slučaju foreach petlje kreće se od obične petlje koju ćemo pretvoriti u paralelnu petlju. Zadatak koji odradi ovaj kod je isti kao i u primjeru for petlje – prebroji sve objave koji imaju dva ili više puta isti hashtag: 

Obicna foreach petlja

Parallel.ForEach – primjer ubrzanja koda 

Parallel.ForEach petlja nam omogućuje da zamijenimo običnu foreach petlju i napišemo sljedeći kod koji radi istu stvar, ali brže: 

Parallel.ForEach ubrzanje koda 1

Parallel.ForEach petlja je 4 do 5 puta brža od obične foreach petlje (isti rezultat kao i u slučaju Parallel.For petlje) što vidimo iz mjerenja vremena izvođenja C# koda (Ratio 0.20 – 0.24): 

Parallel.ForEach ubrzanje koda 2

Kada Parallel.For i Parallel.ForEach ne ubrzavaju kod? 

U idućem promjeru će se promijeniti tijelo petlje tako da se pojedini prolaz kroz tijelo petlje jako brzo izvodi. Parallel.For i Parallel.ForEach samo ćemo odrediti je li duljina objave manja od granične vrijednosti za malu objavu. 

Istu promjenu unutar tijela petlje napraviti ćemo i u slučaju običnih for i foreach petlji, a i u slučaju Parallel.For i Parallel.ForEach. Budući da se radi samo o promijeni unutar tijela petlje i to o istoj promjeni u svim slučajevima, ovdje ću napisati kod samo za paralelne petlje. Za mjerenje s BenchmarkDotNet koristim nove benchmark metode (i za obične i za paralelne petlje).  

Parallel.For je prva petlja koju ćemo testirati kako radi s tijelom petlje koje se jako kratko izvodi: 

Parallel.For ne-ubrzanje koda 1

Kada koristeći BenchmarkDotNet provjerimo vrijeme izvođenja vidimo da je u ovom slučaju Parallel.For verzija petlje sporija nego obična for petlja i to 26 do 45 puta ovisno o veličini kolekcije kroz koju petlja prolazi. 

Parallel.For ne-ubrzanje koda 2

Parallel.ForEach petlju ćemo isprobati u istoj situaciji. Ponovno ćemo napisati isto tijelo petlje koje samo provjerava je li duljina objave manja od granične vrijednosti za kratku objavu (SmallPostLength). 

Parallel.ForEach ne-ubrzanje koda 1

BenchmarkDotNet ponovno pokazuje da je došlo do usporenja koda. U slučaju Parallel.ForEach radi se o usporenju od 41 do 43 puta, što je slično usporenje kao u slučaju Parallel.For petlje za 100 elemenata: 

Parallel.ForEach ne-ubrzanje koda 1

Zašto je sada došlo do usporenja? U slučaju kada se pojedina iteracija tijela petlje jako brzo izvodi dodatni posao (overhead) koji se automatski odradi u slučaju paralelnih petlji troši više vremena nego što uštedimo vremena paralelnim izvođenjem. 

Parallel.For i Parallel.ForEach u praksi 

Parallel.For i Parallel.ForEach su dobar alat koji omogućuje da na jednostavan način optimiziramo for i foreach petlje u C#-u. Na taj jednostavan način možemo dobiti četiri do pet puta brži kod. Pri tome trebamo obratiti pažnju na C# kod koji se nalazi unutar tijela petlje. Ako se radi o kodu čija se pojedina iteracija u petlji jako brzo izvodi tada može doći i do usporenja koda prilikom korištenja Parallel.For i Parallel.ForEach. Razlog leži u overheadu koji se događa kod automatskog pokretanja petlje u paralelnim threadovima 

Koliko je ubrzanje to jest da nije došlo do usporenja najbolje je provjeriti tako da izmjerimo vrijeme izvođenja C# koda. Za to mjerenje koristim, a i preporučujem, BenchmarkDotNet.

Svaki put kada optimiziramo C# kod, ne samo u slučaju Parallel.For i Parallel.ForEach, treba izmjeriti vrijeme izvođenja prije i poslije (pokušaja) optimizacije. Može se dogoditi da ne postignemo željeno ubrzanje koda, pa čak i da usporimo program, kao što se vidi iz gornjeg primjera. 

Preporučio bih da se uzme u obzir da izuzetno mali broj iteracija petlje neće imati isto ubrzanje kao i veliki broj iteracija petlje, bez obzira kakav kod je unutar tijela petlje. 

What's your reaction?

Leave a comment