&Notepad

Will Crichton – 9. syyskuuta 2018
Minua vaivaa ilmaisu ”järjestelmäohjelmointi”. Minusta se on aina tuntunut yhdistävän tarpeettomasti kaksi ajatusta: matalan tason ohjelmointi (koneen toteutuksen yksityiskohtien käsitteleminen) ja järjestelmäsuunnittelu (monimutkaisen joukon toisiinsa liittyvien komponenttien luominen ja hallinta). Miksi näin on? Kuinka kauan tämä on ollut totta? Ja mitä voisimme saavuttaa määrittelemällä järjestelmien idean uudelleen?

1970-luku: Improving on assembly

Matkustetaanpa takaisin nykyaikaisten tietokonejärjestelmien alkulähteille ymmärtämään, miten termi kehittyi. En tiedä, kuka ilmauksen alun perin keksi, mutta hakujeni perusteella voidaan päätellä, että vakavat ponnistelut ”tietokonejärjestelmien” määrittelemiseksi alkoivat noin 70-luvun alussa. Teoksessa Systems Programming Languages (Bergeron1 et al. 1972) kirjoittajat sanovat:

Järjestelmäohjelma on integroitu joukko aliohjelmia, jotka yhdessä muodostavat kokonaisuuden, joka on suurempi kuin osiensa summa, ja jotka ylittävät jonkun koon ja/tai monimutkaisuuden kynnysarvon. Tyypillisiä esimerkkejä ovat moniohjelmointi-, käännös-, simulointi-, tiedonhallinta- ja ajanjakojärjestelmät. Seuraavassa on osittainen joukko ominaisuuksia,joista osa esiintyy muissakin kuin järjestelmissä, eikä kaikkien tarvitse olla läsnä tietyssä järjestelmässä.

  1. Ratkaistava ongelma on luonteeltaan laaja ja koostuu monista ja yleensä varsin erilaisista osaongelmista.
  2. Järjestelmäohjelmaa käytetään todennäköisesti tukemaan muita ohjelmistoja ja sovellusohjelmia, mutta se voi olla myös täydellinen sovelluspaketti itsessään.
  3. Se on suunniteltu jatkuvaan ”tuotantokäyttöön” eikä niinkään kertaratkaisuksi yksittäiseen sovellusongelmaan.
  4. Se todennäköisesti kehittyy jatkuvasti tukemiensa ominaisuuksien määrän ja tyyppien osalta.
  5. Järjestelmäohjelma vaatii tiettyä kurinalaisuutta tai rakennetta sekä moduulien sisällä että niiden välillä (ts. ”kommunikaatio”), ja sen on yleensä suunnitellut ja toteuttanut useampi kuin yksi henkilö.

Tämä määritelmä on melko sopiva – tietokonejärjestelmät ovat laajamittaisia, pitkäikäisiä ja ajassa muuttuvia. Kuitenkin, vaikka tämä määritelmä on suurelta osin kuvaileva, keskeinen ajatus paperissa on määräävä: se puoltaa matalan tason kielten erottamista järjestelmäkielistä (tuolloin assemblerin ja FORTRANin vastakkainasettelu).

Järjestelmäohjelmointikielen tavoitteena on tarjota kieli, jota voidaan käyttää ilman tarpeetonta huolta ”bittien vääntelyyn” liittyvistä näkökohdista, mutta joka silti tuottaa koodia, joka ei ole huomattavasti huonompaa kuin se, joka on luotu käsin. Tällaisessa kielessä olisi yhdistettävä korkean tason kielten tiiviys ja luettavuus sekä tila- ja aikatehokkuus ja kyky ”päästä käsiksi” kone- ja käyttöjärjestelmäominaisuuksiin, jotka assembler-kieli tarjoaa. Suunnittelu-, kirjoitus- ja virheenkorjausaika olisi minimoitava aiheuttamatta tarpeetonta ylikuormitusta järjestelmän resursseille.

Samanaikaisesti CMU:n tutkijat julkaisivat BLISS: A Language for Systems Programming (Wulf et al. 1972), ja kuvailivat sitä seuraavasti:

Viittaamme BLISS:iin ”implementointikielenä” (implementation language), vaikkakin myönnämme, että termi on jokseenkin epäselvä, koska oletettavasti kaikkia tietokonekieliä käytetään jonkin asian toteuttamiseen. Meille ilmaisu tarkoittaa yleiskäyttöistä, korkeamman tason kieltä, jossa pääpaino on asetettu tiettyyn sovellukseen, nimittäin suurten, tuotanto-ohjelmistojärjestelmien kirjoittamiseen tietylle koneelle. Erikoiskielet, kuten kääntäjä-kääntäjät, eivät kuulu tähän kategoriaan, emmekä välttämättä oleta, että näiden kielten on oltava koneriippumattomia. Painotamme määritelmässämme sanaa ”toteutus” emmekä ole käyttäneet sanoja kuten ”suunnittelu” ja ”dokumentointi”. Emme välttämättä odota, että toteutuskieli olisi sopiva väline suuren järjestelmän alkuperäisen suunnittelun ilmaisemiseen tai yksinomaan järjestelmän dokumentointiin. Sellaiset käsitteet kuin koneriippumattomuus, suunnittelun ja toteutuksen ilmaiseminen samalla notaatiolla, itsedokumentointi ja muut ovat selvästi toivottavia tavoitteita, ja ne ovat kriteerejä, joiden perusteella arvioimme eri kieliä.

Tekijät asettavat vastakkain ajatuksen ”toteutuskielestä”, joka on korkeamman tason kuin assembleri, mutta alemman tason kuin ”suunnittelukieli”. Tämä vastustaa edellisessä artikkelissa esitettyä määritelmää, jossa puolletaan sitä, että järjestelmän suunnittelulla ja järjestelmän toteuttamisella pitäisi olla erilliset kielet.

Kumpikin näistä artikkeleista on tutkimustuloksia tai puolestapuhujia. Viimeinen tarkasteltava merkintä (myös vuodelta 1972, joka oli tuottelias vuosi!) on Systems Programming (Donovan 1972), opetusteksti systeemiohjelmoinnin oppimista varten.

Mitä on systeemiohjelmointi? Saatat kuvitella tietokoneen jonkinlaiseksi pedoksi, joka tottelee kaikkia käskyjä. On sanottu, että tietokoneet ovat pohjimmiltaan metallista tehtyjä ihmisiä tai päinvastoin, ihmiset ovat lihasta ja verestä tehtyjä tietokoneita. Kun kuitenkin lähestymme tietokoneita, huomaamme, että ne ovat pohjimmiltaan koneita, jotka noudattavat hyvin tarkkoja ja alkeellisia ohjeita. Tietokoneiden alkuaikoina ihmiset kommunikoivat niiden kanssa kytkimillä, jotka merkitsivät alkeellisia ohjeita. Pian ihmiset halusivat antaa monimutkaisempia ohjeita. He halusivat esimerkiksi pystyä sanomaan, että X = 30 * Y; jos Y = 10, mikä on X? Nykyiset tietokoneet eivät ymmärrä tällaista kieltä ilman järjestelmäohjelmia. Järjestelmäohjelmat (esim. kääntäjät, lataajat, makroprosessorit, käyttöjärjestelmät) kehitettiin, jotta tietokoneet vastaisivat paremmin käyttäjiensä tarpeita. Lisäksi ihmiset halusivat enemmän apua ohjelmiensa laatimisen mekaniikassa.

Pidän siitä, että tämä määritelmä muistuttaa meitä siitä, että järjestelmät palvelevat ihmisiä, vaikka ne ovatkin vain infrastruktuuria, joka ei ole suoraan alttiina loppukäyttäjälle.

1990-luku: Skriptauksen nousu

70- ja 80-luvuilla näyttää siltä, että useimmat tutkijat näkivät järjestelmäohjelmoinnin yleensä vastakohtana assembly-ohjelmoinnille. Järjestelmien rakentamiseen ei yksinkertaisesti ollut muita hyviä työkaluja. (En ole varma, missä Lisp oli tässä kaikessa? Yksikään lukemistani lähteistä ei maininnut Lispiä, vaikka olen hämärästi tietoinen siitä, että Lisp-koneita oli olemassa, joskin vain lyhyen aikaa.)

90-luvun puolivälissä ohjelmointikielissä tapahtui kuitenkin suuri muutos, kun dynaamisesti tyypitetyt skriptikielet yleistyivät. Bashin kaltaisten aikaisempien komentosarjakomentojärjestelmien pohjalta Perlin (1987), Tcl:n (1988), Pythonin (1990), Rubyn (1995), PHP:n (1995) ja Javascriptin (1995) kaltaiset kielet löysivät tiensä valtavirtaan. Tämä huipentui vaikutusvaltaiseen artikkeliin ”Scripting: Higher Level Programming for the 21st Century” (Ousterhout 1998). Siinä artikuloitiin ”Ousterhoutin kahtiajako” ”järjestelmäohjelmointikielten” ja ”skriptiohjelmointikielten” välillä.

Skriptiohjelmointikielet on suunniteltu erilaisiin tehtäviin kuin järjestelmäohjelmointikielet, ja tämä johtaa kielten perustavanlaatuisiin eroihin. Järjestelmäohjelmointikielet on suunniteltu tietorakenteiden ja algoritmien rakentamiseen tyhjästä alkaen tietokoneen alkeellisimmista elementeistä, kuten muistin sanoista. Käsikirjoituskielet sen sijaan on suunniteltu liimaamiseen: niissä oletetaan, että on olemassa joukko tehokkaita komponentteja, ja ne on tarkoitettu ensisijaisesti komponenttien yhdistämiseen toisiinsa. Järjestelmäohjelmointikielet ovat vahvasti tyypiteltyjä monimutkaisuuden hallitsemiseksi, kun taas komentosarjakielet ovat tyypittömiä komponenttien välisten yhteyksien yksinkertaistamiseksi ja nopean sovelluskehityksen mahdollistamiseksi. Useat viimeaikaiset suuntaukset, kuten nopeammat koneet, paremmat komentosarjakielet, graafisten käyttöliittymien ja komponenttiarkkitehtuurien merkityksen kasvu sekä Internetin kasvu, ovat lisänneet huomattavasti komentosarjakielten soveltuvuutta.

Teknisellä tasolla Ousterhout vertasi komentosarjakieliä ja järjestelmäohjelmointiohjelmointikieliä toisiinsa tyypinvarmistuksen ja käskyjä per lauseke -akselilla, kuten edellä on esitetty. Suunnittelutasolla hän luonnehti kummankin kieliluokan uusia rooleja: systeemiohjelmointi on komponenttien luomista varten ja skriptaaminen niiden yhteen liimaamista varten.

Tänään samoihin aikoihin myös staattisesti tyypitetyt, mutta roskien keräilyyn perustuvat kielet alkoivat saada suosiota. Javasta (1995) ja C#:sta (2000) tuli nykyisin tuntemiamme titaaneja. Vaikka näitä kahta ei perinteisesti pidetä ”järjestelmäohjelmointikielinä”, niitä on käytetty monien maailman suurimpien ohjelmistojärjestelmien suunnittelussa. Ousterhout jopa nimenomaisesti mainitsi: ”Nyt muotoutuvassa Internet-maailmassa Javaa käytetään järjestelmäohjelmointiin.”

2010-luku: Rajat hämärtyvät

Viime vuosikymmenellä raja skriptikielten ja järjestelmäohjelmointikielten välillä on alkanut hämärtyä. Dropboxin kaltaiset yritykset pystyivät rakentamaan yllättävän suuria ja skaalautuvia järjestelmiä pelkällä Pythonilla. Javascriptillä renderöidään reaaliaikaisia, monimutkaisia käyttöliittymiä miljardeissa verkkosivuissa. Asteittainen tyypitys on yleistynyt Pythonissa, Javascriptissä ja muissa skriptikielissä, mikä mahdollistaa siirtymisen prototyyppikoodista tuotantokoodiin lisäämällä asteittain staattista tyyppitietoa.

Samaan aikaan massiiviset tekniset resurssit, jotka on käytetty JIT-kääntäjiin sekä staattisten kielten (esim. Javan HotSpot) että dynaamisten kielten (esim. Luan LuaJIT, Javaskriptin V8, Pythonin PyPy) JIT-kääntäjiin, ovat tehneet niiden suorituskyvystä kilpailukykyisen perinteisten systeemiohjelmointikielten (C, C++) kanssa. Suuret hajautetut järjestelmät, kuten Spark, on kirjoitettu Scala2-kielellä. Uudet ohjelmointikielet, kuten Julia, Swift ja Go, jatkavat suorituskyvyn rajojen pidentämistä roskakorjatuissa kielissä.

Paneelissa nimeltä Systems Programming in 2014 and Beyond esiintyivät suurimmat mielet nykypäivän itsestään selvien systeemikielten takana: Bjarne Stroustrup (C++:n luoja), Rob Pike (Go:n luoja), Andrei Alexandrescu (D-kehittäjä) ja Niko Matsakis (Rust-kehittäjä). Kun heiltä kysyttiin ”mikä on järjestelmäohjelmointikieli vuonna 2014”, he vastasivat (muokattu transkriptio):

  • Niko Matsakis: Asiakaspuolen sovellusten kirjoittaminen. Täydellinen vastakohta sille, mihin Go on suunniteltu. Näissä sovelluksissa on suuria latenssitarpeita, suuria tietoturvavaatimuksia, paljon vaatimuksia, jotka eivät tule esiin palvelinpuolella.
  • Bjarne Stroustrup: Stroustrup: Järjestelmäohjelmointi tuli alalta, jossa piti olla tekemisissä laitteistojen kanssa, ja sitten sovellukset muuttuivat monimutkaisemmiksi. On käsiteltävä monimutkaisuutta. Jos sinulla on merkittäviä resurssirajoitteita, olet järjestelmäohjelmoinnin alalla. Jos tarvitaan hienojakoisempaa hallintaa, ollaan myös järjestelmäohjelmoinnin piirissä. Rajoitukset määräävät, onko kyseessä järjestelmäohjelmointi. Onko muisti loppumassa? Onko aika loppumassa?
  • Rob Pike: Pike: Kun julkistimme Go:n, kutsuimme sitä järjestelmäohjelmointikieleksi, ja hieman kadun sitä, koska monet ihmiset olettivat sen olevan käyttöjärjestelmien kirjoituskieli. Meidän olisi pitänyt kutsua sitä palvelinkirjoituskieleksi, jollaisena me oikeastaan ajattelimme sitä. Nyt ymmärrän, että kyseessä on pilvi-infrastruktuurikieli. Toinen määritelmä järjestelmäohjelmoinnille on asiat, jotka kulkevat pilvessä.
  • Andrei Alexandrescu: Minulla on muutama lakmus-testi sen tarkistamiseksi, onko jokin kieli järjestelmäohjelmointikieli. Järjestelmäohjelmointikielen pitää pystyä antamaan sinulle mahdollisuus kirjoittaa siihen oma muistin allokaattori. Sinun pitäisi pystyä väärentämään numero osoittimeksi, koska niin laitteisto toimii.

Onko järjestelmäohjelmoinnissa sitten kyse suuresta suorituskyvystä? Resurssirajoituksista? Laitteiston hallinnasta? Pilvi-infrastruktuurista? Vaikuttaa laajasti ottaen siltä, että C:n, C++:n, Rustin ja D:n kategoriaan kuuluvat kielet eroavat toisistaan koneesta abstraktiotasonsa perusteella. Nämä kielet paljastavat taustalla olevan laitteiston yksityiskohtia, kuten muistin allokaation/asettelun ja hienojakoisen resurssienhallinnan.

Toinen tapa ajatella asiaa: kun sinulla on tehokkuusongelma, kuinka paljon vapautta sinulla on ratkaista se? Matalan tason ohjelmointikielissä on se hieno puoli, että kun tunnistat tehottomuuden, on sinun vallassasi poistaa pullonkaula valvomalla huolellisesti koneen yksityiskohtia. Vektorisoi tämä käsky, muuta tietorakenteen kokoa, jotta se pysyy välimuistissa, ja niin edelleen. Samalla tavalla kuin staattiset tyypit antavat enemmän varmuutta3 kuten ”nämä kaksi asiaa, joita yritän lisätä, ovat varmasti kokonaislukuja”, matalan tason kielet antavat enemmän varmuutta siitä, että ”tämä koodi suoritetaan koneessa niin kuin määrittelin.”

Tulkattujen kielten optimointi on sitä vastoin ehdoton viidakko. On uskomattoman vaikeaa tietää, suorittaako runtime koodisi johdonmukaisesti odottamallasi tavalla. Tämä on täsmälleen sama ongelma kuin automaattisesti rinnakkaistuvissa kääntäjissä – ”automaattinen vektorointi ei ole ohjelmointimalli” (ks. The story of ispc). Se on kuin kirjoittaisi rajapinnan Pythonilla ja ajattelisi: ”No, toivon todellakin, että se, joka kutsuu tätä funktiota, antaa minulle int:n.”

Tänään:

Tästä pääsen takaisin alkuperäiseen murheeseeni. Se, mitä monet kutsuvat järjestelmäohjelmoinniksi, on minusta vain matalan tason ohjelmointia – koneen yksityiskohtien paljastamista. Mutta entä sitten järjestelmät? Palautetaan mieleen vuoden 1972 määritelmämme:

  1. Ratkaistava ongelma on luonteeltaan laaja ja koostuu monista ja yleensä varsin erilaisista osaongelmista.
  2. Järjestelmäohjelmaa käytetään todennäköisesti tukemaan muita ohjelmistoja ja sovellusohjelmia, mutta se voi olla myös itse kokonainen sovelluspaketti.
  3. Se on suunniteltu jatkuvaan ”tuotantokäyttöön” eikä niinkään kertaratkaisuksi yksittäiseen sovellusongelmaan.
  4. Se todennäköisesti kehittyy jatkuvasti tukemiensa ominaisuuksien määrän ja tyyppien osalta.
  5. Järjestelmäohjelma vaatii tiettyä kurinalaisuutta tai rakennetta sekä moduulien sisällä että moduulien välillä (eli ”kommunikaatiota”), ja sen on tavallisesti suunnitellut ja toteuttanut useampi kuin yksi henkilö.

Nämä tuntuvat paljon enemmän ohjelmistoteknisiltä kysymyksiltä (modulaarisuus, uudelleenkäyttö, koodin evoluutio) kuin matalan tason suorituskykyongelmilta. Mikä tarkoittaa, että mikä tahansa ohjelmointikieli, joka priorisoi näiden ongelmien ratkaisemisen, on järjestelmäohjelmointikieli! Se ei silti tarkoita, että jokainen kieli on järjestelmäohjelmointikieli. Dynaamiset ohjelmointikielet ovat luultavasti vielä kaukana systeemikielistä, koska dynaamiset tyypit ja idiomit kuten ”pyydä anteeksi, älä lupaa” eivät edistä hyvää koodin laatua.

Mitä tämä määritelmä sitten tuo meille? Tässä on kuuma näkemys: funktionaaliset kielet kuten OCaml ja Haskell ovat paljon systeemilähtöisempiä kuin matalan tason kielet kuten C tai C++. Kun opetamme järjestelmäohjelmointia perustutkintoa suorittaville opiskelijoille, meidän pitäisi sisällyttää siihen funktionaalisen ohjelmoinnin periaatteita, kuten muuttumattomuuden arvo, rikkaiden tyyppijärjestelmien vaikutus käyttöliittymäsuunnittelun parantamiseen ja korkeamman asteen funktioiden hyödyllisyys. Kouluissa pitäisi opettaa sekä järjestelmäohjelmointia että matalan tason ohjelmointia.

Onko järjestelmäohjelmoinnin ja hyvän ohjelmistotekniikan välillä eroa? Ei oikeastaan, mutta ongelma on se, että ohjelmistotekniikkaa ja matalan tason ohjelmointia opetetaan usein erillään toisistaan. Vaikka useimmat ohjelmistotekniikan kurssit ovat yleensä Java-keskeisiä ”kirjoita hyvät rajapinnat ja testit”, meidän pitäisi opettaa opiskelijoille myös sitä, miten suunnitella järjestelmiä, joilla on merkittäviä resurssirajoitteita. Ehkä kutsumme matalan tason ohjelmointia ”järjestelmiksi”, koska monet mielenkiintoisimmista ohjelmistojärjestelmistä ovat matalan tason järjestelmiä (esim. tietokannat, verkot, käyttöjärjestelmät jne.). Koska matalan tason järjestelmillä on monia rajoitteita, ne vaativat suunnittelijoilta luovaa ajattelua.

Toinen kehys on, että matalan tason ohjelmoijien pitäisi pyrkiä ymmärtämään, mitä järjestelmäsuunnittelun ideoita voitaisiin mukauttaa nykyaikaisen laitteiston todellisuutta vastaaviksi. Mielestäni Rust-yhteisö on ollut tässä suhteessa erittäin innovatiivinen tarkastelemalla, miten hyviä ohjelmistosuunnittelun/toiminnallisen ohjelmoinnin periaatteita voidaan soveltaa matalan tason ongelmiin (esim. futuurit, virheenkäsittely tai tietysti muistin turvallisuus).

Yhteenvetona totean, että sitä, mitä kutsumme ”systeemiohjelmoinniksi”, pitäisi mielestäni kutsua ”matalan tason ohjelmoinniksi”. Tietojärjestelmien suunnittelu alana on liian tärkeä, jotta sillä ei olisi omaa nimeä. Näiden kahden ajatuksen selkeä erottaminen toisistaan antaa enemmän käsitteellistä selkeyttä ohjelmointikielten suunnittelun tilaan, ja se myös avaa oven oivallusten jakamiselle näiden kahden tilan välillä: miten voimme suunnitella järjestelmän koneen ympärille ja päinvastoin?

Suoraathan kommenttisi postilaatikkooni osoitteeseen [email protected] tai Hacker Newsiin.

  1. Viileä fakta: kaksi tämän artikkelin kirjoittajista, R. Bergeron ja Andy Van Dam, ovat grafiikkayhteisön perustajajäseniä ja perustajina on myös suuri osa SIGGRAPH-tiedeprojektin suunnittelusta. Osa jatkuvaa kaavaa, jossa grafiikan tutkijat näyttävät suuntaa järjestelmäsuunnittelussa, vrt. GPGPU:n synty.

  2. Pakollinen linkki Scalabilityyn! Mutta millä KUSTANNUKSELLA?.

  3. Itse asiassa staattiset tyypit ovat 100% takuu (tai rahat takaisin), mutta käytännössä useimmat kielet sallivat jonkin verran Obj.magicia.

Vastaa

Sähköpostiosoitettasi ei julkaista.