Luento 7 T-106.1240 Ohjelmoinnin jatkokurssi T1 & T-106.1243 Ohjelmoinnin jatkokurssi L1 Luennoitsija: Otto Seppälä Kurssin WWW: http://www.cs.hut.fi/opinnot/t-106.1240/s2007
Suunnitelmista Yleistaso oli tänä vuonna alustavien tietojen perusteella varsin hyvä. Monesta suunnitelmapalautuksesta oli unohtunut kuvat. Omatkin algoritmit ovat toki algoritmeja Ohjelmani ei käytä algoritmeja tai tietorakenteita Lähdeluettelo kannattaa tehdä viimeistään dokumenttiin kunnolla Hyvää harjoitusta diplomi- ja kandityötä varten Erilaisia lähdeviittausoppaita löytyy webistä: lib.hut.fi/opetus/informatiikka/tietoiskut/lahde-tietoisku.html Google: Reference guide IEEE Style Google: Reference guide Harvard style
CVS-demo CVS:n käyttöä demottiin kurssisivuston ohjeita noudattaen
Tehokkuusnäkökohtia
Ohjelmien tehokkuus Millainen on tehokas ohjelma Nopeus Muistinkäyttö Vasteaika (reagoi aina nopeasti käyttäjän toimiin) Kuinka näitä voidaan mitata? Profilointi Koodin ja algoritmien analyysi notaatiot : Iso-O, iso-theta, iso-omega Kuinka niihin voidaan vaikuttaa? Hyvät algoritmit ja tietorakenteet Koodin optimointi
Optimoi vasta lopuksi Donald Knuth: Premature optimization is the root of all evil Optimointi monimutkaistaa koodia Luo ehkä uusia virheitä Vaikeuttaa koodin ylläpidettävyyttä Vaikeuttaa jatkokehitystä / perintää Kaikki optimoinnit eivät ole optimaalisia Hyvä kääntäjä osaa tehdä joitakin optimointeja paremmin kuin ohjelmoija: esim. pipelining Aina ei kannata optimoida Jos voitot ovat pieniä, ylläpidettävyys on tärkeämpää Tee ensin toimiva ohjelma; vasta sitten optimointi
Suunnittelu Ohjelman jako metodeihin, luokkiin ja moduuleihin Osa ohjelman pullonkauloista syntyy jo tässä vaiheessa Suunnittele hyvin Kuinka tieto siirtyy moduleiden välillä? Esim tehtävän 2.1 käyttämä BreakIterator palauttaa tavutuskohdat, ei pilkottuja String-olioita Pilkotut String-oliot pitäisi monesti koota takaisin sopivan levyisiksi riveiksi joka aiheuttaisi vain turhaa lisätyötä Tavutuskohdista voidaan laskea kohdat joista rivi voidaan katkaista ja jakaa merkkijono osiin vain näistä kohdista.
Tietorakenteet ja algoritmit Oikeiden tietorakenteiden ja algoritmien valinta on äärimmäisen tärkeää Suurilla tietomäärillä eri algoritmien nopeuserot voivat olla valtavia Helposti yli tuhatkertaisia nopeuseroja
Koodi Erilaisilla koodirakenteilla ja käskyillä on nopeuseroja Riippuvat monesti ajoympäristöstä Koodin optimointi kannattaa keskittää vain keskeisiin osiin koodista Pienillä nopeuseduilla on merkitystä vain jos tehostunutta kohtaa käytetään usein 90/10 sääntö : 90% suoritusajasta kuluu 10%:ssa koodia.
Järjestelmä Laitteiston vaikutus Eri laitteiden ominaisuudet vaikuttavat siihen minkätyyppiset ongelmat ratkeavat parhaiten Optimoiva kääntäjä Hyvä kääntäjä tekee tietyt optimoinnit puolestasi Nopeuserot parhaimmillaan kymmenkertaisia Mutta käytännössä harvoin
Nopeuden mittaus ja arviointi
Profilointi Ohjelman suoritusta voidaan tutkia profilointityökaluilla Ohjelmien muistinkäyttö Tehdyt aliohjelmakutsut (metodikutsut) Metodeissa käytetty aika Java SDK:ssa kaksi profilointityökalua java -X:prof java -X:runhprof Eclipse IDE:en on olemassa TPTP-niminen lisäosa Todella kätevä (mutta haastava asentaa toimivasti) laskee myös koodikattavuudet jne.
F:\tehokkuus\code>java -Xprof:help Matrix Flat profile of 10.70 secs (1063 total ticks): main Interpreted + native Method 0.3% 0 + 3 java.io.winntfilesystem.getbooleanattributes 0.1% 1 + 0 Matrix.kerroMatriisitJIK_NonOp... 0.1% 0 + 1 java.io.fileinputstream.close0 1.8% 11 + 8 Total interpreted Compiled + native Method 11.0% 117 + 0 Matrix.kerroMatriisitJKI_NonOp 10.9% 116 + 0 Matrix.kerroMatriisitKJI_NonOp... 4.2% 45 + 0 Matrix.kerroMatriisitKIJ 0.2% 0 + 2 Matrix.doIt 97.6% 1035 + 3 Total compiled Thread-local ticks: 0.3% 3 Class loader 0.3% 3 Compilation Flat profile of 0.00 secs (1 total ticks): DestroyJavaVM Thread-local ticks: 100.0% 1 Blocked (of total) Global summary of 11.18 seconds: 100.0% 1117 Received ticks 0.4% 4 Received GC ticks 0.3% 3 Compilation 0.1% 1 Other VM operations 0.3% 3 Class loader
Koodin analysointi Koodin voidaan nähdä koostuvan kolmesta rakenteesta Sekvenssi koodilauseiden sarja, jotka suoritetaan peräkkäin ei sisällä haarautumista (ehtolauseet) muiden metodien kutsut käsitellään osana sekvenssiä ehtolauseet if-else switch silmukat for, while, do-while
Koodin analysointi Suoritusaika ilmoitetaan yleensä syötteen funktiona syötteen määrää merkitään muuttujalla N Lasketaan suoritettuja operaatioita perusoperaatiot : =, +, -, taulu[i], jne. Sekvenssissä lasketaan sen sisältämien lauseiden sisältämien operaatioiden määrät yhteen Silmukassa sen sisältämä sekvenssi voidaan suorittaa useita kertoja (mahdollisesti eri N:n arvoilla)
Esimerkki public int laskeyhteen(int[] taulu){ int summa = 0; for (int i=0; i<taulu.length; i++){ summa += taulu[i]; return summa;
Esimerkki public int laskeyhteen(int[] taulu){ int summa = 0; for (int i=0; i<taulu.length; i++){ summa += taulu[i]; return summa; 1 kerta N kertaa N+1 kertaa 1 + 1 + (N+1) + 3*N + 1 = 4N + 4 -> O(N) N f(n) 1 8 10 44 100 404 1000 4004
Asymptoottinen kompleksisuus Mikä osa funktiosta on olennaista kun N kasvaa? pienempien termien merkitys vähenee Myöskään N:n edessä olevalla kertoimella ei ole merkitystä eri lauseet vievät eri ajan (sijoitus, return, yhteenlasku, vertailu) kertoimia ei siis voisi kuitenkaan vertailla Olennaista on siis vain kertaluokka Asymptoottinen kompleksisuus Otetaan vain suurimmat vaikuttavat termit Poistetaan termin vakiokerroin
Asymptoottinen kompleksisuus Kun algoritmien tehokkutta toisiinsa verrataan suuremmalla syötekoolla ei esimerkkimme kaavan 4N+4 kaikilla termeillä ja kertoimilla ole suurta merkitystä Suurilla N:n arvoilla termi 4N dominoi tulosta. Käytännössä tämä olisi kuitenkin jo jäänyt kakkoseksi 4.01N:lle. Verrattaessa minkä tahansa muun kertaluvun funktioon (esim 15N 2 ) myöskään kertoimella 4.01 ei olisi ollut väliä. Vaikka lauseke olisi ollut 15000N niin jo tuhannen paikkeilla 15N 2 olisi ohittanut sen. Tuntuisi siis riittävältä huomata että suurilla arvoilla lausekkeen dominoiva termi on k*n jossa k on vain jokin vakio (yksinkertaistus)
Asymptoottinen kompleksisuus Formaalimmin voidaan sanoa että lausekkeelle voidaan määritellä asymptoottinen yläraja M g(n) joka on aina suurempi kuin meitä kiinnostava funktio f, kunhan vain N on riittävän suuri (yli n 0 ). f(n) O(g(n)) M on jokin vakio n 0 on jokin piste jota suuremmilla arvoilla aina pätee M > 0, n 0 >0 s.e. f(n) M(g(n)), n>n 0 Esimerkiksi kaavalle 4N+4 arvot olisivat voineet olla M=5 ja n 0 = 10 kun g(n) = N Tällöin voitaisiin Iso-O notaatiolla merkitä (4N + 4) O(N)
Asymptoottinen kompleksisuus M*g(n) f(n) f(n) = O(g(n)) n 0
Analyysi Iso-O notaatiolla Koodiesimerkki
public int dosomething(int[] taulu){ int muuttuja = 0; for (int i=0; i<taulu.length; i++) for (int j=i; j<taulu.length; j++){ muuttuja += taulu[i]*taulu[j]; for (int i=0; i<5; i++){ muuttuja -= apumetodi(taulu, i); return muuttuja; public int apumetodi(int[] summattavat, int kerroin){ int summa = 0; for (int i=0; i<summattavat.length; i++) summa+=summattavat[i]*kerroin; return summa;
public int dosomething(int[] taulu){ int muuttuja = 0; for (int i=0; i<taulu.length; i++) for (int j=i; j<taulu.length; j++){ muuttuja += taulu[i]*taulu[j]; for (int i=0; i<5; i++){ muuttuja -= apumetodi(taulu, i); return muuttuja; public int apumetodi(int[] summattavat, int kerroin){ int summa = 0; for (int i=0; i<summattavat.length; i++) summa+=summattavat[i]*kerroin; return summa;
public int dosomething(int[] taulu){ int muuttuja = 0; for (int i=0; i<taulu.length; i++) for (int j=i; j<taulu.length; j++){ muuttuja += taulu[i]*taulu[j]; for (int i=0; i<5; i++){ muuttuja -= apumetodi(taulu, i); return muuttuja; O(N) public int apumetodi(int[] summattavat, int kerroin){ int summa = 0; for (int i=0; i<summattavat.length; i++) summa+=summattavat[i]*kerroin; O(N) return summa;
public int dosomething(int[] taulu){ int muuttuja = 0; for (int i=0; i<taulu.length; i++) for (int j=i; j<taulu.length; j++){ muuttuja += taulu[i]*taulu[j]; for (int i=0; i<5; i++){ muuttuja -= apumetodi(taulu, i); return muuttuja; O(N) O(N) O(N 2 ) public int apumetodi(int[] summattavat, int kerroin){ int summa = 0; for (int i=0; i<summattavat.length; i++) summa+=summattavat[i]*kerroin; O(N) return summa; O(N)
public int dosomething(int[] taulu){ int muuttuja = 0; for (int i=0; i<taulu.length; i++) for (int j=i; j<taulu.length; j++){ muuttuja += taulu[i]*taulu[j]; for (int i=0; i<5; i++){ muuttuja -= apumetodi(taulu, i); return muuttuja; O(N) O(N) O(N 2 ) O(N) public int apumetodi(int[] summattavat, int kerroin){ int summa = 0; for (int i=0; i<summattavat.length; i++) summa+=summattavat[i]*kerroin; O(N) return summa; O(N)
public int dosomething(int[] taulu){ int muuttuja = 0; O(N 2 ) for (int i=0; i<taulu.length; i++) for (int j=i; j<taulu.length; j++){ muuttuja += taulu[i]*taulu[j]; for (int i=0; i<5; i++){ muuttuja -= apumetodi(taulu, i); return muuttuja; O(N 2 ) O(N) public int apumetodi(int[] summattavat, int kerroin){ int summa = 0; for (int i=0; i<summattavat.length; i++) summa+=summattavat[i]*kerroin; O(N) return summa; O(N)
Laitteiston vaikutus
Laitteiston vaikutus Laitteiston tuntemuksesta on hyötyä, jos halutaan kirjoittaa tehokasta koodia Monesti kääntäjä suorittaa nämä optimoinnit Tällöin on olennaista, ettei koodin rakenne estä kääntäjän toimintaa Javan tapauksessa virtuaalikone on pyritty optimoimaan käytettävälle alustalle.
Välimuisti Välimuisti toimii selvästi tavallista muistia nopeammin Välimuistissa säilytetään usein käytettyjä / juuri käytettyjä muistialkioita Usein käytettyjä alkioita ei tarvitse aina hakea tavallisesta muistista Välimuistin tehokkuuteen vaikuttaa useampi tekijä Kuinka hajallaan käytetyt muistialueet ovat? Käyttävätkö algoritmit peräkkäisiä muistipaikkoja? Kuinka suurta määrää muuttujia ohjelma käyttää kerrallaan? Mahtuvatko usein käytetyt muuttujat välimuistiin?
Muisti pullonkaulana Prosessori toimii huomattavasti muistia nopeammin Rekisterit toimivat prosessorin nopeudella Välimuistit toimivat muistia nopeammin Välimuistin koko on kuitenkin rajallinen Lisäksi ohjelmat käyttävät virtuaalimuistia Sopeuttamalla ohjelman ja eri muistien toiminta yhteen voidaan saavuttaa tehokkuusetuja
Virtuaalimuisti Ohjelmille voidaan virtuaalimuistin avulla tarjota paljon todellista enemmän muistia Muistin laajennus toteutetaan käyttämällä kiintolevyä muistin säilytykseen Harvemmin käytetyt muistisivut tallennetaan levylle Usein käytetyt muistisivut pysyvät muistissa Kun jotakin levyllä olevaa sivua tarvitaan, se tuodaan muistiin
Muistinkäytöstä Erilaiset muistinkäyttöstrategiat voivat vaikuttaa paljon ohjelman suorituskykyyn Suuresta datamäärästä kannattaa käsitellä yhtenäisiä osia pidempiä aikoja kerrallaan eikä yksittäisiä alkioita sieltä täältä jotta välimuisti ja virtuaalimuisti toimisivat tehokkaasti
Blocking Kun ohjelma käyttää jotakin I/O-laitetta, voi metodikutsu joutua odottamaan laitteen vastausta Kyseessä on ns. blokkaava metodikutsu Blokkaava säie ei voi jatkaa suoritusta ennen kuin I/Olaite on valmis Yleensä tämä on etu lukeva säie ei kuluta resursseja odottaessaan ja jatkaa automaattisesti työtään kun dataa on taas tarjolla. Web-serverissä tämä voisi olla myös ongelma, kun jokaista asiakasta kohden pitäisi olla oma säie odottamassa Javassa on myös ei-blokkaava I/O-kirjasto
Iteraatio ja rekursio Rekursiossa tehdään paljon metodikutsuja Metodikutsun yhteydessä suorituspinosta varataan kehys paikallisia muuttujia jne. varten Kehyksen varaaminen ja metodikutsusta palaaminen käyttävät prosessoriaikaa ja muistia Rekursio on tämän vuoksi usein tehottomampi ratkaisu kuin iteraatio Riippuu myös koneen arkkitehtuurista ja käytetystä kääntäjästä...sekä tietysti ohjelmointikielestä Usein rekursiivisen ratkaisun selkeys voi olla olennaisempaa kuin saavutettu tehoetu
Kääntäjän optimointeja Loop unrolling Method inlining Software pipelining Reordering Häntärekursion poisto jne.. (Ylläolevat ovat yleisiä esimerkkejä ja lista sisältää myös ei java-spesifejä esimerkkejä. Tarjolla olevan optimoinnin taso vaihtelee ohjelmointikielen ja käytetyn kääntäjän mukaan)
Ohjelmoijan optimointeja (esim.) Olioiden kierrätys Monimutkaisten olioiden luonti on raskasta Käytöstä poistettujen olioiden tallennus ja uudelleen käyttöönotto nopeuttaa (factory pattern) StringBuffer vs. String Runsaasti merkkijonojen yhteenliittämisiä sisältävässä ohjelmassa StringBuffer voi vähentää väliaikaisten merkkijonojen luomista. Nopeuserot suurilla merkkijonoilla parhaimmillaan valtavia. StringBuilder on vielä parempi. (kunhan kaikki kirjoitus tehdään yhdessä säikeessä) final-määreen käyttö final-määreellä metodien korvaaminen aliluokissa estetään. Tällöin kääntäjä voi korvata metodikutsun sijoittamalla metodin koodin suoraan kutsun tilalle. (tätä ei nykyisin enää suositella) Vakioarvot vs muuttujat Kääntäjä voi sijoittaa vakioiksi (final public static) määritetyt muuttujat vakioina lopulliseen konekoodiin
Ohjelmoijan optimointeja (esim.) Välitulosten uudelleenkäyttö Dynamic programming Ei lasketa uudelleen välituloksia jos ne lasketaan joskus aiemmin algoritmissa jotain toista arvoa varten Hakualgoritmeissa karsinta Jos tiedetään että jokin haku tai ratkaisualgoritmi ei voi löytää haluttua tulosta tällä hetkellä työn alla olevaa reittiä, lopetetaan etsintä tätä kautta ajoissa ja kokeillaan jotakin muuta. Tietovirtojen puskurointi Kovalevyn yms. I/O laitteiden luku/kirjoitusoperaatioiden kannattaa mieluummin kirjoittaa kerralla paljon kuin usein vähän. BufferedWriter, BufferedReader, BufferedInputStream BufferedOutputStream.
Ohjelmoijan optimointeja private-määreen käyttö Myös private-määre mahdollistaa kääntäjän optimoinnin Koska muuttujiin / metodeihin ei pääse ulkoa, voidaan koodia käsitellä voimakkaammin method inlining jne. Ja paljon, paljon muuta...
Yhteenveto Ohjelmien tehokkuuteen vaikuttavia tekijöitä on monia Tehokkuuteen eniten vaikuttavat päätökset tehdään usein jo suunnitelmatasolla Tietorakenteiden ja algoritmien valinta on erittäin tärkeää Vasta näiden jälkeen kannattaa ajatella optimointia Algoritmien nopeutta kuvataan Iso-O, Iso-Θ, Iso-Ω ja pieni-o notaatioilla Kääntäjä osaa optimoida koodia usein riittävän hyvin Vain kriittinen koodi on syytä optimoida käsin Koodia tutkimalla tai profiloimalla voidaan löytää mahdolliset pullonkaulat Käsin optimointi voi haitata ylläpidettävyyttä tai jopa hidastaa ohjelmaa jos ei tiedä mitä tekee Laitteiston ja kääntäjän tuntemuksesta on apua