Ohjelmointi

JVM: n suorituskyvyn optimointi, osa 2: kääntäjät

Java-kääntäjät ovat keskeisessä asemassa tässä JVM-suorituskyvyn optimointisarjan toisessa artikkelissa. Eva Andreasson esittelee eri kääntäjärotuja ja vertaa suorituskykyä asiakkaan, palvelimen ja porrastetun kokoamisen avulla. Hän lopettaa yleiskatsauksen tavallisista JVM-optimoinnista, kuten kuolleen koodin poistamisesta, linjauksesta ja silmukan optimoinnista.

Java-kääntäjä on Java-kuuluisan alustan riippumattomuuden lähde. Ohjelmistokehittäjä kirjoittaa parhaan Java-sovelluksen, jonka hän pystyy, ja kääntäjä työskentelee kulissien takana tuottamaan tehokasta ja hyvin toimivaa suorituskoodia aiotulle kohdealustalle. Erilaiset kääntäjät vastaavat erilaisiin sovellustarpeisiin ja tuottavat siten haluttuja suorituskykytuloksia. Mitä enemmän ymmärrät kääntäjistä, niiden toiminnasta ja käytettävissä olevista, sitä enemmän pystyt optimoimaan Java-sovellusten suorituskyvyn.

Tämä toinen artikkeli JVM: n suorituskyvyn optimointi -sarja tuo esiin ja selittää eroja Java-virtuaalikoneen kääntäjien välillä. Keskustelen myös joistakin Just-In-Time (JIT) -kääntäjien Java-sovellusten yleisistä optimoinnista. (Katso JVM: n suorituskyvyn optimointi, osa 1 "JVM: n yleiskatsaus ja johdanto sarjaan.)

Mikä on kääntäjä?

Yksinkertaisesti sanottuna a kääntäjä ottaa ohjelmointikielen tuloksi ja tuottaa suoritettavan kielen ulostulona. Yksi yleisesti tunnettu kääntäjä on javac, joka sisältyy kaikkiin tavallisiin Java-kehityspaketteihin (JDK). javac ottaa Java-koodin syötteeksi ja kääntää sen tavukoodiksi - JVM: n suoritettavaksi kieleksi. Tavukoodi tallennetaan .class-tiedostoihin, jotka ladataan Java-ajonaikaan, kun Java-prosessi käynnistetään.

Bytecodea ei voi lukea tavallisilla suorittimilla, ja se on käännettävä käskykielelle, jonka taustalla oleva suoritusalusta voi ymmärtää. JVM: n komponentti, joka on vastuussa tavukoodin kääntämisestä suoritettaville alustaohjeille, on jälleen yksi kääntäjä. Jotkut JVM-kääntäjät käsittelevät useita käännöstasoja; esimerkiksi kääntäjä saattaa luoda tavutason välitason edustuksen eri tasoja ennen kuin se muuttuu varsinaisiksi koneohjeiksi, viimeiseksi käännöksen vaiheeksi.

Bytecode ja JVM

Jos haluat lisätietoja tavukoodista ja JVM: stä, katso "Bytecode-perusteet" (Bill Venners, JavaWorld).

Alusta-agnostisesta näkökulmasta haluamme pitää koodialustasta riippumattoman mahdollisimman pitkälle, jotta viimeinen käännöstaso - alimmasta esityksestä todelliseen konekoodiin - on vaihe, joka lukitsee suorituksen tietyn alustan prosessoriarkkitehtuuriin . Suurin ero on staattisten ja dynaamisten kääntäjien välillä. Sieltä meillä on vaihtoehtoja riippuen siitä, mihin toteutusympäristöön kohdistamme, mitä suorituskykytuloksia haluamme ja mitä resurssirajoituksia meidän on noudatettava. Keskustelin lyhyesti staattisista ja dynaamisista kääntäjistä tämän sarjan osassa 1. Seuraavissa osissa selitän hieman enemmän.

Staattinen vs. dynaaminen kokoelma

Esimerkiksi staattinen kääntäjä on aiemmin mainittu javac. Staattisissa kääntäjissä tulokoodi tulkitaan kerran ja suoritettava lähtö on muodossa, jota käytetään ohjelman suorituksen yhteydessä. Ellet tee muutoksia alkuperäiseen lähteeseesi ja käännä koodia uudelleen (kääntäjän avulla), tulos tuottaa aina saman tuloksen; tämä johtuu siitä, että tulo on staattinen tulo ja kääntäjä on staattinen kääntäjä.

Staattisessa kokoelmassa seuraava Java-koodi

staattinen int add7 (int x) {return x + 7; }

aiheuttaisi jotain samanlaista kuin tämä tavukoodi:

iload0 bipush 7 iadd palaa

Dynaaminen kääntäjä kääntää kielen toisesta dynaamisesti, mikä tarkoittaa, että se tapahtuu koodin suorituksen aikana - ajon aikana! Dynaaminen kääntäminen ja optimointi antavat ajonaikalle edun siitä, että ne voivat sopeutua sovelluksen kuormituksen muutoksiin. Dynaamiset kääntäjät soveltuvat erittäin hyvin Java-ajonaikaisuuksiin, jotka suoritetaan yleensä arvaamattomissa ja jatkuvasti muuttuvissa ympäristöissä. Useimmat JVM: t käyttävät dynaamista kääntäjää, kuten Just-In-Time (JIT) -kääntäjää. Saalis on, että dynaamiset kääntäjät ja koodin optimointi tarvitsevat joskus ylimääräisiä tietorakenteita, ketjuja ja suorittimen resursseja. Mitä edistyneempi optimointi tai tavukoodikontekstianalyysi on, sitä enemmän resursseja kuluttaa kokoaminen. Useimmissa ympäristöissä yleiskustannukset ovat edelleen hyvin pienet verrattuna lähtökoodin merkittävään suorituskyvyn kasvuun.

JVM-lajikkeet ja Java-alustan riippumattomuus

Kaikilla JVM-toteutuksilla on yksi yhteinen piirre, joka on heidän yritys saada sovelluksen tavukoodi muunnettua koneohjeiksi. Jotkut JVM: t tulkitsevat sovelluskoodia kuormituksessa ja käyttävät suorituskykylaskureita keskittyäkseen "kuumaan" koodiin. Jotkut JVM: t ohittavat tulkinnan ja luottavat yksinomaan kokoamiseen. Kokoamisen resurssiintensiivisyys voi olla suurempi hitti (erityisesti asiakaspuolen sovelluksissa), mutta se mahdollistaa myös edistyneemmät optimoinnit. Katso lisätietoja Resursseista.

Jos olet aloittelija Java-sovelluksessa, JVM: ien monimutkaisuudet ovat paljon kääriä pääsi ympärille. Hyvä uutinen on, että sinun ei todellakaan tarvitse! JVM hallitsee koodin kääntämistä ja optimointia, joten sinun ei tarvitse huolehtia koneohjeista ja optimaalisesta tapasta kirjoittaa sovelluskoodi taustalla olevalle alustan arkkitehtuurille.

Java-tavukoodista suoritukseen

Kun Java-koodisi on koottu tavukoodiksi, seuraavat vaiheet on kääntää tavukoodin ohjeet konekoodiksi. Tämän voi tehdä joko tulkki tai kääntäjä.

Tulkinta

Yksinkertaisinta tavukoodikokoelman muotoa kutsutaan tulkinnaksi. An tulkki yksinkertaisesti etsii jokaisen tavukoodikäskyn laitteisto-ohjeet ja lähettää sen suorittamaan suorittimen.

Voisit ajatella tulkinta samanlainen kuin sanakirjan käyttö: tietylle sanalle (tavukoodikäsky) on tarkka käännös (konekoodikäsky). Koska tulkki lukee ja suorittaa heti yhden tavukoodin käskyn kerrallaan, ei ole mahdollisuutta optimoida komentosarjaa. Tulkin on myös tehtävä tulkinta aina, kun tavukoodi kutsutaan, mikä tekee siitä melko hitaan. Tulkinta on tarkka tapa suorittaa koodi, mutta optimoimaton ulostulokäskyjoukko ei todennäköisesti ole tehokkain sekvenssi kohdealustan prosessorille.

Kokoelma

A kääntäjä toisaalta lataa koko suoritettavan koodin ajonaikaan. Kun se kääntää tavukoodia, sillä on kyky tarkastella koko tai osittaista ajonaikaisen kontekstia ja tehdä päätöksiä koodin oikeasta kääntämisestä. Sen päätökset perustuvat koodikaavioiden, kuten komentojen eri suoritushaarojen ja ajonaikaisen kontekstidatan, analyysiin.

Kun tavukoodisekvenssi käännetään konekoodikäskyjoukoksi ja optimoinnit voidaan tehdä tälle käskyjoukolle, korvaava käskyjoukko (esim. Optimoitu sekvenssi) tallennetaan rakenteeseen, jota kutsutaan koodivälimuisti. Seuraavan kerran, kun tavukoodi suoritetaan, aiemmin optimoitu koodi voidaan välittömästi sijoittaa koodivälimuistiin ja käyttää suoritukseen. Joissakin tapauksissa suorituskykylaskuri saattaa potkaista ja ohittaa edellisen optimoinnin, jolloin kääntäjä suorittaa uuden optimointijärjestyksen. Koodivälimuistin etuna on, että tuloksena oleva komentojoukko voidaan suorittaa kerralla - ei tarvita tulkitsevaa hakua tai kääntämistä! Tämä nopeuttaa suoritusaikaa, erityisesti Java-sovelluksissa, joissa samoja menetelmiä kutsutaan useita kertoja.

Optimointi

Dynaamisen kokoamisen ohella tulee mahdollisuus lisätä suorituskyvyn laskureita. Kääntäjä saattaa esimerkiksi lisätä a suorituskykylaskuri laskea joka kerta, kun tavukoodilohko (esim. vastaava tietty menetelmä) kutsuttiin. Kääntäjät käyttävät tietoja siitä, kuinka "kuuma" tietty tavukoodi on, selvittääkseen, missä koodin optimoinnit vaikuttavat parhaiten käynnissä olevaan sovellukseen. Suorituksenaikaisen profiloinnin avulla kääntäjä voi tehdä monipuolisen koodinoptimointipäätelmän lennossa, mikä parantaa koodin suorittamisen suorituskykyä entisestään. Kun tarkennettua koodiprofilointitietoa tulee saataville, sitä voidaan käyttää tekemään uusia ja parempia optimointipäätöksiä, kuten: kuinka järjestää käskyt paremmin käännetylle kielelle, korvaako komentosarja tehokkaammilla sarjoilla vai jopa poistetaanko tarpeettomat toiminnot.

Esimerkki

Harkitse Java-koodia:

staattinen int add7 (int x) {return x + 7; }

Tämän voisi staattisesti koota javac tavukoodiin:

iload0 bipush 7 iadd palaa

Kun menetelmää kutsutaan, tavukoodilohko käännetään dynaamisesti koneen ohjeiden mukaan. Kun suorituskykylaskuri (jos se on koodilohkossa) saavuttaa kynnyksen, se voi myös optimoida. Lopputulos voi näyttää seuraavalta koneen käskyjoukolta tietylle suoritusalustalle:

lea rax, [rdx + 7] ret

Erilaiset kääntäjät eri sovelluksiin

Eri Java-sovelluksilla on erilaiset tarpeet. Pitkään käynnissä olevat yrityspalvelinpuolen sovellukset voivat sallia enemmän optimointeja, kun taas pienemmät asiakaspuolen sovellukset saattavat tarvita nopeaa suoritusta minimaalisella resurssien kulutuksella. Tarkastellaan kolmea eri kääntäjäasetusta ja niiden etuja ja haittoja.

Asiakaspuolen kääntäjät

Tunnettu optimoiva kääntäjä on C1, kääntäjä, joka otetaan käyttöön -asiakas JVM-käynnistysvaihtoehto. Kuten sen käynnistysnimi viittaa, C1 on asiakaspuolen kääntäjä. Se on suunniteltu asiakaspuolen sovelluksille, joilla on vähemmän resursseja ja jotka ovat usein herkkiä sovelluksen käynnistymisajalle. C1 käyttää suorituskyvyn laskureita koodin profilointiin yksinkertaisten, suhteellisen häiritsemättömien optimointien mahdollistamiseksi.

Palvelinpuolen kääntäjät

Pitkäkestoisissa sovelluksissa, kuten palvelinpuolen yritys Java-sovelluksissa, asiakaspuolen kääntäjä ei välttämättä riitä. Sen sijaan voitaisiin käyttää palvelinpuolen kääntäjää, kuten C2. C2 otetaan yleensä käyttöön lisäämällä JVM-käynnistysvaihtoehto -palvelin Käynnistyksen komentoriville. Koska useimpien palvelinpuolen ohjelmien odotetaan toimivan pitkään, C2: n käyttöönotto tarkoittaa, että pystyt keräämään enemmän profilointitietoja kuin lyhytaikaisessa kevyessä asiakassovelluksessa. Joten voit käyttää kehittyneempiä optimointitekniikoita ja algoritmeja.

Vinkki: Lämmitä palvelinpuolen kääntäjääsi

Palvelinpuolen käyttöönotossa voi kestää jonkin aikaa, ennen kuin kääntäjä on optimoinut koodin alkuperäiset "kuumat" osat, joten palvelinpuolen käyttöönotto vaatii usein "lämpenemisvaiheen". Ennen kuin suoritat minkäänlaista suorituskyvyn mittausta palvelinpuolen käyttöönotossa, varmista, että sovelluksesi on saavuttanut vakaan tilan! Jos annat kääntäjälle riittävästi aikaa oikeaan kokoamiseen, se hyödyttää sinua! (Katso JavaWorld-artikkelista "Watch your HotSpot compiler go" saadaksesi lisätietoja kääntäjän lämmittämisestä ja profiloinnin mekaniikasta.)

Palvelinkääntäjä muodostaa enemmän profilointitietoja kuin asiakaspuolen kääntäjä, ja mahdollistaa monimutkaisemman haara-analyysin, mikä tarkoittaa, että se harkitsee, mikä optimointipolku olisi hyödyllisempi. Enemmän profilointitietoja antaa parempia sovellustuloksia. Tietysti laajemman profiloinnin ja analyysin tekeminen vaatii enemmän resursseja kääntäjälle. JVM, jossa C2 on käytössä, käyttää enemmän ketjuja ja enemmän suorittimen jaksoja, vaatii suuremman koodivälimuistin ja niin edelleen.

Porrastettu kokoelma

Porrastettu kokoelma yhdistää asiakas- ja palvelinpuolen kokoamisen. Azul teki ensin porrastetun kokoelman saatavana Zing JVM: ssä. Viime aikoina (Java SE 7: stä lähtien) sen on ottanut käyttöön Oracle Java Hotspot JVM. Porrastettu kokoaminen hyödyntää sekä asiakas- että palvelinkääntäjän etuja JVM: ssäsi. Asiakaskääntäjä on aktiivisin sovelluksen käynnistyksen aikana ja hoitaa optimoinnin, jonka laukaisevat alhaisemmat suorituskykylaskurin kynnysarvot. Asiakaspuolen kääntäjä lisää myös suorituskykylaskureita ja valmistelee käskyjoukot edistyneemmille optimoinnille, joihin palvelinpuolen kääntäjä vastaa myöhemmin. Porrastettu kokoaminen on erittäin resurssitehokas tapa profiloida, koska kääntäjä pystyy keräämään tietoja vähäisillä kääntäjillä, joita voidaan käyttää myöhemmin edistyneempiin optimointeihin. Tämä lähestymistapa tuottaa myös enemmän tietoa kuin saat käyttämällä vain tulkittuja koodiprofiililaskureita.

Kuvion 1 kaavamalli kuvaa suorituskyvyn eroja puhtaan tulkinnan, asiakaspuolen, palvelinpuolen ja porrastetun kokoamisen välillä. X-akseli näyttää suoritusajan (aikayksikkö) ja Y-akselin suorituskyvyn (ops / aikayksikkö).

Kuva 1. Suorituskykyerot kääntäjien välillä (napsauta suurentaaksesi)

Pelkästään tulkittuun koodiin verrattuna asiakaspuolen kääntäjän käyttö johtaa noin 5-10 kertaa parempaan suoritustehoon (ops / s), mikä parantaa sovellusten suorituskykyä. Vahvistuksen vaihtelu riippuu tietysti kääntäjän tehokkuudesta, mitkä optimoinnit ovat käytössä tai toteutettu, ja (vähemmässä määrin) siitä, kuinka hyvin sovellus on suunniteltu suorituksen kohdealustan suhteen. Jälkimmäinen on tosiasia, josta Java-kehittäjien ei pitäisi koskaan olla huolissaan.

Verrattuna asiakaspuolen kääntäjään palvelinpuolen kääntäjä yleensä lisää koodin suorituskykyä mitattavissa olevalla 30 prosentilla 50 prosenttiin. Useimmissa tapauksissa suorituskyvyn parantaminen tasapainottaa resurssien lisäkustannukset.