ParallelFX – optimizacija .NET koda za multy-core procesore I dio


Članak objavljen u itpro.ba

Uvod

Multy-core procesori su realnost, a proizvođači procesora danas ih ugrađuju čak i u mobilne telefone. S druge strane danas se multy-core procesori „guraju“ i u grafičke kartice, i druge uređaje koje posjeduju procesorsku jedinicu. Ovo je posljedica prestanka razvoja single-core procesora koji su dostigli svoj maksimum, i praktično sa ovom tehnologijom koja se rabi u proizvodnji procesora, nemoguće je napraviti procesore sa taktom iznad 3GHz. Ukoliko se ne može povećavati takt proizvođači su počeli sa proizvodnjom multy-core procesora, ili više jezgri procesora u jednom hardverskom dijelu, što je dalo dodatni vjetar u leđa razvoju procesora. Danas se kućni računari kupuju sa 4 ili 8 jezgri, serveri i do 128 jezgri.
Realno se pitanje postavlja: da li softver koji je razvijan nekoliko godina unazad odgovara takvom hardveru? Da li hardver na multy-core procesorima ima smisla vrtiti dosadašnja softverska rješenja? Moguće se upitati i to da li energija koju troši ovakav hardver odgovara korištenju softvera?
Ovo su samo neka pitanja koja su izašla na površinu pojavom ovakvog hardvera, a što će biti predmet ovog i nekoliko narednih dijelova.

Softver i multy-core procesori

Po suštini i konceptu programski jezici kojim razvijamo aplikacije su sekvencijalne prirode. To znači da se izvršavanje vrši sekvenca po sekvenca odnosno linija po linija, ili drukčije kazano odozgo prema dolje. Izvršavanje takvog koda vrši se samo na jednoj jezgri procesora pa se može reći da ni programski jezici niti aplikacije nisu prilagođene novonastaloj situaciji. Aplikacije koje su razvijane programskim jezicima nisu prilagođene novim hardverskim trendovima, što znači da nisu optimizirane za rad na novom hardveru. Takve se aplikacije vrte samo na jednoj jezgri dok ostale samo troše energiju.
Ako gledamo sa stanovišta .NET frameworka 3.5 i ranijih verzija, sve aplikacije koje se nisu posebno programirale za multy-core procesore, bez dodatnog korištenja APIa niskog nivoa, ne iskorištavaju sve resurse računara odnosno procesora. Svaka for ili do petlja izvršava se jednostrano i može iskorištavati resurse samo jednog procesora odnosno jedne niti (Thread). U slučaju kad se određena aplikacija vrti na mašini sa 2 ili 4 core procesora ona će zauzimati oko 50% ili 25% resursa procesora respektivno. Kao što je kazano, bez dodatnog programiranja i korištenja API-a za višenitno programiranje nismo u stanju da aplikaciju “natjeramo” da koristi sve raspoložive resurse našeg “multy core” PC-a.

Na osnovu prethodnog možmo se uvjeriti da nase stare aplikacije neće brže raditi kupovinom novog multy core PC. Jedino što smo postigli takvom kupovinom jeste multitasking,podizanje i rad s više aplikacija istovremeno, međutim aplikacije pojedinačno nisu ubrzane. Danas nove verzije popularnih aplikacija poput AutoCAD, SolidWorks, Adobe PhotoShop i druge podržavaju multy core procesore u ograničenim segmentima. Npr. AutoCAD prilikom renderiranja 3D CAD modela u fotorealističnu slik,u detektuje multy-core procesor te ravnomjerno rasporedjujući zadatke na sve jezgre iskorištava na najbolji način resurce a samim tim i ubrzava proces renderiranja. Renderiranje predstavlja najzahtjevniji dio aplikacije u kojoj se izvršavaju vrlo složeni proračuni definisanja svakog piksela slike iz 3D scene, te predstavlja jedan od načina prilagođavanja aplikacija multy-core hardveru.

Kako razvijati aplikacija za multy-core hardver

Ukoliko bi pokušali konvencionalnim tehnikama prilagođavati aplikacije multy-core procesorima, zaista bi predstavljalo vrlo složen proces. Svaki takav dio aplikacije trebao bi da odgovori na nekoliko pitanja i zahtjeva i to: dinamiči način formiranja, startanja i završavanje niti, manipulacija i dinamičko određivanje broja rocsora, upravljanje memorijom u više od jedne niti, data racing, dead locking i td. Zaista, razlog za adaptaciju aplikacije trebao bi biti vrlo važan da se krene u ovakav proces implementacije.
Kao odgovor na razvoj multy-core procesora Microsoft je na svjetlo dana izbacio nekoliko godina razvijanu tehnologiju ParallelFX – podršku za konkurentno programiranje pod .NET Framework-om, a koja je danas sastavni dio .NET Frameworka 4.0. ParallelFx je novo proširenje .NET Frameworka, koje doprinosi novom načinu konkurentnog programiranja, a posljedica je hardwerskog razvoja “multy core procesora” koji danas sve više zauzimaju primat u odnosu na dosadašnje tzv. “single core procesore”. S druge strane Parallel FX predstavlja model paralelnog izvršavanja zadataka, obrade podataka te koordiniranog iskorištanja hardwerskih resursa. Optimalno iskorištavanje nove generacije hardwera, aplikacije čini visoko učinkovitim sa velikim performansama koje tradicionalne programe poboljašavaju u svim segmentima.

Programirati aplikacije koje iskorištavaju nove mogućnosti hardwera pod .NET Frameworkom mogu se postići na više načina koje obezbjeđuje ParallelFX proširenje i to:

  • Deklarativni paralelizam obrade podataka – Parallel LINQ – izvršavanje upita paralelno, optimalnim iskorištavanjem hardverskih resursa računara. Obzirom da su LINQ upiti deklarativni i da se izvršavaju onda kad se počinje izvršavati prebrojavanje korištenjem foreach ili neke druge ključne riječi.
  • Imperativni paralelizam obrade podataka – čini mehanizam za izvršavanje osnovnih imperativnih podatkovno orjentisanih operacija korištenjem osnovnih petlji programskog jezika for, foreach, invoke.
  • Imperativni paralelizam izvršavanja zadataka – Prethodna dva načina prilagođavaju paralelno programiranje obradi podataka, imperativni paralelizam predstavlja formiranje zadataka koji se mogu izvršavati paralelno i na taj način iskorištavati resurse hardwera.

Jedna od najznačajnijih osobina koje odlikuju ovo proširenje je ta da se u vrijeme izvršavanja definiše paralelizam, što ovo proširenje čini maksimalno fleksibilnom i skalabilnom. Ovo pak znači da je proces paralelnog programiranja isti u svim slučajevima broja procesora, a da se u toku izvršavanja “run-time”, formira onoliko niti koliko postoji procesora, odnosno shodno trenutnom stanju zauzetosti procesora. U slučaju da se paralelni kod izvršava na single core procesoru, on je u potpunosti kompatibilan i izvršavat će se bez dodatnih modifikacija. Da li će kod prilagođen paralelnom načinu izvršavanja brže raditi na single core hardveru u odnosu na običan kod? Odgovor je ne, iz razloga dodatnog zauzimanja resursa tokom translacije i pripreme za paralelno izvršavanje.

Task Parallel Library TPL

Osnovu paralelnog razvoja aplikacija u .NET Framework čini TPL biblioteka za paralelno programiranje, u kojoj su implemetirani gore pobrojani načini paralelizma. Najjednostavniji primjer upotrebe TPL možemo prikazati pomoću for petlje.
Pretpostavimo da imamo for petlju koja izvršava odredjenu operaciju definisanu pomoću metode Metoda(int a). Neka ta ista metoda uzima argument a i procesuira argument pri čemu vrijeme procesuiranja metoda iznosi 0,001 dijelova sekunde, što u ovim konstalacijama predstavlja vrlo dug period procesuiranja.Da bi sumulirali takav rad koristićemo Sleep metodu od Thread klase. Isto toko formira ćemo objekat Stopwatch štopericu kojom ćemo mjeriti vrijeme izvršavanja Metode.

Klasični način implementacije možemo prikazati na sljedećem listingu:

using System;
using System.Threading;
using System.Diagnostics;
namespace ParallelFXDemo1_ItPro
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch st = new Stopwatch();
            st.Start();
            Console.WriteLine("Operacija započela!");
            //Broj operacija u petlji
            int n = 1000;

            for (int i = 0; i < n; i++)
                 Metoda(i);
             //Završetak programa
             Console.WriteLine("Operacija završila za: {0} sec.", st.Elapsed.TotalSeconds);
             //Press any key to continue...
             Console.ReadKey();
         }
         static void Metoda(int a)
         {
             Thread.Sleep(10);
         }
     }
 }
 

Ukoliko ovakav kod izvršimo trebaće nam oko 10 sekundi da se izvrši program što predstavlja poprilično vrijeme. Isto tako prethodni kod bez obzira koliko iznosio broj n (iteracija u for petlji) koristi resurse samo jednog procesora. To znači kad se aplikacija pokrene na Quad Core procesoru ona zauzima približno 25% procesora. Sada se pitamo na koji način navedeni primjer implementirati da podržava paralelizam i iskorištava sve hardwerske resurse multy core procesora. Prethodni primjer prevesti u kod koji podržava paralelizam nimalo nije složen te zahtjeva minimalnu izmjenu koda. TPL biblioteka sadrži standardne metode koje se izvršavaju paralelno. npr. for petlja u TPL sadrži statičku metodu Parallel.For Parallel klase koja kao argumente uzima početnu i krajnju iteraciju, te delegat koji sadrži implementaciju klasične for petlje. Npr. paralelna verzija prethodnog primjera izgleda kao na sljedećem listingu:

using System.Threading.Tasks;
namespace ParallelFXDemo1_ItPro
 {
     class Program
     {
         static void Main(string[] args)
         {
             Stopwatch st = new Stopwatch();
             st.Start();
             Console.WriteLine("Operacija započela!");
             //Broj operacija u petlji
             int n = 1000;
             Parallel.For(0, n, (i) =>
                {
                    Metoda(i);
                });

            //Završetak programa
            Console.WriteLine("Operacija završila za: {0} sec.", st.Elapsed.TotalSeconds);

            //Press any key to continue...
            Console.ReadKey();
        }

        static void Metoda(int a)
        {
            Thread.Sleep(10);
        }
    }
}

Na isti način ukoliko gornji program pokrenemo na Quad Core procesorskoj mašini (mašini sa 4 core procesora) vrijeme izvršavanja biće 4 puta manje, a ukoliko pogledamo zauzeće resursa računara vidimo da je procesorske jezgre bila sve 100% zauzete.
Parallel.For – je statička metoda klase Parallel koja posjeduje 10 tak preklopljenih verzija koje uzimaju različite argumente za različite načine implementacije paralelizma. O ovoj klasi nešto detaljnije kazat ćemo kasnije.
Sada se postavlja logično pitanje. Šta ako imamo dvije ili više for petlji? Da li trebamo paralelizirati vanjsku, unutrašnju ili obe for petlje.
Npr. Pretpostavimo da imamo sljedeću sekvencijalnu implementaciju koda, koja sadrži dvije for petlje. Definišimo je tako da broj iteracija vanjske i unutrašnje petlje 100 iteracija, a metoda koja se izvršava neka uzma 1 msec. Postavimo mjerač vremena na početku izvršavanja petlje i na kraju, te izmjerimo vrijeme izvršavanja.

using System.Threading.Tasks;
namespace ParallelFXDemo1_ItPro
{
    class Program
    {
        static void Main(string[] args)
        {
            //Broj operacija u petljama
            int n = 100;
            int m = 100;
            Stopwatch st = new Stopwatch();
            st.Start();
            Console.WriteLine("Operacija započela!");

            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < m; j++)
                {
                    Metoda(i*j);
                }
            }
            st.Stop();
            //Završetak programa
            Console.WriteLine("Operacija završila za: {0}", st.Elapsed.TotalSeconds);

            //Press any key to continue...
            Console.ReadKey();
        }

        static void Metoda(int a)
        {
            Thread.Sleep(1);
        }
    }
}

Slično kao i u prethodnom primjeru vrijeme za koje se izvrši ovaj primjer iznosi oko 10 sec, bez obzira što se radi o Quad Core procesoru, te zauzeće tokom izvršavanja bilo je oko 25%.
Ako koristeći ParallelFX implementiramo primjer imamo sljedeće:

using System.Threading.Tasks;
namespace ParallelFXDemo1_ItPro
{
    class Program
    {
        static void Main(string[] args)
        {
            //Broj operacija u petljama
            int n = 100;
            int m = 100;
            Stopwatch st = new Stopwatch();
            st.Start();
            Console.WriteLine("Operacija započela!");

            for (int i = 0; i < n; i++)
              {
                  Parallel.For(0, m, (j) =>
                {
                    Metoda(i * j);
                });
            }
                st.Stop();
            //Završetak programa
            Console.WriteLine("Operacija završila za: {0}", st.Elapsed.TotalSeconds);

            //Press any key to continue...
            Console.ReadKey();
        }

        static void Metoda(int a)
        {
            Thread.Sleep(1);
        }
    }
}

Iz ovog primjera vidimo da se naš kod u paralelnoj verziji ubrzao oko 4 puta koliko i izosi broj jezgri procesora.

Klasa Parallel

Prethodno smo govorili o osnovnim pojmovima ParallelFX odnosno TPL, te kao što smo kazali ranije pobliže ćemo obraditi klasu Parallel i nekoliko načina implementacije paralelizma. Pogledamo li iz ObjectBrowsera System.Threading vidjećemo da ovaj dll posjeduje 4 imenska prostora, u kojem je smješteno cjelokupno proširenje ParallelFX. Ako proširimo prostor Threading možemo vidjeti klasu Parallel o kojoj ovdje želimo nešto više reći.

Klasa Parallel sadrži samo 3 metode: Invoke, For i ForEach. Zadnje dvije metode imaju sličnu implementaciju a njihovo korištenje vidjeli smo u prethodnim primjerima. Međutim, u koliko želimo da imamo složeniji sistem paralelizma kao što je postavljanje uslova u paralelnim petljama na osnovu kojih želimo da prekinemo ili zaustavimo petlju, koristićemo neku od preklopljenih metoda For ili ForEach. S druge strane kasnije će se vidjeti i primjeri razmjene informacija između paralelnih petlji, a koje opet koriste neku od preklopljenih metoda For i ForEach klase Parallel, a koje će biti prikazane nešto kasnije.

Za razliku od For i ForEach, Invoke metoda ima drugačiju logiku. Invoke metoda predstavlja sljedeću metoda koja se nalazi u klasi Parallel. Ova metoda radi na različit način od prethodne dvije, a preko nje izvršavamo odredjene zadatke paralelno i neovisno jedne od drugih. Npr. ako imamo 4 metode koje ne zavise jedna od druge i želimo ih izvršiti paralelno koristi ćemo metodu Invoke na sljedeći način:

using System.Threading.Tasks;
using System.Threading;
namespace ParallelFXDemo1_ItPro
{
class Program
{
    static void Main(string[] args)
    {
        Parallel.Invoke(
            () => Metoda1(),
            () => Metoda2(),
            () => Metoda3(),
            () => Metoda4()
                        );
        //Press any key to continue...
        Console.ReadKey();
    }
    private static void Metoda1()
    {
      Thread.Sleep(1000);
      Console.WriteLine("Metoda 1 završena.");
    }
    private static void Metoda2()
    {
      Thread.Sleep(1000);
      Console.WriteLine("Metoda 2 završena.");
    }
    private static void Metoda3()
    {
      Thread.Sleep(1000);
      Console.WriteLine("Metoda 3 završena.");
    }
    private static void Metoda4()
    {
      Thread.Sleep(1000);
      Console.WriteLine("Metoda 4 završena.");
    }
}
}

Prethodni primjer sadrži 4 metode koje Invoke metoda izvršava paralelno. Možemo primjetiti da Invoke metoda koristi lambda izraze za pozive metoda. Ovom implementacijom sve 4 funkcije izvršavaju se paralelno, a zanimljivo je primjetiti da ukoliko se pokrenete ovaj primjer izlaz na konzolu uvijek nije isti, jer bez obzira što su metode po izvršavanju zauzimaju isto vremena od 1 sec, one se ne ispisuju na konzoli jednako svaki put, zbog samog stohastičkog izvršavanje i procesuiranja ParallelFX biblioteke.

Leave a comment