Ohjelmointi

Tee Java nopeasti: Optimoi!

Uraauurtavan tietojenkäsittelijän Donald Knuthin mukaan "ennenaikainen optimointi on kaiken pahan perusta". Mikä tahansa artikkeli optimoinnista on aloitettava huomauttamalla, että syitä on yleensä enemmän ei optimoida kuin optimoida.

  • Jos koodisi jo toimii, sen optimointi on varma tapa tuoda uusia ja mahdollisesti hienovaraisia ​​vikoja

  • Optimointi tekee koodista vaikeampaa ymmärtää ja ylläpitää

  • Jotkut tässä esitetyistä tekniikoista lisäävät nopeutta vähentämällä koodin laajennettavuutta

  • Koodin optimointi yhdelle alustalle voi todella pahentaa sitä toisella alustalla

  • Paljon aikaa voidaan käyttää optimointiin, suorituskyvyn vähäisellä kasvulla, ja se voi johtaa hämärtyneeseen koodiin

  • Jos olet liian pakkomielle koodin optimoinnista, ihmiset kutsuvat sinua nörtiksi selkäsi takana

Ennen optimointia sinun on harkittava huolellisesti, tarvitseeko sitä optimoida ollenkaan. Java-optimointi voi olla vaikea kohde, koska toteutusympäristöt vaihtelevat. Paremman algoritmin käyttö tuo todennäköisesti suuremman suorituskyvyn kasvun kuin mikä tahansa matalan tason optimoinnin määrä, ja on todennäköisempää, että parannus tapahtuu kaikissa toteutusolosuhteissa. Yleensä korkean tason optimoinnit tulisi harkita ennen matalan tason optimointia.

Joten miksi optimoida?

Jos se on niin huono idea, miksi optimoida ollenkaan? No, ihanteellisessa maailmassa et. Mutta tosiasia on, että joskus ohjelman suurin ongelma on se, että se vaatii yksinkertaisesti liian paljon resursseja, ja nämä resurssit (muisti, suorittimen jaksot, verkon kaistanleveys tai yhdistelmä) voivat olla rajoitettuja. Koodipalat, jotka esiintyvät useita kertoja ohjelmassa, ovat todennäköisesti kokoherkkoja, kun taas koodi, jolla on useita suorituksen iteraatioita, voivat olla nopeusherkkiä.

Tee Java nopeasti!

Tulkituna kielenä, jolla on kompakti tavukoodi, nopeus tai sen puute on se, mikä useimmiten ponnahtaa esiin Java-ongelmana. Tarkastelemme ensisijaisesti sitä, kuinka saada Java ajaa nopeammin sen sijaan, että se mahtuisi pienempään tilaan - vaikka huomautamme, missä ja miten nämä lähestymistavat vaikuttavat muistiin tai verkon kaistanleveyteen. Painopiste on ydinkielessä eikä Java-sovellusliittymissä.

Muuten, yksi asia me tapa keskustele tässä on natiivien menetelmien käyttö kirjoitettu C tai kokoonpano. Natiivien menetelmien käyttäminen voi antaa lopullisen suorituskyvyn kasvun, mutta se tapahtuu Java-alustan riippumattomuuden kustannuksella. On mahdollista kirjoittaa sekä Java-versio menetelmästä että alkuperäiset versiot valituille alustoille; tämä johtaa lisääntyneeseen suorituskykyyn joillakin alustoilla luopumatta mahdollisuudesta toimia kaikilla alustoilla. Mutta tämä on kaikki mitä sanon aiheesta Java-korvaaminen C-koodilla. (Katso lisätietoja aiheesta Java-vinkistä "Kirjoita alkuperäiset menetelmät".) Tässä artikkelissa keskitymme siihen, miten Java voidaan tehdä nopeasti.

90/10, 80/20, kota, kota, vaellus!

Pääsääntöisesti 90 prosenttia ohjelman poistoajasta käytetään 10 prosentin koodin suorittamiseen. (Jotkut ihmiset käyttävät 80 prosentin / 20 prosentin sääntöä, mutta kokemukseni kaupallisten pelien kirjoittamisesta ja optimoinnista useilla kielillä viimeisten 15 vuoden aikana ovat osoittaneet, että 90 prosentin / 10 prosentin kaava on tyypillinen suorituskyvyn nälkäisille ohjelmille, koska harvat tehtävät yleensä suoritetaan suurella taajuudella.) Ohjelman 90 prosentin optimoinnilla (jossa käytettiin 10 prosenttia suoritusajasta) ei ole havaittavaa vaikutusta suorituskykyyn. Jos pystyisit saamaan, että 90 prosenttia koodista suoritetaan kaksi kertaa nopeammin, ohjelma olisi vain 5 prosenttia nopeampi. Joten ensimmäinen tehtävä koodin optimoinnissa on tunnistaa 10 prosenttia (usein vähemmän kuin tämä) ohjelmasta, joka kuluttaa suurimman osan suoritusajasta. Tämä ei ole aina siellä missä odotat sen olevan.

Yleiset optimointitekniikat

On olemassa useita yleisiä optimointitekniikoita, joita käytetään kielestä riippumatta. Jotkut näistä tekniikoista, kuten maailmanlaajuinen rekisterin allokointi, ovat kehittyneitä strategioita koneresurssien (esimerkiksi CPU-rekisterien) allokoimiseksi, eivätkä ne koske Java-tavukoodeja. Keskitymme tekniikoihin, joihin sisältyy periaatteessa koodin uudelleenjärjestely ja vastaavien toimintojen korvaaminen menetelmän sisällä.

Lujuuden vähentäminen

Lujuuden vähennys tapahtuu, kun operaatio korvataan vastaavalla nopeammin suoritettavalla toiminnolla. Yleisin esimerkki voiman vähentämisestä on shift-operaattorin käyttäminen kertomalla ja jakamalla kokonaisluvut arvolla 2. x >> 2 voidaan käyttää x / 4ja x << 1 korvaa x * 2.

Yleinen alailmaisu eliminointi

Yleinen alailmaisuuden poisto poistaa tarpeettomat laskelmat. Kirjoituksen sijaan

kaksinkertainen x = d * (lim / max) * sx; kaksinkertainen y = d * (lim / max) * sy;

yhteinen alilauseke lasketaan kerran ja sitä käytetään molemmissa laskelmissa:

kaksinkertainen syvyys = d * (lim / max); kaksinkertainen x = syvyys * sx; kaksinkertainen y = syvyys * sy;

Koodiliike

Koodiliike siirtää koodia, joka suorittaa toiminnon tai laskee lausekkeen, jonka tulos ei muutu tai on muuttumaton. Koodi siirretään siten, että se suoritetaan vain, kun tulos voi muuttua, eikä suorittaa joka kerta, kun tulos vaaditaan. Tämä on yleisintä silmukoiden kohdalla, mutta siihen voi liittyä myös koodi, joka toistetaan jokaisessa menetelmän kutsussa. Seuraava on esimerkki muuttamattomasta koodiliikkeestä silmukassa:

for (int i = 0; i <x.pituus; i ++) x [i] * = Math.PI * Math.cos (y); 

tulee

kaksinkertainen picosy = Math.PI * Math.cos (y);for (int i = 0; i <x.pituus; i ++) x [i] * = pikosy; 

Silmukoiden purkaminen

Silmukoiden purkaminen vähentää silmukan ohjauskoodin yleiskustannuksia suorittamalla useamman kuin yhden operaation joka kerta silmukan läpi ja suorittamalla siten vähemmän iteraatioita. Työskentelemme edellisestä esimerkistä, jos tiedämme, että pituus x [] on aina kahden kerroin, voimme kirjoittaa silmukan uudestaan:

kaksinkertainen picosy = Math.PI * Math.cos (y);for (int i = 0; i <x.pituus; i + = 2) { x [i] * = näennäinen; x [i + 1] * = pikosy; } 

Käytännössä tällaisten silmukoiden purkaminen - jossa silmukan indeksin arvoa käytetään silmukassa ja sitä on lisättävä erikseen - ei tuota tuntuvaa nopeuden kasvua tulkitussa Java: ssa, koska tavukoodeilta puuttuu ohjeita yhdistää "+1"taulukon hakemistoon.

Kaikki tämän artikkelin optimointivihjeet edustavat yhtä tai useampaa yllä luetelluista yleisistä tekniikoista.

Kääntäjän asettaminen toimimaan

Nykyaikaiset C- ja Fortran-kääntäjät tuottavat erittäin optimoitua koodia. C ++ -kääntäjät tuottavat yleensä vähemmän tehokasta koodia, mutta ovat edelleen hyvällä polulla optimaalisen koodin tuottamiseen. Kaikki nämä kääntäjät ovat käyneet läpi monien sukupolvien voimakkaan markkinakilpailun vaikutuksesta ja niistä on tullut hienoksi hiottuja työkaluja jokaisen viimeisen pisaran poistamiseksi tavallisesta koodista. He käyttävät melkein varmasti kaikkia edellä esitettyjä yleisiä optimointitekniikoita. Mutta jäljellä on vielä paljon temppuja tekemään kääntäjät tuottamaan tehokasta koodia.

javac, JIT ja natiivikoodin kääntäjät

Optimoinnin taso javac kun koodin kääntäminen tässä vaiheessa on minimaalista. Se tekee oletuksena seuraavat:

  • Jatkuva taitto - kääntäjä ratkaisee kaikki vakiolausekkeet siten i = (10 * 10) kääntyy i = 100.

  • Haaran taitto (suurimman osan ajasta) - tarpeeton mene tavukoodeja vältetään.

  • Rajoitettu kuolleen koodin eliminointi - mitään koodia ei tuoteta esimerkiksi lauseille jos (väärä) i = 1.

Javacin tarjoaman optimointitason pitäisi parantua, todennäköisesti dramaattisesti, kun kieli kypsyy ja kääntäjien toimittajat alkavat kilpailla tosissaan koodin luomisen perusteella. Java on juuri saamassa toisen sukupolven kääntäjiä.

Sitten on juuri-in-time (JIT) -kääntäjät, jotka muuntavat Java-tavukoodit natiivikoodiksi ajon aikana. Useat ovat jo saatavilla, ja vaikka ne voivat lisätä ohjelman suorittamisnopeutta dramaattisesti, niiden optimointitaso on rajoitettu, koska optimointi tapahtuu ajon aikana. JIT-kääntäjä huolehtii pikemminkin koodin tuottamisesta nopeasti kuin nopeimman koodin luomisesta.

Alkuperäisten koodien kääntäjien, jotka kääntävät Java suoraan natiivikoodiksi, tulisi tarjota suurin suorituskyky, mutta alustan riippumattomuuden kustannuksella. Onneksi tulevat kääntäjät saavuttavat monet tässä esitetyistä temppuista, mutta toistaiseksi kääntäjän hyödyntäminen vie vähän työtä.

javac tarjoaa yhden suorituskykyvaihtoehdon, jonka voit ottaa käyttöön: -O vaihtoehto saada kääntäjä sisällyttämään tietyt metodikutsut:

javac -O MyClass

Menetelmän kutsun lisääminen lisää menetelmän koodin suoraan menetelmän kutsun tekevään koodiin. Tämä eliminoi menetelmäpuhelun yleiskustannukset. Pienessä menetelmässä tämä yleiskustannus voi edustaa merkittävää osaa sen suorittamisajasta. Huomaa, että vain jommallekummaksi ilmoitetut menetelmät yksityinen, staattinentai lopullinen voidaan harkita inline-muodossa, koska kääntäjä ratkaisee staattisesti vain nämä menetelmät. Myös, synkronoitu menetelmiä ei kerrosteta. Kääntäjä sisällyttää vain pienet menetelmät, jotka yleensä koostuvat vain yhdestä tai kahdesta koodirivistä.

Valitettavasti javac-kääntäjän 1.0-versiossa on vika, joka luo koodin, joka ei voi ohittaa tavutunnistinta, kun -O vaihtoehtoa käytetään. Tämä on korjattu JDK 1.1: ssä. (Tavutunnisteen tarkistaja tarkistaa koodin ennen kuin se saa suorittaa varmistaakseen, että se ei riko Java-sääntöjä.) Se sisällyttää menetelmät, joihin viittaavat luokan jäsenet eivät ole käytettävissä kutsuvan luokan käytettävissä. Esimerkiksi, jos seuraavat luokat kootaan yhdessä käyttämällä -O vaihtoehto

luokka A {yksityinen staattinen int x = 10; public staattinen void getX () {return x; }} luokka B {int y = A.getX (); } 

kutsu A.getX: lle () luokassa B viivoitetaan luokassa B ikään kuin B olisi kirjoitettu seuraavasti:

luokka B {int y = A.x; } 

Tämä saa kuitenkin tavukoodien sukupolven käyttämään yksityistä A.x-muuttujaa, joka luodaan B: n koodissa. Tämä koodi toimii hyvin, mutta koska se rikkoo Java-käyttörajoituksia, todentaja merkitsee sen IllegalAccessError ensimmäisen kerran, kun koodi suoritetaan.

Tämä vika ei tee -O Vaihtoehto on hyödytön, mutta sinun on oltava varovainen sen käyttämisessä. Jos sitä kutsutaan yhdelle luokalle, se voi sisällyttää tiettyjä menetelmäkutsuja luokan sisällä ilman riskiä. Useat luokat voidaan rajata yhteen niin kauan kuin mahdollisia pääsyrajoituksia ei ole. Ja joillekin koodeille (kuten sovelluksille) ei suoriteta tavukoodin vahvistinta. Voit jättää virheen huomiotta, jos tiedät, että koodisi suoritetaan vain ilman todentajan alistamista. Lisätietoja on javac-O: n usein kysytyissä kysymyksissä.

Profiilit

Onneksi JDK: ssa on sisäänrakennettu profilointilaite, joka auttaa tunnistamaan, missä aikaa ohjelmassa käytetään. Se seuraa jokaisessa rutiinissa vietettyä aikaa ja kirjoittaa tiedot tiedostoon java.prof. Suorita profilointi käyttämällä -prof vaihtoehto, kun haetaan Java-tulkki:

java -prof myClass

Tai käytettäväksi sovelman kanssa:

java -prof sun.applet.AppletViewer myApplet.html

Profilerin käytöstä on muutama varoitus. Profiililähtöä ei ole erityisen helppo tulkita. Lisäksi JDK 1.0.2: ssa se lyhentää menetelmien nimet 30 merkkiin, joten joitain menetelmiä ei ehkä ole mahdollista erottaa toisistaan. Valitettavasti Macissa ei ole keinoja kutsua profilointia, joten Mac-käyttäjillä ei ole onnea. Kaiken tämän lisäksi Sunin Java-asiakirjasivu (katso Resurssit) ei enää sisällä -prof vaihtoehto). Kuitenkin, jos käyttöympäristösi tukee -prof Vaihtoehtoa joko Vladimir Bulatovin HyperProf- tai Greg White's ProfileViewer -sovelluksella voidaan auttaa tulkitsemaan tuloksia (katso Resurssit).

On myös mahdollista "profiloida" koodi lisäämällä koodiin selkeä ajoitus:

pitkä alku = System.currentTimeMillis (); // tee aikakatkaisu tässä pitkään = System.currentTimeMillis () - start;

System.currentTimeMillis () palauttaa ajan 1/1 000 sekunnissa. Joissakin järjestelmissä, kuten Windows-tietokoneessa, on kuitenkin ajastin, jonka resoluutio on pienempi (paljon pienempi) kuin 1/1 000 sekunnin. Jopa 1/1 000 sekuntia ei ole tarpeeksi pitkä monien toimintojen tarkkaan ajastamiseen. Näissä tapauksissa tai järjestelmissä, joissa on matalan resoluution ajastimet, saattaa olla tarpeen ajoittaa kuinka kauan operaation toistaminen kestää n kertaa ja jaa sitten kokonaisaika n saada todellinen aika. Silloinkin kun profilointi on käytettävissä, tämä tekniikka voi olla hyödyllinen tietyn tehtävän tai operaation ajoituksessa.

Tässä on muutama loppuhuomautus profiloinnista:

  • Ajastaa koodi aina ennen muutosten tekemistä ja niiden jälkeen varmistaaksesi, että muutokset ovat ainakin testialustalla parantaneet ohjelmaa

  • Yritä tehdä jokainen ajoitustesti samoissa olosuhteissa

  • Jos mahdollista, keksi testi, joka ei perustu mihinkään käyttäjän panokseen, koska vaihtelut käyttäjän reaktioissa voivat aiheuttaa tulosten vaihtelun

Benchmark-sovelma

Benchmark-sovelma mittaa tuhannen (tai jopa miljoonan) kertaa toiminnon suorittamiseen tarvittavan ajan, vähentää muiden toimintojen kuin testin suorittamiseen kuluneen ajan (kuten silmukan yleiskustannukset) ja käyttää näitä tietoja sitten laskemaan kuinka kauan kukin operaatio otti. Se suorittaa kutakin testiä noin sekunnin ajan. Yritettäessä poistaa satunnaiset viiveet muista toiminnoista, joita tietokone voi suorittaa testin aikana, se suorittaa jokaisen testin kolme kertaa ja käyttää parhaan tuloksen. Se pyrkii myös poistamaan roskien keräämisen tekijänä testeissä. Tämän takia, mitä enemmän muistia on vertailuarvon käytettävissä, sitä tarkemmat vertailutulokset ovat.