TEKNILLINEN KORKEAKOULU Informaatio- ja luonnontieteiden tiedekunta Tietotekniikan tutkinto-ohjelma Moderni muistinhallinta Transaktionaalinen muisti ja rinnakkainen roskienkeruu Kandidaatintyö Tuure Laurinolli Tietotekniikan laitos Espoo 2008
TEKNILLINEN KORKEAKOULU Informaatio- ja luonnontieteiden tiedekunta Tietotekniikan tutkinto-ohjelma KANDIDAATINTYÖN TIIVISTELMÄ Tekijä: Tuure Laurinolli Työn nimi: Moderni muistinhallinta Transaktionaalinen muisti ja rinnakkainen roskienkeruu Päiväys: 2. joulukuuta 2008 Sivumäärä: 7 + 25 Pääaine: Ohjelmistotekniikka Koodi: T3001 Vastuuopettaja: prof. Lauri Savioja Työn ohjaaja: TkT Vesa Hirvisalo Tässä kandidaatintyössä tutkittiin transaktionaalista muistia rinnakkaisuudehallintamenetelmänä ja rinnakkaista roskienkeruuta. Työ perustuu tuoreisiin tutkimustuloksiin transaktionaalisen muistin toteutuksen ja teorian, sekä rinnakkaisen roskienkeruun alueella. Roskienkeruun osalta työn tavoitteena oli lähteä liikkeelle erilaisten keruumenetelmien ja niihin liittyvien ongelmien perusteista ja esittää lukijalle, miten erilaisia ongelmia on ratkaistu. Roskienkeruun osalta työssä keskityttiin rinnakkaisiin menetelmiin, jotka ovat joko tuotantokäytössä tai tulossa tuotantokäyttöön. Transaktionaalisen muistin osalta taas pyrittiin esittämään muita rinnakkaisuudenhallintakeinoja, sekä antamaan mielikuva niiden ja transaktionaalisen muistin käytöstä esimerkkien kautta. Lisäksi tarkasteltiin transaktionaalisen muistin toteutusstrategioita, ohjelmointitapoja ja yhteyksiä roskienkeruuseen. Avainsanat: rinnakkaisoghjelmointi, muistinhallinta, transaktionaalinen muisti, roskienkeruu Kieli: Suomi i
HELSINKI UNIVERSITY OF ABSTRACT OF TECHNOLOGY BACHELOR'S THESIS Faculty of Information and Natural Sciences Degree Program of Computer Science and Engineering Author: Tuure Laurinolli Title of thesis: Modern memory management Transactional memory and concurrent garbage collection Date: December 2 2008 Pages: 7 + 25 Professorship: Ohjelmistotekniikka Code: T3001 Supervisor: Professor Lauri Savioja Instructor: Dr. Vesa Hirvisalo This work gives an overview of what transactional memory and concurrent and parallel garbage collection are. The work is based on literary research on current developments in transactional memory implementation and theory, and parallel and concurrent garbage collection. On garbage collection the goal of this work is to demonstrate how garbage collection solves the problem of deallocating memory and to present how various garbage collection techniques solve these problems. The work concentrates on garbage collection techniques that are in production use or are expected to enter production use shortly. On transactional memory the goal is to contrast it with other concurrency management techniques and to give the reader an idea of how they are used through concrete examples. The work also explores various transactional memory implementation strategies and programming patterns related to transactional memory, and the connection of transactional memory to garbage collection. Keywords: Language: concurrent programming, parallel programming, memory management, transactional memory, garbage collection Finnish ii
Alkulause Kiitokset ohjaajalleni, Vesa Hirvisalolle, mielenkiintoisesta aiheesta ja ohjauksesta tutkimuksen harhailtua. Espoossa 2. joulukuuta 2008 Tuure Laurinolli iii
Käytetyt lyhenteet CAS CCR DSTM GHC GC MCAS HTM STM Compare And Swap; Atominen ehdollinen korvauskäsky Conditional Critical Section; Ehdollinen kriittinen osio Dynamic STM; STM, jossa käytettävät muistialueet eivät ole etukäteen määriteltyjä Glasgow Haskell Compiler; Haskell-kielen kääntäjä Garbage Collection; Roskienkeruu Multiword CAS; Monen sanan atominen ehdollinen korvauskäsky Hardware Transactional Memory; Rautapohjainen transaktionaalinen muisti Software Transactional Memory; Ohjelmallinen transaktionaalinen muisti TM Transactional Memory; Transaktionaalinen muisti iv
Sisältö Alkulause iii Käytetyt lyhenteet iv 1 Johdanto 1 2 Muistinhallinta 3 2.1 Yleistä................................. 3 2.2 Roskienkeruu............................. 4 2.3 Roskienkeruun toteutus........................ 5 2.4 Rinnakkaisuus ja roskienkeruu.................... 7 3 Rinnakkaisuuden hallinta 10 3.1 Kriittiset osiot............................. 10 3.2 Lukot................................. 11 3.3 Lukottomat algoritmit........................ 12 3.4 Transaktionaalinen muisti...................... 13 4 Transaktionaalinen toteutus ja käyttö 15 4.1 Transaktionaalinen koodi....................... 15 4.2 Transaktionaalisen muistin toteutus................. 16 4.3 Transaktionaalisen muistin hyödyntäminen sovelluskoodissa.... 17 4.4 Transaktionaalinen muisti ja roskienkeruu............. 18 v
5 Yhteenveto 20 Kirjallisuutta 22 vi
Luku 1 Johdanto Muistinhallinta on vaikea ja tärkeä ongelma. Tietokonejärjestelmän kannalta on tärkeää, että eri prosessit pääsevät lukemaan ja kirjoittamaan vain omaa muistiaan. Sovellusohjelmoijan kannalta taas on tärkeää, että sovellus varaa ja vapauttaa muistia oikein. Järjestelmätason prosessien erotus on nykyään ratkaistu virtuaalimuistilla, jonka käyttöjärjestelmä ja rauta yhteistyössä toteuttavat. Virtuaalimuisti tarkoittaa jokaiselle prosessille esitettävää omaa yksityistä muistiavaruutta, johon muut prosessit eivät pääse vaikuttamaan. Virtuaalimuistijärjestelmä myös erottaa prosessien virtuaaliset osoiteavaruudet fyysisestä osoiteavaruudesta. Sovellusohjelman muistinvapautusongelman taas pääosin ratkaisee siirtyminen moderniin ohelmointikieleen, jonka ominaisuuksiin kuuluu roskienkeruu. Roskienkeruujärjerjestelmä vapauttaa automaattisesti muistin, kun siihen ei enää viitata ohjelmasta. Viime aikoina roskienkeruun sisältävien korkean tason ohjelmointikielten käyttö on yleistynyt huomattavasti, mutta roskienkeruu on silti myös aktiivinen tutkimusalue. Viime aikoina myös prosessorit ovat kehittyneet pikemminkin suoritusydinten määrässä kuin yksittäisten ytimien suoritusnopeudessa. Erillisten suoritusytimien koko laskentatehon hyödyntämiseen tarvitaan joko erillisiä prosesseja tai rinnakkaisohjelmointia. Erillisten yksisäikeisten prosessien tapauksessa olemassaolevat muistinhallintatekniikat riittävät, eivätkä keskenään kommunikoimattomat prosessit myöskään tarvitse synkronointia kommunikaation vuoksi. Tutkimuksen kannalta erillisiä prosesseja kiinnostavampaa onkin rinnakkaisohjelmointi. Tässä työssä rajoitun tarkastelemaan jaetun muistin rinnakkaisohjelmointia (shared memory multiprocessing). Luvussa 2 tarkastelen muistinhallintaa käyttöjärjestelmän ja ohjelmointikielen ajonaikaisen ympäristön näkökulmasta. Esittelen perinteisen virtuaalimuistin ja roskienkeräyksen, sekä roskienkeräyksen parem- 1
LUKU 1. JOHDANTO 2 paan rinnakkaistamiseen tähtääviä algoritmeja. Luvussa 3 tarkastelen rinnakkaisuuden hallintaa ohjelmoijan näkökulmasta. Esittelen rinnakkaisuudenhallintamenetelmistä lyhyesti lukkopohjaiset kriittiset osiot ja lukottomat algoritmit, sekä transaktionaalisen muistin. Luvussa 4 tarkastelen transaktionaalisen muistin toteutuksia ja vaatimuksia sen käytölle. Luku 5 on yhteenveto.
Luku 2 Muistinhallinta 2.1 Yleistä Nykyään yleisimmät käyttöjärjestelmät käyttävät muistinhallintaan virtuaalimuistia (virtual memory). Virtuaalimuistin toiminnan ymmärtäminen on oleellista, koska käyttöjärjestelmän päällä pyörivien ohjelmien muistinhallinta väkisinkin rakentuu virtuaalimuistin päälle. Virtuaalimuistin perusteista on olemassa runsaasti kirjallisuutta, esimerkiksi Tanenbaumin (2001) perusteos käyttöjärjestelmistä Modern operating systems. Seuraavissa kappaleissa esittelen virtuaalimuistin perusteet. Viimeisessä kappaleessa esittelen myös viime vuosien tutkimusta. Virtuaalimuistijärjestelmässä kullakin prosessilla on oma virtuaalinen osoiteavaruus, josta ne voivat varata muistia. Prosessin virtuaaliosoiteavaruuden käytössä olevat osat kuvataan järjestelmän fyysiseen osoiteavaruuteen, käytännössä lähinnä fyysiseen muistiin. Virtuaaliosoitteiden muuntamisesta fyysisiksi osoitteiksi huolehtii prosessorin muistinhallintayksikkö yhdessä käyttöjärjestelmän kanssa. Muistinhallintayksikkö esimerkiksi kutsuu käyttöjärjestelmän selvittämään tilanteen, mikäli prosessi yrittää käyttää muistiosoitetta, jolle ei ole kuvausta (mapping) fyysiseen muistiin. Virtuaalimuisti toteutetaan yleensä muistisivujen (memory page) avulla. Sivulla tarkoitetaan tietyn kokoista osoiteavaruuden aluetta, jonka tarkkuudella virtuaalimuistin kuvaukset fyysiseen muistiin ovat määriteltävissä. Käytännössä muistisivut ovat kooltaan kahden potensseja, jolloin kokonaisen muistiosoitteen muuntaminen vastaavan sivun osoitteeksi on helppoa osoitteen alimmat bitit nollaamalla. Osoitekuvausten lisäksi muistisivuihin liittyy usein myös muita attribuutteja, kuten muistin käyttötapa (luku/kirjoitus/suoritus). Virtuaalimuistin sivuja vastaavat fyysisen muistin sivukehykset. Kun virtuaalimuistin sivu on käytös- 3
LUKU 2. MUISTINHALLINTA 4 sä, sijaitsee sen sisältö joko jossain fyysisen muistin sivukehyksessä (page frame) tai poissa fyysisestä muistista taustamuistissa (backing store), kuten esimerkiksi kiintolevyllä. Tyypillisesti virtuaalimuistiin liittyy myös mahdollisuus jakaa fyysisiä sivuja eri prosessien välillä, eli jaettu muisti (shared memory). Jaettu muisti tarkoittaa, että eri prosessien osoiteavaruudessa olevat osoitteet kuvataan samoihin fyysisiin sivuihin. Vain luettavissa olevaa jaettua muistia käytetään esimerkiksi jaettujen kirjastojen (shared library) ohjelmakoodin jakamiseen useiden prosessien välillä fyysisen muistin säästämiseksi. (Tanenbaum, 2001) Prosessit voivat yleensä myös pyytää käyttöjärjestelmältä jaetun muistin alueita, joiden kautta ne voivat kommunikoida keskenään prosessin sisäisten säikeiden tapaan (IEEE, 2004). Näin voidaan myös prosessien välillä säikeiden tapaan käyttää jaettua muistia kommunikaatiokanavana. Tutkimuskohteita virtuaalimuistissa ovat esimerkiksi sivunkorvausalgoritmit (Paajanen, 2007). Käyttöjärjestelmässä sivunkorvausalgoritmi tekee päätöksen siitä, mikä muistisivu poistetaan (is evicted) taustamuistiin, kun prosessi haluaa käyttää sellaista sivua, jonka data ei ole muistissa, ja tyhjiä sivukehyksiä ei ole. Lisäksi virtuaalimuistiin liityviä suojaustoimintoja voidaan käyttää esimerkiksi roskienkeruun tehostamiseen (Click et al., 2005). 2.2 Roskienkeruu Yleisesti roskienkeruulla tarkoitetaan ohjelman käyttämien muistiobjektien automaattista vapauttamista. Perinteisesti ohjelmointikielissä on kaksi erillistä tapaa varata muistia: kutsupino (call stack) ja keko (heap). Pinosta varattu muisti vapautuu automaattisesti proseduurikutsun palatessa, ja keosta varattu muisti on käytettävissä, kunnes ohjelman suoritus päättyy. Koska muistia on rajallisesti, voidaan keosta varattua muistia myös vapauttaa. Perinteisesti muistin varaamisen ja vapauttamisen on tehnyt ohjelmoija erityisillä kirjastofunktioilla. Muistin vapauttaminen ohjeman suorituksen aikana on tällöin kokonaan ohjelmoijan vastuulla. Muistin vapauttamiseen liittyy kahdenlaisia ohjelmointivirheitä: roikkuva osoitin (dangling pointers) ja muistivuoto. Roikkuva osoiting tarkoittaa vapautettuun objektiin osoittavaa viitettä, ja muistivuoto objektia, johon ei enää ole viitteitä ohjelmassa, ja jota ei siten voida vapauttaa ohjelman sisältä (Varga, 2006). Mikäli muistia olisi rajattomasti, ei muistia tarvitsisi vapauttaa, eikä edellämainittuja virheluokkia olisi. Roskienkeruu luo illuusion rajattomasta muistista. Illuusio on teoriassa mahdollinen, mikäli ohjelmasta ei koskaan sen suorituksen aikana ole saavutettavissa
LUKU 2. MUISTINHALLINTA 5 enempää muistia kuin on varattavissa, ja kaikki saavuttamattomissa oleva muisti pystytään vapauttamaan. Käytännössä roskienkeräin tutkii ohjelmassa olevia muistiosoituksia ja vapauttaa automaattisesti objektit, jotka eivät enää ole saavutettavissa ohjelmasta. Vapautettu muisti voidaan varata uudelleen myöhemmin suorituksen aikana, jolloin illuusio rajattomasta muistista säilyy. Roskienkeruu määritelmän mukaan eliminoi muistivuodot vapauttamalla ohjelmasta saavuttamattomissa olevan muistin. Myös dangling pointers-ongelma eliminoituu, mikäli ohjelmointikielestä samalla poistetaan mahdollisuus vapauttaa muistia väkisin, sillä roskienkeräin ei vapauta muistia, johon on viitauksia. Roskienkeruun yleiseen problematiikkaan liittyy ero muistin elossaolon (liveness) ja saavutettavuuden (reachability) välillä. Objekti, johon ei ohjelman millään mahdollisella suorituspolulla viitata, on kuollut. Kuollut objekti voi kuitenkin olla periaattessa saavutettavissa, mikäli tarkastellaan vain objektien välisiä viittauksia. Periaatteessa roskienkeräin voisi vapauttaa kaikki kuolleet objektit, mutta ohjelman kaikkien suorituspolkujen tarkastelu on käytännössä mahdotonta. Käytännössä oletetaankin kaikki saavutettavissa roskienkerääjät poistavatkin objekteja vasta, kun niitä ei enää voi saavuttaa. Hertz ja Berger (2005) ovat tutkineet elävyys- ja saavutettavuusoraakkeleihin perustuvan eksplisiittisen muistinhallinnan ja todellisten roskankerääjien suorituskykyä. He toteavat yhteenvedossaan, että roskienkeruu on suorituskyvyltään kilpailukykyinen eksplisiittisen muistinhallinnan kanssa, kunhan muistia on käytettävissä runsaasti. Muistin vapauttamisen lisäksi roskienkerääjä voi tehdä muutakin hyödyllistä. On toteutettu roskienkerääjiä, jotka roskien tuhoamisen sijaan lisäksi tiivistävät (compact) elävät objektit yhteen muistissa. Kun objektit sijaitsevat muistissa peräjälkeen ja loppu muisti on tyhjä, voidaan muistin toteuttaa tehokkaasti siirtämällä vapaan muistin alkuun osoittavaa osoitinta. Muistin tiivistäminen myös poistaa perinteistä vapaiden muistialueiden listaan perustuvaa roskienkerääjää ja muistiallokoijaa vaivaavan muistin sirpaloitumisongelman. Sirpaloitumisella vapaan muistin pilkkoutumista pieniin osiin objektien väliin. Ongelmia sirpaloituminen aiheuttaa allokoinnin yhteydessä; muistia voi olla vapaana paljonkin, mutta siitä ei ole mitään hyötyä, ellei tarpeeksi suurta jatkuvaa muistialuetta löydy. 2.3 Roskienkeruun toteutus Roskienkeruujärjestelmiä on perinteisesti toteutettu kahdella tavalla: viittauslaskennalla (reference counting) ja viittausten seurannalla (tracing). Bacon et al. (2004) esittävät näiden olevan toistensa duaaleja, ja että nykyään kielten toteutuksissa käytetyt, tehokkaat menetelmät ovat poikkeuksetta viitauslaskennan ja
LUKU 2. MUISTINHALLINTA 6 viittausten seurannan hybridejä. Esittelen seuraavaksi lyhyesti perusnemetelmät. Viittausten seuranta on menetelmistä suoraviivaisempi. Sen perustana on suoraan saavutettavuuden käsite. Perusmuodossaan viittausten laskennassa käydään läpi kaikki ohjelman pinoissa ja globaaleissa muuttujissa, eli juurijoukossa (root set) olevat muistiviittaukset transitiivisesti, ja merkataan jokainen näin läpikäyty objekti. Objektit, joita ei merkitä, ovat saavuttamattomissa ohjelmasta, ja ne voidaan vapauttaa. Juurijoukon viittausten transitiivinen sulkeuma ja viitatut objektit muodostavat viittausgraan, joka on heikosti kytketty. Viittauslaskennassa kuhunkin muistiobjektiin liitetään laskuri, jota kasvatetaan aina kun objektiin luodaan uusi viittaus, ja vähennetään aina kun viittaus objektiin häviää. Mikäli vähennyksen jälkeen viittauslaskurin arvo on 0, ei objektiin enää ole viittauksia ohjelmasta, ja se voidaan vapauttaa. Perusmuodossaan viittauslaskenta ei välttämättä löydä kaikkia roskia, mikäli viittausgraa sisältää syklejä. Kun viimeinen viittaus ohjelmasta sykliseen rakenteeseen poistuu, on kaikkien sykliin kuuluvien objektien edelleen yli nollan. Viittauslaskurit eivät myöskään enää voi päivittyä, sillä sykli ei enää ole saavutettavissa ohjelmasta, eikä ohjelma näin ollen voi poistaa syklin sisäisiä viittauksia. Viittauslaskenta tarvitsee parikseen jonkin syklit keräävän menetelmän, mikäli tavoitteena on täydellinen roskienkeruumenetelmä. Varga kertoo gradussaan muutamista olemassaolevista ratkaisuista. Perusideana näissä on etsiä potentiaalisia syklin osia, vähentää kokeellisesti niiden viittauslaskuria, ja tutkia, purkautuuko jokin sykli. Viittausten seurantaan perustuvista kerääjistä on olemassa useita variantteja. Alun perin kerääjät vain poistivat lisäsivät merkkaamattomien objektien käyttämän muistin vapaiden muistialueiden listaan (mark-and-sweep). Myöhemmin on toteutettu myös tiivistäviä ja kopioivia kerääjiä. Kopioiva kerääjä on tiivistävän kerääjän variantti, joka kopioi elävät objektit toiseen muistialueeseen saman muistialueen sijaan. Tiivistämisen suurin ongelma on osoittimien päivittäminen tiivistämisen yhteydessä, kun objekteja siirrellään ympäriinsä muistissa. Kopioiva kerääjä helpottaa tämän osoitinten päivitystä, koska alkuperäisiä objekteja ei ylikirjoiteta. (Varga, 2006) Toinen suuri kehitysaskel ennen rinnakkaisten kerääjien yleistymistä oli sukupolviperustainen keräys. Havaittiin, että suurin osa objekteista kuolee nuorena, ja vanhat objektit yleensä elävät hyvin vanhoiksi (Varga, 2006; Ungar, 1984). Havainnon pohjalta on kehitetty sukupoleviperustaisia roskienkerääjiä (generational garbage collector), joissa muisti jaetaan erillisiin alueisiin eri ikäisiä objekteja varten. Kaikki allokointi tehdään uusien objektien alueella, josta objektit siirtyvät vanhempien objektien alueille, kun selviytyvät tarpeeksi monesta roskienkeruusta (engl. termi tenuring). Tavallisesti tarvittaessa lisää muistia kerätään
LUKU 2. MUISTINHALLINTA 7 vain nuorten objektien alue, jonka sisältämistä objekteista suurin osa on kuollut. Jotta nuoren alueen kerääminen on mahdollista, täytyy ohjelmointikielen ajonaikaisne järjestelmän pitää kirjaa viittauksista vanhalta alueelta uudelle alueelle. Tätä viittausjoukkoa nimitetään englanninkielisessä kirjallisuudessa muistetuksi joukoksi (remembered set). (Varga, 2006) 2.4 Rinnakkaisuus ja roskienkeruu Rinnakkaisuus tarkoittaa roskienkeruun yhteydessä kahta asiaa: ohjelman suorituksen ja roskienkerääjän suorituksen samanaikaisuutta (concurrent garbage collection), ja useampien roskienkeruusäikeiden rinnakkaista suoritusta (parallel garbage collection). Molemmista aiheista on olemassa varsin käytännönläheistä tutkimusta, josta tarkemmin seuraavassa. Käytännön kannalta kiinnostavin tutkimus on (Detlefs et al., 2004), joka esittelee Garbage-Firstroskienkeruumenetelmän (G1). Kyseessä on rinnakkainen ja samanaikainen roskienkerääjä, jossa muisti jaetaan blokkeihin, joista voidaan tarpeen vaatiessa kerätä jokin alijoukko. G1:n nimi tulee siitä, että blokkien kuolleisuusasteesta pidetään kirjaa, ja ensisijaisesti kerätään kokonaan tai lähes kokonaan kuolleita blokkeja. Kerääjä on suurelta osin rinnakkainen, eli ohjelman säikeet (mutator threads) voivat jatkaa suoritusta myös suurimman osan keräykseen kuluvasta ajasta. G1:n päätavoitteena on lyhentää roskienkeruun aiheuttamia suorituskatkoja. Detlefs et al. toteavat kokeissaan, että verrattuna olemassaoleviin Javan roskienkerääjiin, lyhenevät pisimmät suorituskatkot merkittävästi. G1:n läpäisykyky kuitenkin (throughput) on huonompi kuin olemassaolevien kerääjien. Muita oleellisia eroja aikaisempiin kerääjiin on G1:n blokkien runsaasta määrästä johtuva blokkien välisten välisten viittausjoukkojen aiheuttama tilakustannus. Tilakustannusta pyrittiin pienentämään ikäpohjaisella optimoinnilla, jossa allokaattoreiden käytössä olevat, eli nuorimman sukupolven blokit kerätään joka seuraavassa keräyksessä, eikä niistä ulospäin osoittavista viittauksista pidetä kirjaa. Huomattavasti G1:tä muistuttaa GHC-ympäristöön (Glasgow Haskell Compiler) toteutettu rinnakkainen, ei-samanaikainen roskienkerääjä (Marlow et al., 2008). Marlow'n et al Kerääjä perustuu myös keon jakamiseen blokkeihin, mutta blokit ovat pienempiä kuin G1:ssä. Marlow'n et al kerääjässä myöskin ikäpohjaisuus on G1:tä suuremmassa roolissa. Heidän kerääjässään vain viittauksista vanhoista blokeista nuorempiin pidetään erikseen kirjaa, ja sukupolvia on enemmän kuin G1:ssä.
LUKU 2. MUISTINHALLINTA 8 Marlow'n et al rinnakkainen kerääjä skaalautuu prosessorimäärän kasvaessa vaihtelevasti kuormaste riippuen. Parhaimmillaan kahdeksan prosessorin tapauksessa saavutettiin 4.5-kertainen keräysnopeus verrattuna samaan menetelmään yhdellä prosessorilla suoritettuna. Huonoimmillaan kahdeksan prosessorin järjestelmässä kuitenkin jäätiin alle kaksinkertaiseen nopeuteen yksiprosessorijärjestelmään nähden. Tutkimuksessa ei ole kvantitatiivista vertailua muihin keräysmenetelmiin. Click et al. (2005) esittävät rinnakkaisen ja samanaikaisen roskienkeruumenetelmän, joka muistuttaa huomattavasti G1:tä. Heidän menetelmänsä (Pauseless) vaatii rauta- ja käyttöjärjestelmätukea, eikä siten ole sovellettavissa täysin yleisesti. Pauseless on suunniteltu ja toteutettu Azul Systems-yhtiön sisällä heidän moniprosessoriarkkitehtuurilleen, eikä ole suoraan yleisesti sovellettavissa. Arkkitehtuuri sinänsä on kuitenkin kiinnostava, koska se mahdollistaa n. 400 rinnakkaisen välimuistikoherentin suorittimen järjestelmien rakentamisen. Lisäksi suorittimien käskykantaan on tehty erityisesti roskienkeruuta tukevia lisäyksiä. Pauseless pyrkii samanaikaisesti lyhyisiin suorituskatkoihin, mutta mahdollistaa ilmeisesti myös korkean läpäisyn. Samoin kuin G1, Pauseless jakaa muistin lohkoihin. Lohkot vastaavat muistisivuja, mitä hyödynnetään myöhemmin päivitettäessä kopioitujen objektien osoitteita. G1:stä poiketen Pauseless ei ole ikäpohjainen, vaan koko elossa oleva muisti käydään läpi jokaisessa merkkaussyklissä. Mielenkiintoista on samanaikaisen merkkauksen toteutus siten että se ei estä ohjelmasäikeiden suoritusta kuin juurijoukon etsinnän ajaksi. Kukin ohjelmasäie suorittaa merkkauksen ollessa käynnissä jokaisen viittauksen luvun yhteydessä lukumuurin, joka merkkaa viittauksen luetuksi ja lisää sen keräääjän läpikäytävien viittausten joukkoon, mikäli sitä ei vielä oltu merkattu luetuksi. Näin vältetään ongelma silloin kun ohjelmasäie lukee merkkaamattoman viittauksen muistista ja poistaa sen muistista, mutta säilyttää viittauksen esimerkiksi pinossa. Ilman lukumuuria viitattu objekti olisi merkkausvaiheen lopussa merkkaamaton, ja siten kerättävissä, vaikka siihen olisi viittaus pinosta. Mielenkiintoista on myös laiska viittausten uudelleenohjaus kopiointivaiheen aikana. Erillistä uudelleenohjausvaihetta ei ole, vaan lopullisesti uudelleenohjaukset tehdään seuraavan keruusyklin merkkausvaiheessa. Laiska uudelleenohjaus toimii siten että kopiointivaiheen lähdesivut merkataan suojatuiksi roskienkerääjälle varatulla tasolle. Mikäli ohjelma yrittää käyttää kopioitavaa sivua kesken kopioinnin, tapahtuu suojausvirhe, ja ohjelmasäie suorittaakin kerääjän keskeytyksen. Keskeytyskoodi etsii oikean lähdesivulta oikean forwarding pointerin, ja lukee alkuperäisen sijaan sen. Mikäli kopiointia ei vielä ole suoritettu, se myöskin ensin kopioi objektin. Roskienkeruutason suojausvirheen keskeytys suoritetaan kutsuvassa säikeessä, mutta kohotetuin oikeuksin, joten kallista järjestelmäkutsua ei tarvita.
LUKU 2. MUISTINHALLINTA 9 Pauseless-algoritmia testattiin Sunin, IBM:n ja BEA:n Java-virtuaalikoneiden roskienkerääjiä vastaan. Testinä oli muokattu SpecJBB. Testeissä mitattiin transaktioiden kestoa, eli käytännössä pitkien roskienkeruutaukojen vaikutusta transaktioden kestoon, sillä transaktiot itsessään ovat lähes vakiomittaisia. Testituloksissa Pauseless on selkeästi kilpailijoitaan parempi. Yli 3 ms kestäneitä transaktioita ei Pauselessilla ollut lainkaan, kun lähimmäksi päässeellä BEA:lla niitä oli yli 20%. Lisäksi on huomattava, että BEA:n yli 3 ms kestäneet transaktiot kestivät lähes kaikki vähintään noin 100 ms. On huomioitava, että testitulokset eivät ole helposti toistettavissa, koska ainoa Pauseless-toteutus vaatii erikoista rautaa toimiakseen, ja koska tarkkoja tietoja käytetystä testistä ei ole. Silti tulokset nähdäkseni puhuvat Pauselessin ja sen pohjalla olevan arkkitehtuurin puolesta. Kaikki esitelly rinnakkaisuuteen ja samanaikaisuuteen pyrkivät roskienkeruualgoritmiet perustuvat muistin jakamiseen aiempia sukupolviperustaisia algoritmeja ueampaan osaan. Kaikki myös vaikuttavat onnistuvan tavoitteissaan nopeamman tai vähäkatkoisemman roskienkeruun suhteen. Keräimistä G1 on edelleen kehitysasteella, mutta sen pitäisi olla mukana seuraavassa Sunin Java-virtuaalikoneen versiossa - ainakin se löytyy jo OpenJDK:n versionhallinnasta. Vastaavasti Pauseless on ollut jo vuosia tuotantokäytössä, ja GHC:n kerääjä on mukana GHC:n versiossa 6.10.1.
Luku 3 Rinnakkaisuuden hallinta 3.1 Kriittiset osiot Rinnakkaisohjelmoinnin perusongelma ovat kilpatilanteet (race condition). Kilpatilanne on hieman harhaanjohtava nimitys, jolla yleensä tarkoitetaan mahdollisuutta päätyä kilpaan (race). Kilpaongelma (race hazard) on kuvaavampi, mutta vähän käytetty termi, eikä suomennos ole vakiintunut. Kirjallisuudessa termille on esitetty erilaisia tarkkoja määritelmiä, joita Netzer ja Miller (1992) selvittävät tutkimuksessaan. Tässä luvussa tarkoitan kilvalla Netzerin et al. käyttämää termiä data race. Kilpa on tilanne, jossa säikeiden suoritusjärjestys (scheduling) vaikuttaa ohjelman suorituksen oikeellisuuteen, vaikka säikeiden suorittamien toimintojen suoritusjärjestys itsessään ei ole oikeellisuuden kannalta oleellinen. Esimerkiksi lipunmyyntijärjestelmän varatessa samanaikaisesti paikkoja kahdelle eri asiakkaalle ei lopputuloksen oikeellisuuden kannalta ole merkitystä, missä järjestyksessä paikat valitaan tai mitkä paikat kukin asiakas siten saa, mutta ohjelma ei saa varata samoja paikkoja usealle asiakkaalle. Kilpaongelmien ratkaisuna jaetun muistin rinnakkaisohjelmoinnissa ovat perinteisesti kriittiset osiot (critical section). Kriittisellä osiolla tarkoitetaan sellaista ohjelman osiota, jota vain yksi säie kerrallaan voi suorittaa. Lipunvarausesimerkissä voisi paikkojen poiminta olla kriittinen osio. Mikäli kukin muuten samanaikainen paikkoja varaava säie valitsee paikat yksi kerrallaan, ei kilpaa synny, sillä myöhemmät paikkoja varaavat säikeet näkevät aikaisempien tekemät varaukset. 10
LUKU 3. RINNAKKAISUUDEN HALLINTA 11 3.2 Lukot Kriittisiin osioihin pääsyn kontrollointiin on olemassa useita menetelmiä, joista lukot (mutex, mutual exclusion lock), sekä niiden yleistykset semaforit ja monitorit ovat yleisesti käytössä. Lukkoprimitiivin periaatteena on, että yksi säie kerrallaan voi ottaa lukon (acquire), suorittaa kriittisen osionsa ja vapauttaa sitten lukon (release). Mikäli toinen säie yrittää ottaa lukon samaan aikaan kun se on ensimmäisen hallussa, estyy toisen säikeen suoritus (engl. the other thread blocks) siihen asti kunnes ensimmäinen säie vapauttaa lukon. (Tanenbaum, 2001) Lukot ratkaisevat rinnakkaisen datankäsittelyn ongelman muuntamalla sen peräkkäiseksi. Suorituskykymielessä peräkkäiseen suoritukseen siirtyminen poistaa kaiken rinnakkaisuudesta saatavan hyödyn, ja lisäksi lukkojen käsittely itsessään hidastaa ohjelman suoritusta. Käytännössä voidaan rinnakkaisuutta lisätä pienentämällä lukkojen vaikutusaluetta - puhutaan karkeasta ja hienojakoisesta lukituksesta (coarse-grained ja ne-grained locking). Lipunvarausjärjestelmässä esimerkki karkeasta lukituksesta olisi aiemmin mainittu koko paikkojen valinnan sijoittaminen yhteen kriittiseen osioon. Koska vain paikkojen valinnan ulkopuolinen osa varausprosessista voitaisiin suorittaa rinnakkain, karkea lukitus rajoittaisi saavutettavaa rinnakkaisuutta. Hienojakoinen lukitus taas voisi lipunvarausjärjestelmässä tapahtua siten että jokaiseen paikkaan liittyisi lukko. Säie ottaisi kaikkien haluamiensa paikkojen lukot haltuunsa, merkitsisi paikat varatuksi, ja vapauttaisi lopuksi lukot. Tällöin paikanvaraussäikeet voisivat parhaimmillaan toimia rinnakkain ilman kilpailua lukoista (contention), mikäli ne eivät yrittäisi varata samoja paikkoja. Hienojakoiseen lukitukseen liittyy kuitenkin ongelmia ohjelman suorituksen oikeellisuuden kanssa. Tarkastellaan lipunvarausesimerkin tilannetta, jossa asiakkaat A ja B yrittävät varata kahta paikkaa, kun täsmälleen kaksi paikkaa on vapaana. Mikäli säie A ottaa paikan 1 lukon ja säie B ottaa paikan 2 lukon, ja tämän jälkeen säie B yrittää ottaa paikan 1 lukon ja säie A paikan 2 lukon, päädytään lukkiumaan (deadlock). Lukkiumassa olevia säikeitä ei voi suorittaa, koska ne kaikki odottavat jonkin toisen lukkiumaan kuuluvan säikeen hallussa olevaa lukkoa. Lipunvarausesimerkissä ongelmalta voitaisiin välttyä esimerkiksi ottamalla paikkojen lukot aina paikkojen numerojärjestyksessä (Tanenbaum, 2001, 3.6.4). Aina lukoilla ei kuitenkaan ole luonnollista järjestystä, mikä rajoittaa kriittisten osioiden käyttökelpoisuutta rinnakkaisuudenhallinnassa toteutuksessa. Erityisesti lukkojen käyttö kirjastojen sisällä on ongelmallista, mikäli kirjastot voivat esimerkiksi kutsua toisia kirjastoja ristiin, tai aiheuttaa vastakutsuja (callback) takaisin sovelluksen koodiin. Esimerkiksi Kahden lukkoja sisäisesti käyttävän, risitiin toisiaan kutsuvan kirjastoa johtaa helposti lukkiumaan, jos kirjastot voivat joutua
LUKU 3. RINNAKKAISUUDEN HALLINTA 12 odottamaan kumpikin toistensa lukkoa pitäen samaaan aikaan omaa lukkoaan hallussaan. 3.3 Lukottomat algoritmit Lukkojen aiheuttamia rinnakaisuusrajoituksia on vältetty suunnittelemalla ohjelman käyttämät tietorakenteet siten että niitä voidaan käsitellä rinnakkaisesti ilman lukkoja. Yksinkertaisista tietorakenteista, kuten jonoista on olemassa tehokkaita lukottomia versioita, esimerkiksi Michaelin ja Scottin (1996) lukoton jono. Lukottomien algoritmien toteutuksia on myöskin jo yleisten ohjelmointikielten standardikirjastoissa. Esimerkiksi Java-kielen standardikirjaston java.util.concurrentpaketti sisältää useita kokonaan tai osittain lukottomia rinnakkaisia tietorakenteita (Inc., 2008) ja CAS-semantiikan tarjoavia tyyppejä. Intel on julkaissut C++kielelle vastaavan rinnakkaisohjelmointikirjaston (Corporation, 2008). Monimutkaisten tietorakenteiden manipulointi ilman lukkoja kuitenkin on vaikeaa. Fraser (2004) toteaa, että tällaisia algoritmeja ole juurikaan julkaistu. Monimutkaisten tietorakenteiden manipuloinnin tekee vaikeaksi prosessorien rajoittunut käskykanta. Fraser toteaa erityisesti, että monimutkaisten tietorakenteiden käsittely ja kriittisten osioiden korvaaminen pelkkiä primitiivikäskyjä käyttäen on epäkäytännöllistä (Fraser, 2004, luku 2.4). Hän ehdottaa ratkaisuksi laajempia atomisia muistioperaatioita: MCAS-primitiiviä (Multiword Compare And Swap) ja transaktionaalista muistia. Yksinkertaisten tietorakenteiden tapauksessa erityisesti kyseistä tietorakennetta varten suunnitelluilla lukottomilla algoritmeilla voidaan kuitenkin sopivissa olosuhteissa saavuttaa huomattavasti parempi suorituskyky kuin esimerkiksi transaktionaalisella muistilla (Fraser, 2004, kuva 6.1). Onneksi lipunvarausesimerkin tietorakenne on yksinkertainen. Lukottomasti sen voi toteuttaa atomisen korvauskäskyn (compare and set, CAS) avulla. CASkäskyllä voidaan vaihtaa yhden muistipaikan arvo atomisesti ja ehdollisesti toiseksi. Mikäli muistipaikan nykyinen arvo on odotettu. Mikäli muistipaikan nykyinen arvo poikkeaa odotetusta, CAS-käsky ei korvaa sitä uudella arvolla, vaan arvon poikenneen odotetusta. CAS-käskyillä lipunvarausesimerkki voidaan toteuttaa samaan tapaan kuin hienojakoisella lukituksella, mutta ilman lukkoja. Yrittäessään varata paikan säie yrittää korvata vapaa-statuksen varattu-statuksella. Jos korvaus onnistuu, säie tietää että paikka on varattu sille, ja jos se epäonnistuu, säie tietää paikan olleen jo varattu, ja voi esimerkiksi yrittää uudelleen eri paikalla.
LUKU 3. RINNAKKAISUUDEN HALLINTA 13 3.4 Transaktionaalinen muisti Transaktionaalisen muistin (transactional memory, TM) käsite on peräisin Herlihyn ja Mossin (1993) rautapohjaisesta transaktionaalisesta muistista. Transaktionaalisella muistilla (TM) tarkoitetaan yleisesti useiden muistioperaatioiden suorittamista tietokannoista tutulla tavalla atomisesti ja sarjallistuvasti (atomic, serializable). Atomisuus tarkoitetaan että joko kaikki transaktion sisällä olevat muistioperaatiot suoritetaan, tai mitään ei suoriteta. Sarjallistuvuus puolestaan että transaktiot suoritetaan siten että samanlainen suoritus (execution) saataisiin suorittamalla ne peräkkäin ilman rinnakkaisuutta (Herlihy ja Moss, 1993). Herlihyn transaktionaalisella muistilla lipunvarausesimerkin rinnakkaisuudenhallinta voitaisiin toteuttaa yksinkertaisesti suorittamalla paikkojen vapauden tarkistaminen ja varatuksi merkkaaminen transaktiossa. Joko kaikki varattavat vapaat paikat saadaan merkattua varatuksi, tai sitten transaktio epäonnistuu, ja voidaan yrittää varata uudet paikat. Herlihyn ja Mossin transaktionaalinen muisti ajateltiin toteutettavaksi prosessorin sisällä erikoiskäskyillä. Shavit ja Touitou (1995) toteuttivat vastaavan transaktiojärjestelmän ohjelmallisesti, ja nimesivät sen ohjelmalliseksi transaktionaalisen muistin (software transactional memory, STM). Shavitin ja Touitoun menetelmä rajoittuu kiinteästi määriteltyihin transaktioihin, ja he pitävät menetelmäänsä lähinnä monen sanan CAS-käskynä. Vuonna 2003 Herlihy ym. kehittivät dynaamisen STM:n (Dynamic STM, DSTM), joka toimii sanatason sijaan objektitasolla. Dynaamisen STM:n etuna aikaisempiin staattisiin STM:iin on soveltuvuus linkkipohjaisten rakenteiden, kuten puiden, käsittelyyn (Herlihy et al., 2003). Shavitin ja Touitoun STM:stä ei juurikaan olisi lipunvarauksen toteutuksessa apua, mutta DSTM:llä toteutus olisi samankaltainen kuin Herlihyn ja Mossin TM:llä. Myös erilaisia laajennuksia transaktioiden semantiikkaan on esitetty. Harris ja Fraser (2003) esittävät ehdollisten kriittisten osioiden (conditional critical section, CCR) käyttöä aikaisempien transaktioiden sijaan. Erona aikaisempiin transaktionaalisiin muisteihin on, että transaktio voidaan suorittaa ehdollisesti siten että transaktiota suorittava säie blokkaa (blocks) odottaen ehdon täyttymistä ja suoritetaan automaattisesti ehdon täyttyessä. (Martin et al., 2006) esittelevät kaksi erilaista transaktioiden atomisuuteen liittyvää semantiikkaa, sekä erilaisia ongelmia liittyen kriittisten osioiden korvaamiseen transaktioilla ja transaktioiden ja CCR:ien yhdistelemiseen (composition). Heidän tutkimuksensa jälkeen on julkaistu useita TM:n semantiikkaan pureutuvia tutkimuksia (Smaragdakis et al., 2007; Guerraoui ja Kapalka, 2008; Maessen ja Arvind, 2007). Attiyan (2008) mukaan vakaata teoreettista pohjaa ei kuitenkaan ole. Eräänä transaktionaalisen muistin etuna lukkopohjaisiin kriittisiin osioihin näh-