Moniydinsuorittimien rinnakkaisohjelmointi Turun yliopisto Informaatioteknologian laitos Tietojenkäsittelytiede Pro gradu -tutkielma Jari-Matti Mäkelä Maaliskuu 2011
TURUN YLIOPISTO Informaatioteknologian laitos / Matemaattis-luonnontieteellinen tiedekunta MÄKELÄ, JARI-MATTI: Moniydinsuorittimien rinnakkaisohjelmointi Pro gradu -tutkielma, 115 s. Tietojenkäsittelytiede Maaliskuu 2011 Tietokoneiden nopeuskehitys on aina viime vuosiin asti ollut eksponentiaalista ja perustunut peräkkäismuotoisen käskyvirran suorituksen nopeuttamiseen kellotaajuutta nostamalla ja yksittäisen käskyn suoritusaikaa kutistamalla (käskytason rinnakkaisuus, ILP). Kehitys on seurausta valmistustekniikan paranemisesta sekä ns. Mooren laista, jonka mukaan piirille integroitujen transistorien määrä kaksinkertaistuu säännöllisin välein. Nopeuden kasvu näin on kuitenkin lähes pysähtynyt, koska ILP ei ole parantunut enää ratkaisevasti, transistorien lukumäärän kasvaessa tehonkulutus on karannut hallitsemattomaksi ja suorittimen toiminnallisten yksiköiden välisen viestinvälityksen viiveet ovat muuttuneet erityisen ongelmallisiksi. Peräkkäismuotoisen ohjelmoinnin seuraajaksi on nyt toistamiseen kaavailtu rinnakkaisarkkitehtuureja, jotka perustuvat tehon lisäämiseen suorittimien määrää kasvattamalla. Aiemmin rinnakkaisohjelmointi nähtiin lähinnä vaihtoehdoksi monen suorittimen ja tietokoneen laskentaryppäissä, joissa yksittäinen suoritin toimi lähes täysin peräkkäisesti tai paljon moniydinsuorittimia suppeammin keinoin, esimerkiksi vektorikäskyin. Koska Mooren laki pätee yhä transistorien lukumäärän kasvun osalta, uutena ideana on integroitu useita, tehonkulutuksen laskemiseksi mahdollisesti aiempaa hitaampia, suoritinytimiä yksittäisen suorittimen sisään. Työssä on otettu tarkasteluun neljä merkittävää moniydinarkkitehtuuria (Intel x86, Cell BE, Nvidia Fermi ja XMT) ja pyritty identifioimaan ja analysoimaan arkkitehtuurin tasolla merkittävimmät parametrit ja ominaisuudet rinnakkaisuuden kannalta. Tarkastelu on tämän jälkeen laajennettu laskenta- ja ohjelmointimalleihin, joiden rooli on määrittää tehokas, helppokäyttöinen, virheitä estävä ja yleinen tapa kuvata rinnakkaisalgoritmeja. Alustoista on otettu esimerkeiksi OpenMP, MPI, tapahtumaperustainen muisti sekä lyhyesti joukko muita relevantteja ja tunnettuja teknologioita. Malleista on vastaavasti poimittu yleisiä analysoitavia piirteitä, minkä jälkeen niiden oikeellisuutta, monikäyttöisyyttä ja suorituskykyä on arvioitu valittujen arkkitehtuurien käyttöönotossa. Asiasanat: moniydinsuoritin, rinnakkaisohjelmointi, rinnakkaisarkkitehtuuri
UNIVERSITY OF TURKU Department of Information Technology MÄKELÄ, JARI-MATTI: Parallel programming on multi-core architectures Master s thesis, 115 p. Computer science March 2011 Until to date, the performance improvements in computers have followed an exponential trend. The main accelerators of this phase have been ever increasing operating frequencies and shortening of instruction execution time via instruction level parallelism (ILP), which have been made possible by the advancements in chip processing technology and the so-called Moore s law. The law states that the number of transistors doubles periodically. Unfortunately this pace has almost grinded to a halt, due to the diminishing returns of ILP, an uncontrollable increase in heat production, and the growing latencies between on-chip operational units. Parallel architectures have been proposed as a successor to this sequental computing. Parallel programming enables higher throughput via utilizing more computational cores. Previously this opportunity was seen as a realistic option only in clusters and other high performance computing, where each CPU operated sequentially or supported only a limited form of parallelism, e.g. vector processing. Since Moore s law still applies, we can now cram several, possibly slower low-power processing cores on a single chip multiprocessor (CMP). In this thesis four major multicore architectures (Intel x86, Cell BE, Nvidia Fermi, and XMT) are examined. First, we identify a set of parameters and properties for analysing the parallelism in various designs. After the architectural analysis we continue with a similar set of characteristic features for computational models of parallel programming. The task of these models is to provide an efficient, productive, easy to use, safe, and general way of expressing parallel algorithms. OpenMP, MPI, software transactional memory, and a set of other contemporary technologies are analysed both as computational and programming models and as an enabling technology for the physical architectures analysed earlier in the thesis. Special emphasis is laid upon the analysis of correctness, versatility, and performance of these hardware/software combinations. Keywords: multicore processor, parallel programming, parallel architecture
Sisältö 1 Johdanto 1 1.1 Laskennan mallit............................ 1 1.2 Von Neumann -arkkitehtuuri...................... 1 1.3 Rinnakkaisarkkitehtuurit........................ 6 2 Moniydinarkkitehtuurien käsitteitä 10 2.1 Arkkitehtuurin perusosat........................ 10 2.1.1 Laskentayksikkö......................... 11 2.1.2 Muistiyksikkö.......................... 11 2.1.3 Verkko.............................. 16 2.2 Termejä................................. 18 2.3 Rinnakkaisuuden mallintaminen.................... 20 2.3.1 Abstraktiotasot......................... 20 2.3.2 Flynnin taksonomia....................... 25 2.3.3 Säikeiden hallinta laitteistossa................. 26 3 Moniydinarkkitehtuureja 29 3.1 Valintaperusteet............................. 29 3.2 Intel x86................................. 31 3.2.1 Kommunikaatioverkko..................... 32 3.2.2 Muistihierarkia......................... 33 3.2.3 Laskentayksiköt......................... 36 3.2.4 MMX/SSE-käskykanta..................... 39 3.3 Cell Broadband Engine......................... 39 3.3.1 Kommunikaatioverkko..................... 40 3.3.2 Muistihierarkia......................... 42 3.3.3 Laskentayksiköt......................... 43 3.3.4 Muita käyttökohteita...................... 46 3.4 NVIDIA Fermi............................. 46
3.4.1 Kommunikaatioverkko..................... 47 3.4.2 Laskentayksiköt......................... 48 3.4.3 Muistihierarkia......................... 51 3.5 PRAM-arkkitehtuurit.......................... 53 3.5.1 ECLIPSE............................ 54 3.5.2 MOTH.............................. 55 3.6 XMT................................... 57 3.6.1 Kommunikaatioverkko..................... 58 3.6.2 Muistihierarkia......................... 60 3.6.3 Laskentayksiköt......................... 61 3.7 Yhteenveto............................... 62 3.7.1 Laskentateho.......................... 62 3.7.2 Muisti.............................. 65 3.7.3 Kommunikointi......................... 67 3.7.4 Tehokas käyttö......................... 69 4 Laskenta- ja ohjelmointimallit 71 4.1 Karakterisoivia piirteitä........................ 73 4.1.1 Työn esittäminen........................ 73 4.1.2 Työn ratkaisumenetelmä.................... 74 4.1.3 Kielelliset abstraktiot...................... 75 4.1.4 Esitysmuodon aiheuttamat ongelmatilanteet......... 76 4.2 Jaettu muisti.............................. 77 4.2.1 PRAM.............................. 77 4.2.2 Tapahtumaperustainen muisti................. 78 4.2.3 OpenMP............................. 80 4.3 Viestinvälitys.............................. 84 4.3.1 MPI............................... 84 4.4 Katsaus muihin malleihin....................... 89 4.4.1 Tietovirtaohjelmointi...................... 89 4.4.2 Implisiittinen rinnakkaisuus.................. 90 4.4.3 Deklaratiivinen ohjelmointi.................. 91
5 Moniydinarkkitehtuurien ohjelmointi 92 5.1 Oikeellisuus............................... 92 5.2 Monikäyttöisyys............................. 94 5.3 Suorituskyky.............................. 96 6 Johtopäätökset 99 6.1 Arkkitehtuuri.............................. 99 6.2 Laskenta- ja ohjelmointimallit..................... 100 6.3 Arkkitehtuurien ohjelmointi...................... 102 6.4 Tutkimuksen jatkaminen........................ 103 Viitteet 105
Kuvat 1 Von Neumann -arkkitehtuurimalli................... 2 2 Suorittimen ja muistin suhteellinen nopeuskehitys 1980 2000.... 3 3 Suorittimien keskeisten tehosuureiden kehitys 1970 2010....... 5 4 Grafiikka- ja x86-suorittimien nopeuskehitys 2001 2006....... 6 5 Erilaisia laitteiston arkkitehtuuriratkaisuja............... 10 6 Eri tapoja täyttää superskalaari liukuhihna.............. 27 7 Symmetric multiprocessing (SMP) -tyylinen väylä.......... 32 8 QuickPath Interconnect -verkon abstrakti rakenne.......... 33 9 Core i7:n muistimalli ja verkko..................... 35 10 Core i7 -arkkitehtuuri.......................... 37 11 Cell-arkkitehtuuri............................ 41 12 Cell BE:n muistimalli ja verkko.................... 44 13 Fermi-grafiikkasuorittimen rakenne.................. 48 14 Tietovirtasuorittimen rakenne..................... 49 15 Liikkuvien säikeiden arkkitehtuurin hahmotelma........... 56 16 XMT:n lohkokaavio........................... 58 17 Rypäsyksikön lohkokaavio....................... 60 18 Fork/join-ohjelmointimalli....................... 81 19 Joukkomuotoisen kommunikoinnin tekniikat MPI:ssä........ 87
Taulukot 1 Bittitason rinnakkaisuus kertolaskussa................ 21 2 Flynnin taksonomia........................... 25 3 Arkkitehtuurien teoreettinen laskentateho............... 63 4 Arkkitehtuurien teoreettinen muistikaista ja viiveet.......... 65
1 Johdanto 1.1 Laskennan mallit Tietojenkäsittelyssä laskennallisen ongelman algoritmista mallintamista ja analysointia varten voidaan poimia kolme keskeistä abstraktiota, jotka ovat tietokoneen arkkitehtuuri-, ohjelmointi- ja laskentamalli. Nämä kolme mallia karakterisoivat tarkasti koneen luoman toimintaympäristön, jossa algoritmi suoritetaan. Mallit määrittävät sen, mikä algoritmi soveltuu ongelmanratkaisuun parhaiten. Mallien avulla voidaan määrittää algoritmin perusominaisuuksia, esimerkiksi sen ajan- ja tilankäyttö, mutta niitä voidaan myös käyttää hyödyksi esimerkiksi algoritmin oikeaksi todistamisessa. [MMT95]. Laskenta- ja ohjelmointimalli ovat abstrakteja korkean tason malleja. Laskentamalli kuvaa abstraktin laskentalaitteen, jolla algoritmi voidaan suorittaa. Se määrittää rajapinnan datan käsittelyn semantiikalle algoritmin peräkkäisissä ja samanaikaisuutta vaativissa osissa. Ohjelmointimalli määrittelee algoritmin esittämistä varten laskentamallia noudattavan ja mahdollisesti laskentamallin ja laitteiston rajapintoja seuraavan joukon ohjelmointikielen rakenteita ja käsitteitä. [MMT95]. Arkkitehtuurimalli on matalan tason malli, joka määrittää yksityiskohtaisesti menetelmät ohjelmointikielellä laaditun ohjelman kääntämiseksi (tehokkaasti) konekielelle sekä laskutoimitusten kustannukset käytetyllä laitteistolla. Algoritmin rakentaminen laskenta- ja ohjelmointimallia vasten antaa alustavan kuvan sen algoritmisista ominaisuuksista sekä asymptoottisesta suorituskyvystä, mutta vasta arkkitehtuurimalli mukaan huomioituna nämä ominaisuudet voidaan laskea tarkasti. [MMT95]. Vastaavasti pelkkää arkkitehtuurimallia käyttäen on usein vaikea todentaa erilaisia algoritmisia ominaisuuksia. 1.2 Von Neumann -arkkitehtuuri Historiallisesti merkittävä ja nykyisten tietokoneiden rakennetta merkittävästi motivoinut tietokonearkkitehtuuri oli ns. von Neumann -tyyppinen hajasaantikone, 1
joka on käytännöllisesti katsoen Turing-täydellinen 1 laskentamalli. Hajasaantikone muodostuu ohjelman ja datan kesken jaetusta keskusmuistista sekä ohjaus- ja aritmeettis-loogisten yksiköiden muodostamasta suorittimesta (kuva 1). Sen käskykanta kattaa suorituksen ohjauksen, laskutoimitukset sekä keskusmuistin lukemisen ja kirjoittamisen vapaassa järjestyksessä. Ohjelman suoritus etenee peräkkäismuotoisena siten, että aina edellisen käskyn jälkeinen lopputila toimii alkutilana seuraavalle. [Neu45]. Control Unit Arithmetic Logic Unit Input/Output Memory Kuva 1: Von Neumann -arkkitehtuurimalli [Neu45]. 1980-luvun taitteessa muisti- (luku 2.1.2) ja laskentayksiköt (luku 2.1.1) kyettiin valmistamaan yhtä nopeiksi, mutta laskentayksiköiden nopeus on kasvanut vuosittain vuodesta 1987 asti noin 55% ja DRAM-tyyppisen [HPGA03] keskusmuistin vain noin 7% (kuva 2). Tämä epätasapaino on kannustanut toteuttamaan ratkaisuja muistin viipeen kompensoimiseksi, jotta laskennan suorituksen ei tarvitsisi pysähtyä aina muistioperaatioiden ajaksi. Lähes kaikki nykyaikaiset arkkitehtuurit hyödyntävät näitä tekniikoita. [HPGA03], eivätkä siis ole enää olekaan puhtaasti von Neumann -mallin mukaisia. Niitä on siitä huolimatta kritisoitu mm. tästä von Neumann -mallille tyypillisestä muistikaistan rajoittavasta vaikutuksesta suoritinten laskentanopeuden kasvaessa. Käytännössä suorittimiin on aina integroitu hyvin nopeaa paikallista muistia (rekisterit ja vastaavat loogiset komponentit), joka on tarkoitettu lähinnä laskennan 1 Turing-täydellisyys edellyttäisi ääretöntä muistiresurssia, mikä on käytännössä mahdottomuus. 2
Kuva 2: Suorittimen ja muistin suhteellinen nopeuskehitys 1980 2000 [HPGA03]. välituloksia varten. Tämä usein SRAM-tyyppinen [HPGA03] muisti on kertaluokkaa fyysisesti kookkaampaa ja kalliimpaa kuin keskusmuisti, mikä rajoittaa sen käyttöä merkittävästi. Suuren nopeuden vuoksi sitä käytetään myös ns. muistihierarkioissa välimuistina; käytännön arkkitehtuureissa muistihierarkia koostuu 1 4 eri tasosta, jotka eroavat toisistaan kapasiteetin ja viipeen osalta siten, että muistiyksikön viive on usein lähes kääntäen verrannollinen sen kapasiteettiin nähden. [HPGA03]. Välimuistin tehtävä on tarjota suorittimelle keskusmuistin muistiavaruus niin, että muistioperaatiot suoritetaan välimuistissa, mikäli data on saatavilla niissä paikallisesti. Toiminta perustuu muistin käytön säännöllisyyteen; heuristiikkana on esimerkiksi osoitteiden paikallinen ja ajallinen läheisyys; lähekkäiset osoitteet tallennetaan välimuistiin riveiksi kutsutuissa lohkoissa, kun taas laitteiston käyttämä rivien korvaamispolitiikka määrittää ajallisen läheisyyden. [HPGA03]. Välimuisti aiheuttaa datan kopioitumista useaan paikkaan, ja tätä myötä se edellyttää datan synkronointia keskusmuistin kanssa, mikäli samaa dataa käyttää jokin ulkopuolinen taho muistihierarkian ohitse. Synkronoinnin viive pyritään kätkemään suorittimelta niin, että kokonaisuutena datan nopeampi saatavuus laskee muistin käytön keskimääräistä viivettä. [HPGA03]. Välimuistin ensimmäiset tasot 3
on nykyään tyypillisesti integroitu suoritinpiirille tehokkuussyistä. Tämä tarkoittaa, että välimuistit kilpailevat samoista teho- ja transistoriresursseista laskentayksiköiden kanssa. [Pol99]. Käytännössä monien laskettavien ongelmien kullakin lyhyellä aikavälillä käyttämä työmuisti (working set [Tan07]) on verrattain pieni, joten kapasiteetin lisääminen muistihierarkian välimuisteihin tuo tietyn pisteen jälkeen vain pieniä nopeusparannuksia. Suuremman muistimäärän käyttö saattaa pakottaa käyttämään myös hitaampaa muistityyppiä transistorien säästämiseksi, joten nykyisin käytettyjen välimuistimäärien voi nähdä olevan mm. näiden parametrien välisen optimoinnin tulos. Muistioperaatioiden ja laskennan huomattavan nopeuseron lisäksi toinen von Neumann -vaikutteisten arkkitehtuurien rajoite näkyy peräkkäisten laskentaoperaatioiden nopeuskehityksessä valmistustekniikan kehittyessä; viimeisen puolen vuosikymmenen ajan tämä nopeus ei ole kasvanut enää merkittävästi. Syitä tähän ovat kellotaajuuden kasvattamiseen liittyvät fyysiset sekä suorittimen loogisessa rakenteessa ns. käskytason rinnakkaisuuden (luku 2.3.1) ongelmat. [HPGA03, Pol99]. Kellotaajuuden kasvattamisen ongelma liittyy puolijohdetekniikan tehonkulutukseen. Transistorin tehonkulutus (P ) määräytyy kaavasta: P = α C V 2 f missä α kuvaa komponentin aktiivisuutta (kuinka suuren osan ajasta virta on kytkettynä), C kapasitanssia, V jännitettä ja f kellotaajuutta [HPGA03]. Kun yhtäältä ns. Mooren laki [Gor65] 2 on mahdollistanut piirille integroitujen transistorien määrän tuplaamisen aina noin 18 kuukauden välein ja toisaalta laskentatehoa on tuotu lisää kellotaajuutta nostamalla, tehonkulutus on kasvanut valtavasti. [Pol99]. Ongelmia eivät ole pelkästään sähkönkulutuksen aiheuttamat käyttökustannukset suurteholaskennassa sekä vaikutukset esim. kannettavissa laitteissa akun käyttö- 2 Tutkielmassa Mooren lailla viitataan suurpiirteiseen nykynäkemykseen. Sekä intervalli (12-24 kk) että kasvun merkitys (transistorien määrä, laskentatehon määrä, laskentatehon määrä samalla ostohinnalla) vaihtelevat tulkinnan mukaan. 4
aikaan, vaan suoraviivainen lämpöenergian siirto pois mikropiiriltä ja sen läheisyydestä on muuttunut haasteelliseksi. [Pol99]. Ilmiötä havainnollistaa valtavirtakielien käyttäjille suunnatun H. Sutterin artikkelin [Sut05] yhteydessä esitetty kuva 3. Tehonkulutuksen hallinta onkin nykyisin relevantti ongelma joka sovellusalueella: keskuskoneissa, palvelimissa, työpöytäkoneissa että mobiililaitteissa. Kuva 3: Suorittimien keskeisten tehosuureiden kehitys 1970 2010. [Sut05]. Esitetty tehonkulutusyhtälö antaa kuitenkin myös toivoa. Paremman valmistustekniikan ansiosta tehonkulutusta saadaan laskettua sekä käyttöjännitettä että virran kytkennän muita kustannuksia laskemalla. Jännite ja kellotaajuus ovat kyt- 5
köksissä niin, että taajuuden pienentyessä myös toimintajännitteen stabiili alue laajenee alaspäin. [HPGA03]. Tämä tarkoittaa käytännössä sitä, että tehonkulutuksen pysyessä vakiona on laskentatehoa mahdollista kasvattaa lisäämällä transistoreja enemmän kuin mitä kellotaajuutta tarvitsee laskea. Tämä toimii myös johdatuksena seuraavaksi esitettävään ideaan. 1.3 Rinnakkaisarkkitehtuurit Muistin ja suorittimen suorituskyvyn kasvun taantumisen seurauksena alettiin etsiä vaihtoehtoisia suoritusmalleja. Luonnollinen tapa jatkaa kehitystä oli monistaa mainittuja yksiköitä, jonka seurauksena laskentaa voitiin tehdä rinnakkain. [Sut05, DvdS96]. Yksiköiden monistaminen on toiminut yllättävän hyvin grafiikkasuoritinteknologiassa; näiden laskentakyky on viime vuosina kasvanut huomattavasti tietokoneiden keskussuorittimia nopeammin (kuva 4). Kuva 4: Grafiikka- ja x86-suorittimien nopeuskehitys 2001 2006 [OLG + 05]. Rinnakkaislaskennalla on jo pitkä, 1950-luvulta [BBH + 81] alkanut historia ns. supertietokoneiden yhteydessä, mutta tutkielman kannalta olennainen kehitysaskel otettiin, kun kehittyneemmän valmistusprosessin ansiosta syntyi mahdollisuus koota aiempi erillisistä yksiköistä ( ytimistä ) koostuva järjestelmä yksittäisen mikropiirin sisään (chip multiprocessor, CMP). Nykyään nämä moniydinarkkitehtuurit 6
muodostavat yhden seuratuimmista laitteiston kehityssuunnista. Moniydintekniikka on viime aikoina yleistynyt räjähdysmäisesti eri käyttökohteissa, mistä syystä moniytimisiä suorittimia käytetään jo älypuhelinten [vb09] kokoisista laitteista alkaen erilaisessa elektroniikassa. Rinnakkaislaskenta tuo kuitenkin haasteita sekä laitteiston että algoritmien suunnitteluun. Arkkitehtuurin käyttämät komponentit voidaan valita ja organisoida eri tavoin mikropiirille. Molemmat seikat vaativat huomiointia laskentakuorman tasaamisessa sekä komponenttien välisen kommunikoinnin reitittämisessä. Luku 2 käsittelee lähemmin ongelmaa arkkitehtuurin kannalta ja perustelee myös, miksi jatkokehitettykin von Neumann -tyylinen ratkaisu näyttäisi tulleen tiensä päähän. Luku 3 jatkaa arkkitehtuurien tarkastelua tunnetuilla konkreettisilla arkkitehtuurityypeillä. Esimerkeiksi otetaan Intel x86, Cell BE, grafiikkasuoritinteknologiat sekä PRAM-mallia [FW78] soveltavat arkkitehtuurit. Niin sanottu Amdahlin laki lausuu, miten tehtävästä rinnakkaisuudella (parallelism) saavutettava laskentakyky on riippuvainen viime kädessä siitä, miten suuri osa siitä voidaan rinnakkaistaa. Esimerkiksi jos suoritusajallisesti 90% algoritmista rinnakkaistuu äärettömän monelle suorittimelle, mutta 10% vaatii peräkkäisen suorituksen, kokonaisuudessaan rinnakkaisuus voi lisätä tehtävän tehokkuuden vain kymmenkertaiseksi. Laitteisto voi toki suorittaa vapaaksi jääneillä resursseilla mahdollisesti myös muita samanaikaisia tehtäviä. Amdahlin laki määrittää algoritmin suoritusnopeudelle teoreettisen ylärajan, joten se on syytä ottaa huomioon tarpeettomien rajoitteiden välttämiseksi jokaisella kolmella esitellyllä abstraktiotasolla. Samanaikainen suoritus (concurrency), joko rinnakkain tai peräkkäin esim. säikeiden (thread) vuorottelulla on ollut ja on yhä monesti veret seisauttava ohjelmointikokemus. Arkkitehtuurit ovat yhä uusien optimointimahdollisuuksien toivossa sekoittaneet muistin eheysmalleja (consistency model) eli muistiresurssin semantiikan säännöstöjä niin, että oikein toimivan monisäikeisen ohjelman kirjoittaminen on muuttunut huomattavasti peräkkäismuotoista vastinetta vaikeammaksi. Ohjelmointikielten tekijät ovat pahentaneet tilannetta jättämällä abstrahoimatta vastaavan muistimallin joko kokonaan tai osittain, tai tehneet määrittelystä niin 7
monimutkaisen, ettei se ole verifioitavissa tai juuri avuksi tavanomaisissa ohjelmointitilanteissa. [AB10]. Samanaikaisen suorituksen ongelmat liittyvät ohjelman koordinointiin siten, että algoritmin ei haluta päätyvän virhetilanteeseen, josta järjestelmä ei automaattisesti kykene toipumaan. Tyypilliset ongelmat ovat suorituksen jumiutuminen resurssien virheellisen varausjärjestyksen takia eli lukkiuma (deadlock) sekä kilpailutilanne (race condition). Kilpatilanteessa ohjelman suoritukseen liittyvä epädeterministisyys voi vaikuttaa suorituksen välituloksiin tai lopputulokseen. Koska kilpatilanteeseen ei ole kehitetty mitään käyttökelpoista suoritusmallia, tila on järkevintä pyrkiä välttämään kokonaaan. On kuitenkin syytä huomata, että rinnakkaisuus ei automaattisesti johda samanaikaisuuden ongelmiin, varsinkaan huolellisesti rajattuja laskentamalleja käytettäessä. [AB10]. Moniydinarkkitehtuurien muodossa valtavirtaan uuden paluun tehnyt rinnakkaisohjelmointi sisältää kuitenkin aineksia tutkimuskysymyksille siitä, miten näitä arkkitehtuureja tulisi ohjelmoida tehokkaasti ja tuottavasti. Perinteisen rinnakkaisohjelmoinnin ansiosta saatavilla on joukko rinnakkaisohjelmoinnin työkaluympäristöjä ja teorioita [KR90], mutta näiden soveltuvuus uusille alustoille ei ole automaattisesti selvää. Selvä esimerkki moniydinjärjestelmien erosta perinteisiin laskentaryppäisiin on laskenta- ja muistiresurssien kertaluokkia suurempi fyysinen läheisyys, mikä mahdollistaa nopeamman kommunikoinnin ja datan jakamisen. Työn päätavoite onkin selvittää joukolla moderneja arkkitehtuureja sekä laskentaja ohjelmointimalleja keinoja nähdä, mitkä piirteet ovat relevantteja moniydinjärjestelmien ohjelmoinnissa ja miten mallit soveltuvat tehtäväänsä. Luku 4 havainnollistaa rinnakkaisongelmien ohjelmointia ottamalla esimerkinomaisesti käsittelyyn erilaisia luvun 3 arkkitehtuurien ohjelmointiin soveltuvia tunnettuja laskenta- ja ohjelmointimalleja sekä niiden toteutuksia. Huomio kiinnitetään myös ohjelmointimallien suureen saatavuuteen, joka on paljolti seurausta aihepiirin aktiivisesti kehittymisestä. Aihepiirin analysoinnin päättää luku 5, jossa pohditaan laskenta- ja ohjelmointimallien ominaisuuksia valittujen moniydinarkkitehtuurien hyödyntämisessä, saatavilla olevia resursseja tehokkaasti hyödyntäen, mutta toisaalta samalla ohjelmoitavuutta arvioiden. Lopuksi työn loppupäätelmissä (luku 8
6) kootaan yhteen tulokset käsitellyiltä abstraktiotasoilta ja pohditaan järkeviä suuntia jatkaa moniydinjärjestelmien ohjelmointiin liittyvää tutkimusta. 9
2 Moniydinarkkitehtuurien käsitteitä Tämä luku luo pohjan moniydinohjelmoinnin myöhemmälle analysoinnille tarkastelemalla monen suoritinytimen rinnakkaisarkkitehtuureja laitteiston näkökulmasta. Luku esittelee ensin arkkitehtuurin muodostavat osat (luku 2.1). Luku 2.2 määrittelee ne termit, joilla tässä tutkielmassa viitataan eri arkkitehtuurit muodostaviin perusosiin. Lopuksi luku 2.3 tarkastelee joukon rinnakkaisuuden toteutuksen mallintamiseen käytettyjä menetelmiä. Käytettyjä lähestymistapoja sovelletaan myöhemmin luvussa 3, jossa tarkastelu laajenee konkreettisiin arkkitehtuureihin sekä lyhyeen yhteenvetoon niiden välillä. 2.1 Arkkitehtuurin perusosat Kaupalliset suoritin- ja tietokonearkkitehtuurit ovat hyvin monimutkaisia kokonaisuuksia. Tyypillisesti mikään yksittäinen ominaisuus ei karakterisoi niiden suorituskykyä käytännöllisissä tilanteissa, sillä laskentateho muodostuu useiden eri toteutustekniikoiden yhteisvaikutuksesta. Mikäli arkkitehtuurien kuvauksesta abstrahoidaan pois kaikki laskennan kannalta toissijaiset komponentit, esimerkiksi syöttö- ja tulostuslaitteet, eri arkkitehtuureissa on kuitenkin nähtävissä kolme osaa, jotka määrittävät merkittävästi niiden suorituskykyä. Nämä tutkielman kannalta olennaiset osat ovat i) laskentayksiköt, ii) muistiyksiköt sekä näiden väliset iii) kommunikaatiokanavat tai verkot. Kuvassa 5 on esimerkinomaisesti kuvattu kaksi mahdollista tapaa kytkeä mainitut kolme osatyyppiä toisiinsa (rengasverkon reititys on katsottu kuvassa sisältyvän laskentayksiköihin). Router CPU CPU CPU CPU CPU node Memory Memory Memory Memory (a) Rengasverkko, hajautettu muisti. (b) Harva 2D-hilaverkko. Kuva 5: Erilaisia laitteiston arkkitehtuuriratkaisuja. Seuraavaksi käsitellään tarkemmin kuvan 5 esittelemät kolme yksikköä. 10
2.1.1 Laskentayksikkö Laskentayksiköksi (computational unit) määritellään tässä sellainen suorittimen (luku 2.2) osa, joka pystyy itsenäisesti suorittamaan joukon käskykannan käskyjä, toisin sanoen viemään ohjelman suoritusta askeleen eteenpäin. Esimerkiksi esitetystä Von Neumann -mallissa (kuva 1) laskentayksikön määritelmä kattaa kontrolli- ja aritmeettis loogiset yksiköt, joiden suoritettavaksi säikeen (luku 2.2) seuraava käsky voidaan antaa. Edelliseen on kuitenkin poikkeuksia, esimerkiksi liukuhihnat, jaetut laskentayksiköt sekä rinnakkaiset ja identtiset saman ohjelmalaskurin jakavat laskentayksiköt. Liukuhihnat suorittavat joka askelella vain osan käskyä, mutta samanaikaisesti osia eri käskyistä; liukuhihnan leveys määrittää suorituksesta valmistuvien käskyjen maksimimäärän aikayksikköä kohti. Jaetut laskentayksiköt saavat eri ohjelmalaskuria käyttäviltä kontrolliyksiköiltä käskyjä esimerkiksi jonotusperiaatteella, minkä tarkoituksena on fyysisen tilantarpeen pienentäminen vähän käytettyjä yksiköitä jakamalla. Tekniikka ei laske suorituskykyä merkittävästi, mikäli jaettu yksikkö ei ruuhkaudu samanaikaisten suorituspyyntöjen vuoksi. 2.1.2 Muistiyksikkö Muistiyksiköt sisältävät ohjelman sekä sen käyttämän datan, joko samassa tai erillisissä muistiavaruuksissa. Ohjelman ja datan jaon lisäksi muistiavaruus on tyypillisesti joko jaettu kaikkien laskentayksiköiden kesken (= jaettu muisti) tai hajautettu, jolloin laskentayksiköillä on omat yksityiset muistialueensa (= hajautettu muisti). Kolmas mahdollisuus on hajautettu jaettu muisti, joka on esimerkiksi fyysisesti hajautettu, mutta käsitteellisesti jaettu yksittäinen muistiavaruus. [HPGA03]. Jaettu muisti jaetaan tyypillisesti yhä edelleen UMA- (uniform memory architecture) ja NUMA-tyyppeihin (non-uniform memory architecture) käyttökustannuksensa mukaan. Tyypin eroavat siten, että UMA-muistin koko muistiavaruudella on 11
homogeeninen käyttökustannus, kun taas NUMA-muistissa osa muistiavaruudesta on lähempänä, ja näin edullisempaa käyttää. [HPGA03]. Yksittäinen muistiyksikkö voi sisäisesti olla jaettu aliyksiköihin, esimerkiksi suorituskyvyn parantamiseksi, limittäisiksi muistipankeiksi ja/tai muistihierarkiaksi. Muita olennaisia muistin parametreja ovat muistiväylän leveys, joka määrittää yhden muistikomennon kerralla siirtämän datan määrän sekä eri muistin toimintojen (esim. luku, kirjoitus) viipeet. Nykyisissä arkkitehtuureissa keskusmuistina käytettyjen DRAM-tyyppisten muistiyksiköiden suorituskyky on huomattavasti suoritinyksiköiden vastaavaa heikompi. Tämä näkyy siinä, että muistioperaatiot tarvitsevat 1-2 kertaluokkaa aritmetiikan suoritusta suuremman viiveen, jonka aikana muistiyksikkö ei kykene suorittamaan muita muistioperaatioita. Muistin rakenteen suunnittelu onkin keskittynyt pääasiassa parantamaan suorituskykyä tätä rajoitetta kiertäen. [HPGA03]. Muistien spesifikaatioiden analyysin selventämiseksi tässä esitetään esimerkkinä yleisen DDR-muistityypin (double data rate) laskelmia. DDR-muisti siirtää kaksi bittiä per kellojakso per väylän datalinja, josta sen nimi. DDR-ominaisuuden vuoksi esim. kaupallinen 2133 MHz:n DDR3-2133-muisti on kellotaajuudeltaan 1066 MHz. Muistiväylän kapasiteetti on 64 bittiä, joten sen teoreettinen suurin kaistanleveys on esim. 1066 MHz:llä 1066 8 = 17.066 GB/s. Käytännössä nopeuteen vaikuttaa koko muistihierarkian toiminta viiveineen sekä käskykanta, jolla muistia käsitellään. Teoreettisen muistikaistan merkitys ohjelman suoritusnopeuteen on kiistanalainen. Esimerkiksi Anandtech on saanut ristiriitaisia tuloksia [SK08]. Muistin lomitus on keino pienentää muistiyksiköiden keskimääräistä viivettä. Tekniikan idea on monistaa muistiyksikkö rinnakkaisiksi aliyksiköiksi (pankeiksi) ja käsitellä tätä kokonaisuutta kuten yksittäistä muistiyksikköä. Muistioperaatio varaa yksittäisen pankin tämän käyttöviiveen ajaksi, mutta operaatiota voidaan tasapainottaa pankkien kesken hajauttamalla muistiavaruus niin, että samanaikaiset pyynnöt jaetaan jouten odottaville pankeille. [HPGA03]. Muistiavaruus voidaan lomittaa aliyksiköiden kesken esimerkiksi yksinkertaisella 12
jakojäännöskaavalla osoite mod n, missä n on rinnakkaisten pankkien lukumäärä. Hajautus on tyypillisesti määritetty staattisesti laitteiston rakenteessa. Mikäli hajautus johtaa samanaikaisiin operaatioihin jossakin pankissa, on kyse pankkikonfliktista. Konflikti voidaan ratkaista esimerkiksi suorittamalla konfliktin synnyttäneet operaatiot peräkkäisesti. [HPGA03]. Hajautus voidaan saada joissakin tilanteissa saada optimaalisen tehokkaaksi mitoittamalla algoritmi käytetylle laitteistolle. Työ voidaan automatisoida jossain määrin myös kääntäjälle. Käytännössä konfliktit ovat muissa tapauksissa aina mahdollisia. Konfliktista aiheutunut lisäviive tulee huomioida myös arkkitehtuurin rakenteessa, sillä se muuttaa vakiokokoisenkin muistin viipeen vaihtelevaksi, ja voi jopa huonontaa suorituskykyä käyttötapauksessa, jossa peräkkäiset konfliktit johtavat operaatiot samaan muistipankkiin. Lomituksen vaikutus muistin viipeeseen riippuu suoritettavasta algoritmista, mutta jos muisti on jaettu n identtiseen pankkiin, yksittäisen muistioperaation keskimääräiselle viiveelle voidaan laskea paras (t best ) ja huonoin (t worst ) arvo seuraavasti (t bank on yksittäisen pankin viive): t best = t bank n t worst = t bank Lomitus mahdollistaa myös muistiväylän leventämisen siten, että yksittäinen muistioperaatio käsittelee tietoa samanaikaisesti useasta eri pankista. Mikäli operaatiot käyttävät pankkeja hallitusti yhdessä, vältetään konfliktien riski. [HPGA03]. Tekniikka tosin edellyttää tukea ohjelmoijalta, esimerkiksi laajempien vektoritietotyyppien tai iterointisilmukoiden avaamisen muodossa, jolloin peräkkäiset viittaukset saadaan hallitusti hajautettua. Muistihierarkia Johdantoluvussa kuvattiin lyhyesti syyt ja periaate, miten arkkitehtuureissa tavanomaisesti käytetään monitasoista muistihierarkiaa; muistiteknologioissa muistin käytön viive on karkeasti kääntäen verrannollinen muistin hin- 13
taan ja fyysiseen kokoon [HPGA03]. Käytännön sovellukset asettavat myös rajoja vaaditun muistin määrälle, laitteen fyysiselle koolle ja hinnalle, joten suhteellisesti hitaammasta DRAM-muistista ei voida kokonaan luopua. Muistien koon kasvun myötä uusimmat välimuistihierarkiat ovat laajentuneet jo kolmitasoisiksi, kuten Alpha 21264:ssa [HPGA03] ja nykyisin Intelin Nehalemarkkitehtuurissa [Int11b]. Rinnakkaisuuden parantamiseksi (suorittimesta katsoen) ensimmäisen tason välimuisti on usein lisäksi jaettu käsky- ja dataosiin. [HPGA03]. Välimuistin toiminta on tavallisesti automaattista ja johonkin heuristiikkaan perustuvaa, mutta käsiteltävistä arkkitehtuureista Cell BE (luku 3.3) antaa poikkeuksellisesti kokonaan ja Fermi (luku 3.4) osittain välimuistin hallinnan ohjelmoijan tehtäväksi. Välimuistia voi käyttää näin eräänlaisena paikallisena käyttömuistina. Välimuistin suorituskyvyn mittaamiseen on useita menetelmiä, mutta [HPGA03] katsoo yksinkertaisesti keskimääräisen muistin hakuajan t avg olevan vertailukelpoisin. Hakuaika lasketaan kaavalla: t avg = t hit + f miss t miss Tässä t hit kuvaa arvon välimuistista hakemisen viivettä, f miss frekvenssiä, jolla arvon luku välimuistista epäonnistuu ja t miss viivettä, joka syntyy arvon hakemisesta alempaa muistihierarkiasta. f miss riippuu välimuistin koosta, toimintaperiaatteesta ja laskennallisesta ongelmasta, ja myös jossain määrin esimerkiksi käyttöjärjestelmän toimintaperiaatteesta sekä muusta samanaikaisesta laskennallisesta kuormasta. Kaava voidaan yleistää useampitasoiselle muistihierarkialle ketjuttamalla viiveen arvo t miss alemman muistihierarkian kustannukseen: t avg = t hitl1 + f missl1 t missl1 t missl1 = t hitl2 + f missl2 t missl2... 14
Muistihierarkia tuo myös uuden viiveen optimointimahdollisuuden; data voidaan esihakea nopeampaan muistiin, jolloin sen käyttö ei aiheuta käytön yhteydessä uutta hakua hitaammasta muistista, ja pienentää näin keskusmuistin aiheuttaman viiveen vaikutusta. Esihaku voidaan tehdä joko eksplisiittisesti osana suoritettavaa koodia tai automaattisesti osana muistiarkkitehtuuria. Automaattinen haku edellyttää jotain heuristiikkaa, esimerkiksi ajallista tai viittauksen sijainnin paikallisuutta; mikäli muistiviittaukset ovat vierekkäin, haku voi noutaa ne kaikki ja mikäli muistiviittauksen viime käyttökerrasta on vähän aikaa, se saattaa yhä sijaita myös välimuistissa. [HPGA03]. Vaikka edellä esitetty kaava ei asiaa suoraan kerro, on syytä huomata, että välimuisti toimii tyypillisesti lohkoissa, ei yksittäinen muistihaku kerrallaan. Lohkojen ideana on nimenomaan tasoittaa hakemisen kustannusta silloin, kun käsitellään vierekkäisiä muistiviittauksia lyhyen ajan sisällä. Edellä mainittu esihaku voidaan heuristisesti ajoittaa niin, että esimerkiksi seuraavan välimuistin lohko noudetaan jo kun edellisen käyttö on loppumassa. Muistihierarkia hajauttaa ja replikoi käsiteltävän datan tilaa eri välimuistitasoille. Muistin toiminnan tehostamiseksi hierarkia saattaa käyttää kevennettyä eheysmallia, jossa keskusmuisti ei ole jatkuvasti ajantasaisessa tilassa, vaan tieto välitetään erilaisin tekniikoin viivästetysti eteenpäin seuraaville muistin tasoille, esimerkiksi eri tavoin puskuroituna. Vaikka tällä tavoin löysennetty muistin eheys on vaihtelevaa viivettä lukuun ottamatta lähes läpinäkyvä laskentayksikölle, yhdenkin laskentayksikön arkkitehtuurissa ongelmia tuottaa kuitenkin syöttö- ja tulostuslaitteisto, joka saattaa vaatia muistilta eheää tilaa. [HPGA03]. Mikäli arkkitehtuurissa useampi kuin yksi laskentayksikkö käyttää samaa muistia muistihierarkioiden kautta, tarvitaan ns. koherenssiprotokolla, jonka perusteella tieto saadaan tarvittaessa noudettua toisen muistihierarkian välimuistista, mikäli keskusmuistin data ei ole ajan tasalla. Koherenssiprotokollista on kerrottu tarkemmin luvussa 2.1.3. Muistiviiveen peittäminen säikeistyksellä Muistiviiveen peittäminen säikeillä perustuu olettamukseen siitä, että i) suorituksessa on samanaikaisesti useita 15
säikeitä (luku 2.2) ja ii) muistioperaation suorituksen aikana jokin aktiivisista säikeistä suorittaa muistioperaation joko toiseen, vapaaseen muistipankkiin, johonkin muuhun käytettävissä olevaan muistihierarkian yksikköön tai suorittaa paikallisesti laskentaa ilman muistiviittauksia. Mikäli suorittimella riittää laskettavaksi aktiivisia säikeitä koko muistiviiveen ajaksi, sen ei tarvitse pysähtyä odottamaan. Viivettä on pyritty peittämään sekä käskyettä säietasolla (luku 2.3). Koska käskyjen suorituksen ja muistin viiveiden ero on niin merkittävä, käskytason rinnakkaisuudella on vaikea kätkeä koko viivettä. Tähän on kuitenkin kehitetty tekniikoita, muun muassa ns. ylimääräisen rinnakkaisuuden (parallel slackness) [Val90, LP07] hyödyntäminen käskytason rinnakkaisuuden lisäämiseksi. 2.1.3 Verkko Moniydinarkkitehtuurin pelkistetyssä mallissa laskenta- ja muistiyksiköt ovat yhteydessä toisiinsa verkon välityksellä. Yhden suorittimen ja muistin tapauksessa kytkentämahdollisuuksia ei käytännössä ole kuin yksi, mutta yksiköiden määrän kasvaessa erilaiset topologiat eli verkon perusrakenteet tulevat mahdollisiksi. Verkon merkitys on ilmeinen, sillä se vaikuttaa suoraan muistin ja laskentayksikköjen kesken vaihdetun datan viiveisiin ja kaistanleveyteen. Verkkoa voi kuvata parametreilla, jotka ovat laskennan kannalta aina rajoituksia verrattuna siihen, että verkon päätepisteet kuuluisivat samaan yksikköön. Nämä parametrit ovat Hennessyn ym. [HPGA03] mukaan: 1. kaistanleveys, 2. viestin alun perille kulkeutumisen viive, 3. koko viestin verkon yli välittämisen viive, 4. lähettäjän käsittelystä johtuva viive ja 5. vastaanottajan käsittelystä johtuva viive. 16
Mainitut viiveet voivat olla topologiasta ja sen reitityksestä sekä viestien koosta ja tyypistä riippuen joko staattisia tai dynaamisia. Viiveisiin vaikuttaa luonnollisesti verkon kuormitusaste, mutta käsite sisältyy mainittuihin parametreihin. Erilaisia topologioita ovat esimerkiksi rengas, tähti, hila, perhonen, hyperkuutio ja täysin kytketty verkko. [HPGA03]. Erilaisten verkkotopologioiden kattava analyysi sivuutetaan tässä muutamia moniydinjärjestelmiin liittyviä huomioita lukuun ottamatta. Vaikka aiemmissa monitietokonejärjestelmissä on käytetty monenlaisia topologioita, moniydinjärjestelmät ovat vasta siirtymässä niihin. Laajalti käytetty moniydintopologia on SMP (symmetric multiprocessing), jossa eri laskenta- ja muistiyksiköt on kytketty samaan väylään. [HPGA03]. SMP skaalautuu kuitenkin huonosti laskentayksiköiden määrän lisääntyessä, sillä väylällä on kiinteä kaistan ja viiveen yläraja, mutta väylän toimintavaatimukset kasvavat lineaarisesti laskenta- ja muistiyksiköiden määrää lisäämällä. Lisäksi huomioitavana on samanaikaisten operaatioiden koherentti ja konfliktivapaa eteneminen väylällä, mikä aiheuttaa tehokkuuden kustannuksella lisätyötä. [HPGA03]. Esimerkiksi rengasväylä kuvassa 5 ratkaisee skaalautumisongelman siten, että sopivasti arkkitehtuurille suunniteltu algoritmi voi paloitella laskennan ja muistin käytön enemmän liukuhihnatyylisesti eri laskentayksiköiden välille, jolloin renkaan yksittäisiä linkkejä voidaan hyödyntää samanaikaisesti. Luku 3 antaa viitteitä siitä, että kuluttajamarkkinoilla yleiset arkkitehtuurit (x86) ovat siirtymässä pois SMP:stä. Luku myös esittelee muutaman toisenlaisen arkkitehtuuriratkaisun. On kuitenkin arveltu, että laskentayksiköiden määrän kasvaessa huomattavasti nykyisestä tarve kaistanleveydelle kasvaa yhä. Ratkaisuksi on ehdotettu harvoja verkkoja, joissa verkon solmut koostuvat lähettäjien ja vastaanottajien lisäksi reitityskoneistosta. Harvat verkot pystyvät tarjoamaan suuren kaistanleveyden nostamatta viivettä käyttökelvottoman korkeaksi, mikä on tärkeää, koska kaistanleveyttä on mahdollista kasvattaa vain arkkitehtonisin ratkaisuin, kun taas viiveitä on mahdollista kätkeä ohjelmallisesti. [LP07]. Jaetun muistin arkkitehtuureissa muistihierarkiat luovat verkolle lisävaatimuksia. SMP on skaalautumisongelmista huolimatta ollut yleinen tekniikka, sillä sitä käyt- 17
täen on mahdollista toteuttaa pienille suoritinmäärille tehokas koherenssiprotokolla, joka tunnetaan nimellä nuuskintaprotokolla (snooping protocol). Tämä mahdollistaa tiedon verrattain nopean poiminnan kunkin muistihierarkian sisältä. Tekniikan idea on, että jokainen väylään kytketty muistihierarkia pitää kirjaa siitä, omistaako se uusimman versio kustakin datasta, poimii väylältä muistipyynnöt analysoitavakseen ja joko mitätöi datan, mikäli joku muu väylällä kertoo päivittävänsä sitä, tai syöttää pyynnön vastauksen väylään, mikäli paikallinen data on kirjanpidon mukaan ajan tasalla. [HPGA03]. 2.2 Termejä Moniydinteknologian nopean kehityksen ja kaupallisten syiden takia laitteita käsittelevän kirjallisuudenkin mukaan käsitteet ovat hieman yliampuvia, esimerkiksi NVIDIA:n CUDA-ydin on käsitteenä varsin erilainen kuin Intelin x86-ydin. Tässä on katsottu, että selvyyden vuoksi on hyvä käyttää eri arkkitehtuurien osiin viitatessa joukkoa standardinimiä, jotta rakenteiden käsitteellinen hahmottaminen ja vertailu olisi helpompaa. Määritelmät sopivat kuvaamaan myös ohjelmointimallien osia, mutta esim. säikeet ovat olennainen osa jo arkkitehtuuritason kuvausta, joten ne on esitelty tässä. Suoritin (processor) Suoritin voidaan ymmärtää toisiinsa liittyvien laskentayksikköjen, rekisterien, välimuistien ja muiden resurssien kokonaisuudeksi. Käsitteen alkuperä on perinteisissä arkkitehtuureissa, joissa on tavallisesti yksi monoliittinen keskussuoritin (CPU, central processing unit), joka on samalla yksittäinen fyysinen komponentti. Nykyisin suorittimen sijaan on alettu käyttää termiä suoritinydin (core) kuvaamaan tätä kokonaisuutta, ja termiä suoritin viittaamaan mahdollisesti usean suoritinytimen kesken jaettuun fyysiseen integroituun piiriin. Tutkielmassa käsitteiden ongelmallisuus on ratkaistu käyttämällä tätä kuvattua jakoa. Moniydinsuoritin (multicore) ja monisuoritin (multi-processor) Tässä moniydinsuorittimella viitataan edellä kuvatun arkkitehtuurin suorittimeen, joka 18
voi koostua useasta itsenäisestä suoritinytimestä. Monisuoritin viittaa (usein hajautettuun) järjestelmään, joka koostuu monesta suorittimesta. Säie (thread) Säie on kirjallisuudessa ja teollisuudessa de facto -asemaan vakiintunut abstrakti tapa mallintaa laskentaresursseja, joita käyttäen suoritinydin tai tarkemmin sen laskentayksikkö saadaan laskemaan säikeen edustamaa ohjelmaa. Ytimessä on ikään kuin säikeen suorituspaikkoja, joissa säikeitä voidaan suorittaa askel kerrallaan. Säie käsittää tilan ja ympäristön (ohjelmalaskuri, rekisterit, pino, muisti) muodostaman kokonaisuuden, joilla laskenta suoritetaan. Säie on eri laskentamallien kannalta perusyksikkö, jota käyttäen työ voidaan jakaa ja rinnakkaistaa eri laskentayksiköiden kesken. Monisäikeisyys (multi-threading) Usean säikeen samanaikaista suorittamista arkkitehtuuritasolla. Samanaikaisuus voi myös olla näennäistä esimerkiksi siten, että laskentayksikkö vuorottelee suoritusta eri säikeiden välillä, mikäli laskentayksiköitä on esimerkiksi vain yksi. Kuitu (fiber) Joskus käyttöjärjestelmän tarjoamat säikeet ja prosessit (luku 4.1.1) koetaan raskaiksi ja joustamattomiksi, ja ne halutaan erottaa vielä hienojakoisemmasta käsitteestä, kuidusta. Kuidulla tarkoitetaan säikeiden päälle rakennettua järjestelmää, jossa jokaisen varsinaisen säikeen hallittavana on joukko kevyitä kuituja. Kuitujen aikataulutus tapahtuu suorituksen aikaisen järjestelmän toimesta ja se perustuu vapaaehtoisen kontrollin luovuttamiseen, mikä implisiittisesti johtaa synkronisointiin saman säikeen ohjaamien kuitujen välillä. Ympäristön vaihto (context switch) voidaan tehdä näin myös huomattavasti nopeammin, sillä vaihto ei edellytä käyttöjärjestelmän siirtymistä eri tilojen välillä, mikä on SMT-tekniikkaa (luku 2.3.3) käyttävässä arkkitehtuurissa aikaa vievää. [SSS92]. 19
2.3 Rinnakkaisuuden mallintaminen 2.3.1 Abstraktiotasot Suorituksen rinnakkaistamistekniikat voidaan jakaa viiteen erityyppiseen abstraktiotasoon, jotka ovat bitti-, käsky-, tieto-, säie- ja tehtävätasot. Näistä kaksi ensimmäistä ovat yksittäisen ytimen ja säikeen kautta vaikuttavia nopeutustekniikoita, kun taas tieto-, säie- ja tehtävätason rinnakkaisuus voidaan toteuttaa myös hyödyntämällä useaa säiettä ja suoritinydintä. Abstraktiotasoista on syytä mainita, että kuvatut tekniikat eivät ole toisiaan poissulkevia vaan ennemmin täydentäviä. Bittitaso Bittitason rinnakkaisuus on hyvin matalan tason tekniikka, jonka pääidea on rinnakkaistaa tietotyyppien arvoalueen rajallisuudesta seuraavan tiedon järjestelyn peräkkäisyys lisäämällä yksittäisen käskyn käsittelemien bittien lukumäärää. Esimerkiksi jos n-bittiset luvut lasketaan yhteen korkeintaan n bitin lohkoissa, 2 osasummien muodostus ja yhdistäminen vaativat esimerkiksi taulukon 1 tapauksessa 14-kertaisen määrän laskutoimituksia. Tekniikka on menettänyt merkitystään nykyisin, kun yleisimmin käytetyt 32- ja 64-bittiset tietotyypit ovat osoittautuneet riittäviksi useimpiin tarkoituksiin. Tietotyyppien koon kasvattamista hankaloittaa myös se, että saman fyysisen tilan piirillä voi käyttää rekisterien määrän lisäämiseen. Käskytaso Käskytason rinnakkaisuudella (ILP) tarkoitetaan tekniikoita, joilla rinnakkaisuutta lisätään yksittäisten konekielisten käskyjen tasolla niin, että samalla aikavälillä voidaan suorittaa entistä enemmän käskyjä. Tekniikoita tunnetaan niin paljon, että tässä voidaan keskittyä vain muutamaan merkittävimpään, joita ovat mm. suorituksen liukuhihnoitus, superskalaaritekniikka ja VLIW (very long instruction word), aikataulutus, haarautumisen ennustus sekä suoritus epäjärjestyksessä (out-of-order execution). [HPGA03]. Liukuhihnoituksen idea on jakaa käskyt osiin, joiden suoritusaika on yksittäistä käskyä lyhempi. Liukuhihna kykenee suorittamaan laskennallisesti yhden koko- 20
Taulukko 1: Bittitason rinnakkaisuus kertolaskussa c = a b. n-bittinen CPU a low = a:n alimmat n bittiä 2 a high = a:n ylimmät n bittiä 2 b low = b:n alimmat n bittiä 2 b high = b:n ylimmät n bittiä 2 t low = a low b low t middle = a low b high + a high b low t high = a high b high t low = t:n alimmat n bittiä 2 t high = t:n ylimmät n bittiä 2 c low = t low + t low 2 n 2 c high = t high + t high 2n-bittinen CPU c = a b naisen käskyn jokaista kellojaksoa kohti, joskin hieman viivästetysti. Liukuhihnoituksen seurauksena käskyn suoritusaika määräytyy monoliittisen suoritusvaiheen sijaan yksinkertaistetusti liukuhihnan hitaimman vaiheen mukaisesti. Käytännössä lähes kaikki modernit arkkitehtuurit käyttävät liukuhihnoitusta. [HPGA03]. Superskalaaritekniikka on perusmuotoisen liukuhihnan laajennus siten, että liukuhihna kykenee suorittamaan fyysisesti rinnakkaisten ja jaettujen laskentayksiköiden avulla useampia käskyjä yhdellä kellojaksolla. Tekniikan voi nähdä myös niin, että liukuhihnalla on käytännössä aina rinnakkaisia yksiköitä (esimerkiksi muistioperaatiot ja aritmetiikka), joita ei voida käyttää samaan aikaan yhdestä säikeestä. Superskalaari rakenne poistaa tätä tyhjäkäyntiä mahdollistamalla rinnakkaisen käytön. Laitteisto määrittää tällöin, mitkä suoritettavista käskyistä rinnakkaistetaan. [HPGA03]. Superskalaarin ohella toinen tapa rinnakkaistaa liukuhihnaa on suorittaa rinnakkain leveitä useasta rinnakkaisesta käskystä koostuvia käskyjä (Very Long Instruction Word, VLIW). Tällä tavoin rinnakkaisuus on määritetty staattisesti suoritettavassa koodissa. Käytännössä mainitun kaltaisilla tekniikoilla saadaan aikaan yleensä korkeintaan 8 käskyn samanaikainen suoritus samassa laskentayksikössä, vaikka teoreettinen maksimi olisikin suurempi. [HPGA03]. Liukuhihna saadaan mahdollisesti vielä suorituskykyisemmäksi poimimalla käsky- 21
virrasta käskyjen väliset riippuvuudet ja aikatauluttamalla dynaamisesti käskyjen suoritusjärjestys. Tällöin käskyjä on mahdollista jopa suorittaa epäjärjestyksessä suorittimen suoritusmallin muuttumatta. Tekniikkaan liittyy myös haarautumisen ennustus, joka viittaa kontrollivuon hyppykäskyjen vaikuttamiin tehokkuusongelmiin esimerkiksi muistihierarkian kanssa. Hyppyjen toiminnan arvaava suoritin pystyy esilataamaan dataa, joten hyppy ei aiheuta yhtä suuria kustannuksia muistiviiveiden takia. Epäjärjestyksessä suorituksen haittapuoli on sen verrattain iso tilantarve suorittimen mikrosirulla, mistä syystä jotkin moniydinarkkitehtuurit eivät sitä hyödynnäkään. [HPGA03]. Säietaso Säietason rinnakkaisuus käsittelee käskytasoa suurempia kokonaisuuksia, nimittäin kokonaisia suoritettavia säikeitä. Perusideana on määrittää ensin laitteistosta laskentayksiköt, eli jokainen ohjelmalaskuri, rekisterit ja muut tarvittavat tiedot sisältävä kokonaisuus. Laskentayksiköillä on kyky suorittaa yksittäistä säiettä. Algoritmi on ylemmillä abstraktiotasoilla jaettu jollakin tapaa suoritettaviksi säikeiksi. Nyt säietason rinnakkaisuuden tehtävä on aikatauluttaa säikeet siten, että ne hyödyntävät kaikkia rinnakkaisia laskentayksiköitä mahdollisimman tehokkaasti. [MSM04]. Säikeiden jakamiseen laskentayksiköille on eri arkkitehtuureissa erilaisia toteutuksia ja osa jättää kuorman tasauksen käyttöjärjestelmän, kielen tai käyttäjän vastuulle. Säikeiden aikatauluttamisesta laskentayksikön sisällä otetaan tässä kaksi esimerkkiä, kiinteä aikaviipaleiden jako sekä irrottava aikataulutus (pre-emptive scheduling), jossa aikaviipale on tyypillisesti pidempi, mutta suoritus voi keskeytyä ympäristön vaihtoon jo aiemmin esim. syöttö- ja tulostuslaitteiden sekä muistihierarkian laitteistopohjaisten keskeytysten takia. [HPGA03]. Kiinteiden aikaviipaleiden jako lomittaa säikeiden suoritusta säännöllisesti. Tällöin laskentayksikön kapasiteetti tavallaan jaetaan aika-akselilla säikeiden kesken. Pakotetun aikalomituksen heikkoutena peräkkäissuoritukseen nähden on se, että suorituksessa voi ilmetä tarpeetonta joutokäyntiä, jos seuraavaksi suoritettava säie ei ole valmis tai säikeitä on ajettavana liian pieni määrä. Irrottava aikataulutus puolestaan ei tuhlaa aikaa jouten oloon, mikäli säikeitä 22
on suoritusvalmiina, mutta se ei ole yhtä deterministinen suorituksen etenemisen muodossa. Se kuitenkin mahdollistaa esimerkiksi viiveiden peittämisen laskennalla vaihtamalla suoritettavaa säiettä muistiviiveen aikana. Säikeiden hallintaa on tarkemmin tarkasteltu myöhemmin luvussa 2.3.3. Tietorinnakkaisuus Tietorinnakkaisuus on rinnakkaisuuden abstraktio, jonka perusidea on tasata yksittäisen datan laskenta rinnakkaisten laskentayksiköiden kesken. Mekanismin toteutus voidaan tehdä usealla tavalla sen mukaan, miten laskentayksiköt ja niiden välinen kommunikointi on toteutettu. Mikäli tarkastelusta suljetaan pois usean tietokoneen laskentaryppäät, ja keskitytään monisuoritin- ja moniydinratkaisuihin, mahdollisia toteutustapoja ovat tiedon jakaminen suorittimien, ytimien ja/tai ytimien sisäisten erikoisleveiden vektorirekisterien kesken. Monien kaupallisten suorittimien tukemat multimediakäskyt voidaan ottaa esimerkiksi tietorinnakkaisista vektorikäskyistä. Käskyjen käyttämien rekisterien rakenne on joko kiinteä tai käskyn konfiguroitavissa. Käskyjen toimintaperiaate on määrittää ensin vektorin sisäinen rakenne bittitasolla, esimerkiksi jakaa se kahdeksi tasalevyiseksi arvoksi, tämän jälkeen ladata ja pakata arvot latauskäskyllä, suorittaa vektorioperaatio, ja lopuksi purkaa arvot tavallisiin rekistereihin tai keskusmuistiin. Vektorikäskyjen toteutus on melko suoraviivainen, mutta samalla joustamaton, sillä kääntäjän tai ohjelmoijan on erikseen sovitettava data käyttämään niitä. Vektorikäskyt ovat harvoin läheskään identtisiä eri arkkitehtuureissa, vaikka perusperiaate on sama. [HPGA03]. Toinen perinteinen tapa on kuvata tietorinnakkaisuus tehtävärinnakkaisuudeksi ja jakaa syntyvät tehtävät eri laskentayksiköille, olivat ne sitten suorittimia, ytimiä tai jopa hitaan lähiverkon takana. Tehtävien lukumäärä tulisi mitoittaa eri laskentayksiköiden mukaan niin, että tehtävien ylläpidon aiheuttaman kirjanpidon kustannukset ja toisaalta usean limittäisen tehtävän viiveitä peittävä vaikutus ovat tasapainossa. Hitaampi kommunikaatiokanava edellyttää isompia tehtäviä kirjanpidon kustannuksen pienentämiseksi. [MSM04]. Tietorinnakkaisuus voidaan toteuttaa myös muilla säikeisiin perustuvilla tekniikoilla. GPU-arkkitehtuurien yhteydessä luvussa 3.4.2 on esitetty SIMT-tyyppinen 23
(single instruction, multiple thread) ratkaisu, joka mahdollistaa saman käskyn suorittamisen useassa eri säikeessä samanaikaisesti. Tekniikka on vektorikäskyjä joustavampi, sillä säikeiden suoritus voi haarautua, eikä tästä aiheudu kooditasolla ongelmia, koska laitteisto käsittelee koko haarautumisen. Toinen mahdollinen toteutusesimerkki on XMT-arkkitehtuurin (luku 3.6) omaksuma SPMD (luku 2.3.2), jossa myös eri säikeet käsittelevät dataa rinnakkain tähän määritetyissä lohkoissa. Tehtävärinnakkaisuus Tehtävärinnakkaisuudessa perusyksikkö on tehtävä, joka on tyypillisesti useita käskyjä sisältävä, mahdollisesti muihin tehtäviin nähden täysin itsenäinen algoritmin osa. Tehtävät voivat kommunikoida keskenään erinäisin kommunikaatiokanavin, jotka riippuvat toteutustekniikasta. [MSM04]. Käytännössä tehtävä on kuvattu yleensä prosessilla, säikeellä tai kuidulla, sillä nämä ovat sopivan kokoisia abstraktioita tarjoamaan tehtävän vaatiman käskyvirran ja sen suorituksen. Tehtävärinnakkaisuus on perinteisissä suurteholaskennan sovelluksissa sekä työasema- ja palvelintietokoneissa tyypillinen toteutustekniikka. Ratkaisun perustelu seuraa siitä, että alemman tason abstraktiot eivät esimerkiksi skaalaudu suoraan monen tietokoneen välille laskentaryppäissä, mikäli näillä ei ole jaettuja muisteja ja laskentayksiköitä. Myös mikäli suorittimen sisällä eri ydinten välinen kommunikointi ja synkronointi on hidasta, ainoastaan tarpeeksi suuret tehtävät tekevät rinnakkaisuuden hyödyntämisen kannattavaksi ja tehtävärinnakkaisuuden ainoaksi järkeväksi muodoksi. [MSM04]. Kolmas merkittävä syy on tehtävärinnakkaisuuden suosiolle on sen tuoma ilmainen rinnakkaisuus samanaikaisten eri sovellusten muodossa; moniajava käyttöjärjestelmä mallintaa minkä tahansa suoritettavan sovelluksen tehtävänä, joten useita säikeitä ja laskentayksiköitä tukeva käyttöjärjestelmä kykenee rinnakkaistamaan automaattisesti myös peräkkäismuotoisia ohjelmia sillä oletuksella, että käynnissä on riittävästi sovelluksia eri laskentayksiköille. Tämän rajoitteena on kuitenkin se, että ydinmäärät jatkavat yhä eksponentiaalisesti kasvuaan, mutta työasemaympäristössä käyttäjän on vaikeaa tai työlästä luoda rinnakkaisuutta yleisesti suorittamalla yhä enemmän ja enemmän samanaikaisia sovelluksia. Rinnakkaisuuden 24
luominen tähän tapaan erillisillä tehtävillä on myös kestämätön siksi, että monissa sovelluksissa erilaisten korkean tason tehtävien määrä ei ole merkittävä, toisin kuin sovellusten käsittelemän tietorinnakkaisen datan määrä. 2.3.2 Flynnin taksonomia Flynnin taksonomia on vuonna 1966 esitetty klassinen tapa kategorisoida tietokoneet neljään eri ryhmään niiden rinnakkaisuuden toteutuksen perusteella. Jako on verrattain karkea, mutta se on yhä käyttökelpoinen nykyisiä toteutustapoja arvioitaessa ja antaa tavan mallintaa rinnakkaisuuden toteutusta. Taksonomian kategoriat (taulukko 2) ovat SISD (single instruction stream, single data stream), SIMD (single instruction stream, multiple data streams), MISD (multiple instruction streams, single data stream) ja MIMD (multiple instruction streams, multiple data streams). [HPGA03] Taulukko 2: Flynnin taksonomia. Yksi käskyvirta Useita käskyvirtoja Yksi datavirta SISD MISD Monta datavirtaa SIMD MIMD SISD käsittää yksiytimiset suorittimet, joissa peräkkäiset käskyt käsittelevät aina yhtä datayksikköä. SIMD laajentaa edellistä siten, että useampi laskentayksikkö suorittaa saman käskyn eri datalle. Tällöin käskymuisti on yhteinen, mutta yksiköiden datamuisti eroaa. Vektorisuorittimet ja modernien suorittimien multimedialaajennukset voidaan nähdä SIMD-toteutuksiksi. MISD on eräänlainen tietovirtasuoritintyyppi [GEC + 07], jossa useampi laskentayksikkö käsittelee samaa dataa. Toinen MISD:n käyttökohde on vikasietoisten suoritinten arkkitehtuureissa, joissa sama laskutoimitus halutaan tuloksen varmistamiseksi ajaa monesti. MIMD on taksonomian monipuolisin suoritintyyppi, jossa jokaisella laskentayksiköllä on itsenäinen käsky- ja datamuisti. [HPGA03] MIMD on usein jaettu edelleen kahteen osaan, SPMD:hen (single program, multiple data) ja MPMD:hen (multiple program, multiple data). SPMD kuvaa tilannetta, jossa eri laskentayksiköt suorittavat samaa ohjelmaa samanaikaisesti eri 25
tahdissa, aina synkronointiin asti. Esimerkiksi luvun 3.6 XMT noudattaa tätä tekniikkaa. MPMD laajentaa edellistä sallimalla eri laskentayksiköille eri ohjelmatilat, mikä mahdollistaa suorituksen haarautumisen itsenäisemmin samanaikaisuudesta tinkimättä. NVIDIA:n GPU-alusta (luku 3.4) käyttää SPMD-tyylin toteutuksessa useita säikeitä yhdistävää SIMT-tekniikkaa. Flynnin taksonomiaa tarkasteltaessa on syytä huomata, että monet nykyisin tunnetut suorittimet ovat eri kategorioiden yhdistelmiä. Esimerkiksi moniin suorittimiin on lisätty SIMD-käskyjä, sillä se on nähty edullisena ja helppona tapana moninkertaistaa näiden käskyjen laskentakapasiteetti; SIMD-käskyjen lisääminen on esimerkiksi Intelin arkkitehtuurissa lisännyt tehonkulutusta ja mikropiirin pintaalaa alle 10%, mutta lisännyt laskentatehoa 50-300% [Pol99]. SIMD-tekniikka sai huomiota 80-luvulla ja 90-luvun alkupuolella, mutta monet uudemmat arkkitehtuurit ovat toiminnaltaan lähempänä MIMD-tekniikkaa. MISD käyttö kaupallisissa suorittimissa on puolestaan ollut aina vähäistä. [HPGA03] 2.3.3 Säikeiden hallinta laitteistossa Kuten luvussa 2.3.1 on mainittu, säikeitä voi pitää tietyllä tavalla universaalina laskentaresurssien rajapintana. Säikeiden pääasiallisimmat käyttökohteet ovat tällöin rinnakkaisten laskentayksiköiden käyttöönotto, laskentaan ja muistin käyttöön liittyvien viiveiden piilottaminen sekä suoritusajan jakaminen eri toimintojen kesken. Yksi syy monisäikeisyyden tarjontaan on myös se, että valmistuskustannusten ja piirin pinta-alan pienentämisen kannalta on verrattain edullista esittää liukuhihnoitettu toteutus monisäikeisenä kokonaisten itsenäisten laskentayksiköiden sijaan. Esimerkiksi tiettyjen vähän käytettyjen erikoislaskentayksikköjen jakaminen saattaisi mahdollistaa useamman ytimen sijoittamisen samalle suorittimelle. Samanaikaisuus saavutetaan säieabstraktion keinoin silloin, kun arkkitehtuuri tarjoaa monisäikeisen suoritusmallin, rinnakkaisuus silloin, kun säikeitä suorittavia yksiköitä on fyysisesti useita. Erilaisia monisäietoteutuksia voidaan nähdä ainakin kolmea eri tyyppiä: lohkomuotoista, lomitettua ja samanaikaista monisäikeisyyttä hyväksikäyttäviä. Näitä kuvaavat tässä järjestyksessä kuvan 6 liukuhihnatyypit 26
Issue slots Superscalar Coarse MT Fine MT SMT T i m e Kuva 6: Eri tapoja täyttää 4:n rinnakkaisen yksikön superskalaari liukuhihna. Eriväriset harmaat laatikot kuvaavat eri säikeitä. [HPGA03]. Coarse MT (karkean tason monisäikeisyys), Fine MT (hienojakoinen monisäikeisyys) ja SMT (simultaneous multi-threading) eli samanaikainen monisäikeisyys. Lohkomuotoinen monisäikeisyys Lohkomuotoisessa säietoteutuksessa säikeitä suoritetaan peräkkäisissä lohkoissa samaa laskentayksikköä käyttäen aina siihen asti, että suoritus jää odotustilaan esimerkiksi muistiviiveen tai laitteistopyynnön vuoksi. Esimerkiksi toisen tason (L2) välimuistista haun epäonnistumisen voidaan katsoa keskeyttävän suorituksen. [HPGA03]. Lohkomuotoisen suorituksen etuna on se, että suurin joutokäynti vältetään, kun liukuhihna vaihtaa suoritettavaa säiettä ison viiveen edessä. Toisaalta, koska liukuhihna suorittaa vain yhtä säiettä, kaikkea käskytason rinnakkaisuutta ei pystytä aina hyödyntämään ja uuden säikeen käynnistys voi tuoda jonkin verran lisää joutokäyntiä. Lisäksi suorittimen suoritusaika ei jakaannu tasaisesti eri säikeiden välille eikä säikeiden suorituskaan etene tasaisesti. [HPGA03]. Limittäinen monisäikeisyys Limittäisessä monisäikeistyksessä laskentayksikön laskentakapasiteetti jaetaan esimerkiksi tasavälein säielokeroiden kesken, kuten luvussa 2.3.1 esitetään. Mikäli säie on viiveen takia odotustilassa, sen suoritusvuoro voidaan myös tarvittaessa ohittaa. Tällainen limittäinen monisäikeisyys eliminoi kokonaan joutokäynnin liukuhihnalla, vaikkei se pysty täyttämään superskalaaria liukuhihnaa vielä kokonaan joka ajan hetkellä. Toteutus on hieman 27