Ohjelman optimointi

Optimointi voi tapahtua monella tasolla. Tyypillisesti korkeammilla tasoilla on suurempi vaikutus, ja niitä on vaikeampi muuttaa projektin myöhemmässä vaiheessa, ja ne vaativat merkittäviä muutoksia tai täydellisen uudelleenkirjoittamisen, jos niitä on muutettava. Näin ollen optimointi voi tyypillisesti edetä tarkentamalla korkeammalta tasolta matalammalle, jolloin alkuvaiheen parannukset ovat suurempia ja saavutetaan vähemmällä työllä, ja myöhemmät parannukset ovat pienempiä ja vaativat enemmän työtä. Joissakin tapauksissa kokonaissuorituskyky riippuu kuitenkin ohjelman hyvin matalan tason osien suorituskyvystä, ja pienillä muutoksilla myöhäisessä vaiheessa tai matalan tason yksityiskohtien varhaisella huomioon ottamisella voi olla suuri vaikutus. Tyypillisesti tehokkuuteen kiinnitetään jonkin verran huomiota koko projektin ajan – vaikkakin tämä vaihtelee huomattavasti – mutta merkittävää optimointia pidetään usein hienosäätöön liittyvänä toimenpiteenä, joka tehdään myöhään, jos koskaan. Pitkäkestoisissa hankkeissa on tyypillisesti optimointisyklejä, joissa yhden osa-alueen parantaminen paljastaa rajoituksia jollakin muulla osa-alueella, ja näitä optimointisyklejä supistetaan tyypillisesti, kun suorituskyky on hyväksyttävä tai kun parannuksista tulee liian pieniä tai kalliita.

Koska suorituskyky on osa ohjelman määrittelyä – ohjelma, joka on epätavallisen hidas, ei sovellu tarkoitukseensa: videopeli, jossa on 60 Hz (ruutua sekunnissa), on hyväksyttävä, mutta 6 ruutua sekunnissa on sietämättömän epätasainen – suorituskyky on otettava huomioon alusta alkaen, jotta voidaan varmistaa, että järjestelmä pystyy tuottamaan riittävää suorituskykyä, ja varhaisten prototyyppien suorituskyvyn on oltava suurin piirtein hyväksyttävä, jotta voidaan luottaa siihen, että lopullisen järjestelmän suorituskyky tulee olemaan hyväksyttävä (optimoinnilla). Tämä jätetään joskus huomiotta siinä uskossa, että optimointi voidaan aina tehdä myöhemmin, jolloin syntyy prototyyppijärjestelmiä, jotka ovat aivan liian hitaita – usein kertaluokkaa tai enemmän – ja järjestelmiä, jotka lopulta epäonnistuvat, koska ne eivät arkkitehtuuriltaan kykene saavuttamaan suorituskykytavoitteitaan, kuten Intel 432 (1981), tai järjestelmiä, joiden hyväksyttävän suorituskyvyn saavuttaminen vaatii vuosien työn, kuten Java (1995), joka saavutti hyväksyttävän suorituskyvyn vasta HotSpotilla (1999). Se, missä määrin suorituskyky muuttuu prototyypin ja tuotantojärjestelmän välillä ja kuinka helposti se on optimoitavissa, voi olla merkittävä epävarmuuden ja riskin lähde.

SuunnittelutasoEdit

Korkeimmalla tasolla suunnittelua voidaan optimoida siten, että käytettävissä olevia resursseja hyödynnetään parhaalla mahdollisella tavalla ottaen huomioon päämäärät, rajoitteet ja odotettu käyttö/kuormitus. Järjestelmän arkkitehtuurisuunnittelu vaikuttaa ylivoimaisesti sen suorituskykyyn. Esimerkiksi järjestelmä, joka on riippuvainen verkon viiveestä (jossa verkon viive on kokonaissuorituskyvyn tärkein rajoitus), optimoitaisiin siten, että verkkomatkat minimoitaisiin ja että se tekisi mieluummin yhden pyynnön (tai ei pyyntöjä, kuten push-protokollassa) kuin useita kiertomatkoja. Suunnittelun valinta riippuu tavoitteista: kun kääntäjä suunnitellaan, jos nopea kääntäminen on tärkein prioriteetti, yksisuuntainen kääntäjä on nopeampi kuin monisuuntainen kääntäjä (olettaen, että työ on sama), mutta jos tavoitteena on tulostettavan koodin nopeus, hitaampi monisuuntainen kääntäjä täyttää tavoitteen paremmin, vaikka se itsessään kestääkin kauemmin. Alustan ja ohjelmointikielen valinta tapahtuu tällä tasolla, ja niiden muuttaminen edellyttää usein täydellistä uudelleenkirjoittamista, vaikka modulaarinen järjestelmä voi sallia vain joidenkin komponenttien uudelleenkirjoittamisen – esimerkiksi Python-ohjelma voi kirjoittaa suorituskykykriittiset osat uudelleen C:llä. Hajautetussa järjestelmässä arkkitehtuurin valinta (asiakas-palvelin, vertaisverkko jne.) tapahtuu suunnittelun tasolla, ja sen muuttaminen voi olla hankalaa etenkin, jos kaikkia komponentteja ei voida vaihtaa synkronoidusti (esim, vanhoja asiakkaita).

Algoritmit ja tietorakenteetEdit

Seuraavana vaiheena on tehokkaiden algoritmien ja tietorakenteiden hyvä valinta sekä näiden algoritmien ja tietorakenteiden tehokas toteutus. Suunnittelun jälkeen algoritmien ja tietorakenteiden valinta vaikuttaa tehokkuuteen enemmän kuin mikään muu ohjelman osa-alue. Yleensä tietorakenteita on vaikeampi muuttaa kuin algoritmeja, koska tietorakenneoletusta ja sen suorituskykyoletuksia käytetään koko ohjelmassa, joskin tämä voidaan minimoida käyttämällä abstrakteja tietotyyppejä funktiomäärittelyissä ja pitämällä konkreettiset tietorakennemäärittelyt rajoitettuina muutamaan paikkaan.

Algoritmien osalta tämä koostuu ensisijaisesti sen varmistamisesta, että algoritmit ovat vakio O(1), logaritmisesti O(log n), lineaarisesti O(n) tai joissakin tapauksissa loogisesti lineaarisesti O(n-log n) syötteessä (sekä tilassa että ajallisesti). Algoritmit, joiden kompleksisuus on kvadraattinen O(n2), eivät skaalautu, ja jopa lineaariset algoritmit aiheuttavat ongelmia, jos niitä kutsutaan toistuvasti, ja ne korvataan tyypillisesti vakioilla tai logaritmisilla, jos mahdollista.

Asymptoottisen kasvujärjestyksen lisäksi vakiokertoimilla on väliä: asymptoottisesti hitaampi algoritmi voi olla nopeampi tai pienempikokoisempi (koska se on yksinkertaisempi) kuin asymptoottisesti nopeampi algoritmi, kun molemmilla on edessään pieni syötemäärä, mikä voi olla se tapaus, joka toteutuu todellisuudessa. Usein hybridialgoritmi tarjoaa parhaan suorituskyvyn, koska tämä kompromissi muuttuu koon mukaan.

Yleinen tekniikka suorituskyvyn parantamiseksi on työn välttäminen. Hyvä esimerkki on nopean polun käyttö tavallisissa tapauksissa, mikä parantaa suorituskykyä välttämällä tarpeetonta työtä. Esimerkiksi yksinkertaisen tekstin asettelualgoritmin käyttäminen latinalaiselle tekstille ja siirtyminen monimutkaiseen asettelualgoritmiin vain monimutkaisissa kirjoitusmerkeissä, kuten devanagarissa. Toinen tärkeä tekniikka on välimuistiin tallentaminen, erityisesti muistiin tallentaminen, jolla vältetään turhia laskutoimituksia. Koska välimuistiin tallentaminen on tärkeää, järjestelmässä on usein useita välimuistitasoja, mikä voi aiheuttaa muistin käytöstä johtuvia ongelmia ja vanhentuneista välimuisteista johtuvia virheettömyysongelmia.

Lähdekoodin tasoMuokkaa

Yleisten algoritmien ja niiden toteuttamisen abstraktilla koneella lisäksi lähdekoodin konkreettisilla tason valinnoilla voidaan saada aikaan merkittävä ero. Esimerkiksi varhaisissa C-kääntäjissä while(1) oli hitaampi kuin for(;;) ehdottomalle silmukalle, koska while(1) arvioi 1 ja sen jälkeen oli ehdollinen hyppy, joka testasi, oliko se tosi, kun taas for (;;) oli ehdoton hyppy . Jotkin optimoinnit (kuten tämä) voidaan nykyään suorittaa optimoivilla kääntäjillä. Tämä riippuu lähdekielestä, kohdekoneen kielestä ja kääntäjästä, ja sitä voi olla vaikea ymmärtää tai ennustaa ja se voi muuttua ajan mittaan; tämä on keskeinen kohta, jossa kääntäjien ja konekoodin ymmärtäminen voi parantaa suorituskykyä. Silmukan muuttumaton koodiliike ja paluuarvon optimointi ovat esimerkkejä optimoinneista, jotka vähentävät apumuuttujien tarvetta ja voivat jopa johtaa nopeampaan suorituskykyyn välttämällä kierto-optimointeja.

Build levelEdit

Lähdekoodi- ja kääntötason välillä direktiiveillä ja build flageilla voidaan virittää lähdekoodin ja kääntäjän suorituskykyasetuksia, esimerkiksi käyttämällä esiprosessorimääritteitä tarpeettomien ohjelmisto-ominaisuuksien poistamiseksi käytöstä, optimoimalla tietyille prosessorimalleille tai laitteisto-ominaisuuksille tai ennustamalla haarautumista. Lähdekoodipohjaiset ohjelmistojen jakelujärjestelmät, kuten BSD:n Ports ja Gentoon Portage, voivat hyödyntää tätä optimoinnin muotoa.

Compile levelEdit

Optimoivan kääntäjän käytöllä on taipumus varmistaa, että suoritettava ohjelma on optimoitu vähintään niin paljon kuin kääntäjä voi ennustaa.

KokoonpanotasoEdit

Alhaisimmalla tasolla koodin kirjoittaminen tietylle laitteistoalustalle suunnitellulla kokoonpanokielellä voi tuottaa tehokkainta ja kompaktimpaa koodia, jos ohjelmoija hyödyntää koko konekäskyjen repertuaaria. Monet sulautetuissa järjestelmissä käytettävät käyttöjärjestelmät on tästä syystä perinteisesti kirjoitettu assembler-koodilla. Ohjelmia (muita kuin hyvin pieniä ohjelmia) kirjoitetaan harvoin alusta loppuun assemblerilla siihen kuluvan ajan ja kustannusten vuoksi. Useimmat ohjelmat käännetään korkean tason kielestä assembleriksi ja optimoidaan siitä käsin. Kun tehokkuus ja koko eivät ole niin tärkeitä, suuret osat voidaan kirjoittaa korkean tason kielellä.

Nykyaikaisempien optimoivien kääntäjien ja viimeaikaisten suorittimien suuremman monimutkaisuuden ansiosta on vaikeampaa kirjoittaa tehokkaampaa koodia kuin mitä kääntäjä tuottaa, ja vain harvat projektit tarvitsevat tätä ”viimeistä” optimointivaihetta.

Suuri osa nykyään kirjoitetusta koodista on tarkoitettu toimimaan mahdollisimman monessa koneessa. Tämän seurauksena ohjelmoijat ja kääntäjät eivät aina hyödynnä uudempien suorittimien tarjoamia tehokkaampia ohjeita tai vanhempien mallien omituisuuksia. Lisäksi assembly-koodi, joka on viritetty tietylle prosessorille ilman tällaisten ohjeiden käyttöä, saattaa silti olla epäoptimaalinen toisella prosessorilla, joka edellyttää koodin erilaista virittämistä.

Tyypillisesti nykyään sen sijaan, että ohjelmoijat kirjoittaisivat assembler-kielellä, he käyttävät disassembleriä analysoidakseen kääntäjän tulosteen ja muuttaakseen korkean tason lähdekoodia niin, että se voidaan kääntää tehokkaammin, tai ymmärtääkseen, miksi se on tehotonta.

SuoritusaikaMuokkaus

Just-in-time-kääntäjät pystyvät tuottamaan räätälöityä konekoodia suoritusaikaisen datan perusteella kääntämisen yleiskustannusten kustannuksella. Tämä tekniikka juontaa juurensa varhaisimmista säännöllisten lausekkeiden moottoreista, ja se on yleistynyt Java HotSpotin ja JavaScriptin V8:n myötä. Joissakin tapauksissa adaptiivinen optimointi voi pystyä suorittamaan ajoaikaoptimointia, joka ylittää staattisten kääntäjien kyvyt, säätämällä dynaamisesti parametreja todellisen syötteen tai muiden tekijöiden mukaan.

Profiiliohjattu optimointi on ajoaikaprofiileihin perustuva ajoaikaisen kääntämisen optimointitekniikka, ja se on samankaltainen kuin dynaamisen tekniikan dynaamisen tekniikan, adaptiivisen optimoinnin, staattisen analogian staattisen ”keskimääräisen tapauksen” kaltainen.

Self-modifying code (itseään muokkaava koodi) voi muuttaa itseään vastauksena ajonaikaisiin olosuhteisiin koodin optimoimiseksi; tämä oli yleisempää assembler-kielisissä ohjelmissa.

Jotkut CPU-mallit voivat suorittaa joitakin optimointeja ajonaikana. Joitakin esimerkkejä ovat järjestyksen ulkopuolinen suoritus, spekulatiivinen suoritus, komentoputket ja haarautumisennusteet. Kääntäjät voivat auttaa ohjelmaa hyödyntämään näitä suorittimen ominaisuuksia esimerkiksi käskyjen ajoituksen avulla.

Alustariippuvaiset ja alustariippumattomat optimoinnitMuutos

Koodin optimointi voidaan myös jakaa karkeasti alustariippuviin ja alustariippumattomiin tekniikoihin. Kun jälkimmäiset ovat tehokkaita useimmilla tai kaikilla alustoilla, alustariippuvaiset tekniikat käyttävät yhden alustan erityisominaisuuksia tai luottavat yksittäisestä alustasta tai jopa yksittäisestä prosessorista riippuviin parametreihin. Tästä syystä saattaa olla tarpeen kirjoittaa tai tuottaa eri versioita samasta koodista eri prosessoreille. Esimerkiksi kääntötason optimoinnissa alustariippumattomat tekniikat ovat yleisiä tekniikoita (kuten silmukoiden purkaminen, funktiokutsujen vähentäminen, muistitehokkaat rutiinit, ehtojen vähentäminen jne.), jotka vaikuttavat useimpiin prosessoriarkkitehtuureihin samalla tavalla. Hyvä esimerkki alustariippumattomasta optimoinnista on esitetty sisäisellä for-silmukalla, jossa havaittiin, että silmukka, jossa on sisäinen for-silmukka, suorittaa enemmän laskutoimituksia aikayksikköä kohden kuin silmukka, jossa sitä ei ole tai jossa on sisäinen while-silmukka. Yleensä näiden tarkoituksena on vähentää ohjelman suorittamiseen tarvittavan käskypolun kokonaispituutta ja/tai vähentää muistin kokonaiskäyttöä prosessin aikana. Toisaalta alustasta riippuviin tekniikoihin liittyy käskyjen aikataulutus, käskytason rinnakkaisuus, tietotason rinnakkaisuus, välimuistin optimointitekniikat (eli parametrit, jotka vaihtelevat eri alustoilla), ja optimaalinen käskyjen aikataulutus voi olla erilainen jopa saman arkkitehtuurin eri prosessoreissa.