• Nem Talált Eredményt

A gyakorlatban két szélsőséges esettel és ezek átmeneteivel találkozhatunk:

• ritkább esetben a programból egyetlen sor kód sincsen kész, a tervezés a megfelelő algoritmus kiválasztásától/kifejlesztésétől az implementáció megkezdéséig a mi feladatunk;

• gyakrabban azonban rendelkezésünkre áll egy szekvenciálisan futó program, amelyet akár több 10 évre visszanyúló evolúciós fejlesztés előz meg. Feladatunk úgy módosítani a programot, hogy az a lehető legjobban kihasználja egy sokprocesszoros architektúra lehetőségeit, ugyanakkor a fejlesztés idő-hatékony legyen: nem szervezhetjük újra az egész programot.

Bármelyik eset is valósuljon meg, első és talán legfontosabb feladatunk, hogy teljes mélységében értsük meg a problémát: tisztában kell lennünk vele, hogy a problémához kapcsolódó feladatok közül mit és hogyan tudunk megoldani és kiszámolni; tudományos szoftverek esetén meg kell értenünk a matematikai hátteret; ipari szoftverek esetén azokat az ipari folyamatokat, amelyekhez a szoftver kapcsolódik és amelyek a párhuzamosíthatóságot korlátozhatják. Ha már rendelkezünk valamennyi programkóddal; fel kell térképeznünk annak működését; meg kell értenünk az egyes objektumok életciklusát; azt hogy miért azokat a megoldásokat alkalmazták a fejlesztők, amelyek jelenleg megtalálhatóak a kódban, stb.

Fontos szem előtt tartani, hogy nem minden probléma párhuzamosítható a jelenleg rendelkezésre álló hardver nyújtotta lehetőségekkel: a lineáris algebra módszerei (vektoriális-, skaláris-szorzás, mátrix szorzás, stb.) jellemzően jól párhuzamosíthatók, azonban a Fibonacci-sorozat elemeinek klasszikus, rekurzív előállítása már nem1 az, mert minden számítási lépés a megelőző lépések eredményeitől függ. Ne erőltessük a párhuzamos végrehajtást! Ha a probléma megoldására nem adható elegáns párhuzamos algoritmus, egy elbonyolított megoldással nagy valószínűséggel nem fogjuk elérni a kívánt eredményeket; ha a fejlesztési időt a szekvenciális megoldás alacsonyszintű optimalizálására szánjuk, hatékonyabb szoftvert kaphatunk.

Keressünk a szakirodalomban a probléma megoldására kidolgozott párhuzamos algoritmusokat és nézzünk utána, hogy maga az algoritmus, vagy annak komponensei nem érhetőek-e el valamely hardver gyártó párhuzamos, optimalizált, matematikai programkönyvtárában. Alacsony szintű párhuzamos matematikai módszereket tartalmaz például az Intel Math Kernel Library (MKL)2, AMD Core Math Library (ACML)3, AMD Accelerated Parallel Processing Math Libraries 4, NVidia CUDA Math Library5, ViennaCL6.

1Megjegyezzük azonban, hogy a Fibonacci-sorozat elemeinek kiszámítására létezik zárt formula - a Binet-formula - mellyel direkt módon

meghatározhatjuk a sorozat n. elemét: Számítógépen alkalmazva azonban a Binet-formula a pontosan nem ábrázolható érték és a hatványozás miatt nagy n esetén pontatlan eredményt adhat.

2http://software.intel.com/en-us/intel-mkl

3http://developer.amd.com/tools/cpu-development/amd-core-math-library-acml/

4http://developer.amd.com/tools/heterogeneous-computing/amd-accelerated-parallel-processing-math-libraries/

5https://developer.nvidia.com/cuda-math-library

6http://viennacl.sourceforge.net/

1.1. Forró pontok és szűk keresztmetszetek

Ha a probléma párhuzamosítható, azonosítsuk az un. forró pontokat, vagyis azokat a pontokat, ahol az algoritmus a legszámításigényesebb feladatokat végzi; ha rendelkezünk szekvenciális programkóddal, akkor azonosítsuk azokat a kódrészleteket, amelyekben a vezérlés a legtöbb processzoridőt tölti. A legtöbb probléma esetén a kódnak csak nagyon kicsiny részei felelősek a számítások legnagyobb részéért, így ezek párhuzamosításával közel akkora teljesítménynövekedést érhetünk el, mintha jóval hosszabb tervezést és fejlesztést követően a szoftver jelentős részét átszerveznénk úgy, hogy az a lehető legtöbb párhuzamos végrehajtást tartalmazza. A forró pontok végrehajtását leggyakrabban a korábban leírt adatpárhuzamosítással gyorsíthatjuk: próbáljuk a problémát diszkrét, független részproblémákra bontani és a párhuzamosan futó programegységek ezen diszkrét problémák megoldásán dolgozzanak.

Ha már rendelkezünk működő implementációval, azonosítsuk az un. szűk keresztmetszeteket, a kód azon részeit, ahol kis processzorterhelés mellett a vezérlés sok időt tölt el. Ezek a területek jellemzően lassú I/O műveletekhez kapcsolódnak. Gondoljuk át, hogy szükséges-e megvárni a lassú műveletek befejezését a program folytatásához? Ha nem, bontsuk részekre a feladatokat úgy, hogy a lassú műveletek lehetőleg egy, a többitől független egységet alkossanak, és a funkcionális párhuzamosítás szemléletével szervezzük a lassú, de nem számításigényes műveleteket párhuzamosan futó programegységekbe.

Létező szekvenciális kód esetén a szűk keresztmetszetek és a számításigényes forró pontok azonosítására un.

profiler szoftvereket használhatunk. Ezekről bővebben lesz szó a fejezet egy későbbi szakaszában.

1.2. Kommunikáció és szinkronizáció

A párhuzamosan futó programrészek közötti kommunikáció mennyisége és jellege a problémától függ. Sok esetben szinte egyáltalán nincs szükség kommunikációra (például grafikai alkalmazásoknál, ahol nagy mennyiségű egyenes és sík metszéspontjainak meghatározása a cél), más esetekben (például fizikai szimulációkban, ahol anyagi részecskék kölcsönhatásait és azok rendszerének időfejlődését modellezzük) viszonylag nagy mennyiségű kommunikáció és adat megosztás szükséges.

A programozónak fel kell mérnie, hogy adott architektúrán és technológiával mennyire költséges a kommunikáció és mekkora adatmennyiséget kell mozgatni. Bármilyen memóriamodellt is alkalmazunk, a legkisebb egységnyi kommunikáció is erőforrás- és időköltséggel rendelkezik. Osztott memóriamodell esetén gondoskodni kell róla, hogy ne történhessen egyszerre ugyanazon memóriaterület írása és olvasása. Elosztott memóriamodell esetén magasabb szintű, például hálózati vagy az operációs rendszer által támogatott kommunikációra van szükség a folyamatok között, s ehhez az adatok megfelelő ,,csomagolása'' szükséges (például TCP/IP kommunikáció esetén). Ha gyakran kommunikálunk kis mennyiségű adatot, sok időt veszíthetünk a kommunikáció adatmennyiségtől független lépései során (osztott memória esetén a kölcsönös kizárás megvalósításával; TCP/IP kommunikáció esetén az Ethernet-keret adatrészének minimális mérete 46B).

Ha azonban nagy mennyiségű adatot küldünk/osztunk meg és a kommunikációs csatorna lassú, akkor lényegében egy szűk keresztmetszetet valósítunk meg, s ezzel veszítünk időt. Fontos tehát megtalálni a kommunikálandó adatok mennyiségének azon optimumát, amely biztosítja, hogy a párhuzamosan futó egységek ne várakozzanak sokat, viszont ne is veszítsünk sokat a kommunikációval járó egyéb műveletek során. A kommunikáció szervezése során tehát a programozónak kell kompromisszumra jutnia.

Elosztott memória modell esetén lehetőségünk van blokkolt (szinkron) és nem-blokkolt (aszinkron) kommunikációra. Előbbi esetében egy un. kézfogási művelet végrehajtása során a kommunikációs ponton a párhuzamosan futó egységek futása blokkolódik, egészen addig, amíg a kommunikáció be nem fejeződik. Nem-blokkolt esetben csak jelezzük, hogy bizonyos mennyiségű és típusú adatot szeretnénk küldeni vagy fogadni. A tényleges adatmozgatás csak később történik meg, a programegységek a kommunikáció igényének jelzése után folytathatják futásukat. A nem-blokkolt kommunikáció hatékonyabb lehet, azonban a programozónak gondoskodnia kell róla, hogy a küldő oldal ne módosítsa, a fogadó oldalon pedig ne használja fel az adatokat egészen addig, amíg az adatküldés biztosan be nem fejeződött.

Fontos, hogy pontosan tisztázzuk a kommunikáció jellegét: egy-egy folyamat kommunikál egymással vagy egy folyamat kommunikál a több másik folyamattal (kollektív kommunikáció). Utóbbi esetben több technológia (például az MPI) is biztosít speciális, optimalizált függvényeket a művelet végrehajtására, így érdemes lehet akár különböző folyamatpárok egymást követő kommunikációit is kollektív kommunikációvá szervezni, ha a probléma lehetővé teszi azt. A kollektív kommunikációs műveleteket irányuktól és a kommunikált üzenet különbözőségétől függően négy csoportba sorolhatjuk:

broadcast - egy folyamat küldi ugyanazt az adatot több másik folyamatnak,

scatter - egy folyamat küld különböző (de többnyire megegyező mennyiségű) adatot több másik folyamatnak,

gather - egy folyamat különböző helyre gyűjt össze adatokat a többi folyamattól,

reduction - egy folyamat gyűjti össze az adatokat a többi folyamattól, de azonos memóriaterületre, miközben az adatok között valamilyen matematikai műveletet végez (összeadás, szorzás, minimum, maximum meghatározása, stb.).

Az angol terminológiával megnevezett kommunikációs műveletekre az azokat támogató technológiák is így hivatkoznak, ezért a kézenfekvő redukció-reduction kivételével mi is az angol kifejezéseket használjuk a továbbiakban.

Szinkronizációról beszélünk, ha a párhuzamosan futó programegységek munkáját szeretnénk összehangolni, így a szinkronizáció is tekinthető egyfajta kommunikációnak. A programozónak fel kell mérnie, hogy a párhuzamosan végzett feladatoknak vannak-e egymástól függő részei, meg kell-e várnia valamelyik szálnak/folyamatnak egy másik munkájának eredményét? Fel kell tárni az erőforrásokért folytatott versenyhelyzeteket: a fizikai és absztrakt I/O eszközök zöme, mint például a merevlemez és a rá épülő fájlrendszerek, architektúrájukból kifolyólag nem támogatják a párhuzamosságot7 (hiszen a hardverben csak egy író/olvasó fej található); hasonló versenyhelyzet alakul ki akkor is, ha a párhuzamosan futó egységek azonos memóriaterületre szeretnének írni. A versenyhelyzetek kezelésére kölcsönös kizárást kell megvalósítani, azaz azonosítani kell az egymással versengő kódrészleteket és biztosítani, hogy a párhuzamosan futó programegységek közül egyszerre csak egy tartózkodjon a versenyhelyzethez tartozó kritikus szekcióban. Ha a kritikus szekciók nagyok, a szálak/folyamatok sokat várakozhatnak, amivel sok időt veszíthetünk. Ha a probléma lehetővé teszi, célszerű a nagy kritikus szekciókat a kód átszervezésével kisebb, de egymástól független kritikus szekciókra bontani.

1.3. Terheléselosztás és finomság

Adatpárhuzamosítás esetén könnyen kialakulhat az a helyzet, hogy nem érjük el a kívánt vagy várt gyorsulást, mert valamelyik szál vagy folyamat lassabban végzi el a munkáját. Ennek több oka is lehet:

• a végrehajtandó feladatokat (vagy a feldolgozandó adathalmazt) látszólag azonos méretű partíciókra bontottuk, azonban ezek számításigénye különböző. Gondoljunk például arra, hogy a [2, ..., N] tartomány minden eleméről el kell döntenünk, hogy prímszám-e, és a rendelkezésre álló 2 processzor rendre a [2, ..., N/2] és ]N/2, ..., N] tartomány egészeit dolgozza fel. A nagyobb számokat feldolgozó processzor munkája nagyságrendekkel több, mint a kisebb számokat feldolgozó processzoré, így hiába a párhuzamosítás, a futási időt az ]N/2, ..., N] tartomány feldolgozása fogja meghatározni, ami közel azonos lesz a teljes, [2, ..., N]

tartomány feldolgozásával.

• ha különböző számítógépek klaszterén dolgozunk, egyik-másik processzor lassabban dolgozhat, mint más processzorok, így az azonos számításigényű feladatokat is különböző idő alatt oldják meg. Egyetlen többmagos számítógépen sem tudjuk felhasználni az összes processzormagot számításokra, mert sok egyéb feladat mellett az operációs rendszert is futtatnia kell némely magoknak, így egyes processzorok ,,lemaradhatnak'', annak ellenére, hogy az architektúrájuk és az elvégzendő feladatok számításigénye is teljesen megegyezik.

A fentiek miatt kialakult egyenetlen terhelést az elvégzendő számítási feladatok megfelelő ütemezésével oldhatjuk meg: a feldolgozandó adatokat ne a processzorok számának megfelelő részre bontsuk, hanem jóval kisebb egységekre. Ekkor a processzorok egyszerre csak egy kis részét dolgozzák fel az adatoknak, és csak akkor kapnak újabb feladatot, ha az aktuális feladatot befejezték. Ezzel a megközelítéssel könnyen redukálhatjuk az egyenetlen terhelésből származó lassúságot. A feladatok kiosztása azonban egy szinkronizációs és kommunikációs művelet, amely szintén idő- és erőforrásigényes. Az egységnyi idő alatt elvégzett kommunikációs és szinkronizációs műveletek idejének és a problémát megoldó számítások idejének hányadosát az adatpárhuzamosítás finomságának nevezzük. Ha a finomság kicsi, akkor kevés kommunikáció történik sok számítás mellett, ami egyenetlen terheléselosztáshoz vezethet. Ha a finomság nagy, akkor sok kommunikáció és kevés számítás történik egységnyi idő alatt: nem alakul ki egyenetlen terhelés, viszont sok időt veszítünk a

7Meg kell említeni ugyanakkor, hogy bizonyos architektúrákon kialakíthatóak párhuzamos fájlrendszerek, például Linux számítógépek klaszterén a Lustre (http://wiki.lustre.org/index.php/Main_Page) és a Parallel Virtual File System (http://www.pvfs.org/).

kommunikációs műveletek végrehajtásával. Akárcsak a kommunikáció esetén, itt is kompromisszumra kell jutnia a programozónak és megtalálni a részproblémáknak azon méretét, amellyel nem alakul ki egyenetlen terhelés, de a feladatok kiosztása sem válik túl költségessé.