Ohjelmointi

Java-suorituskyvyn ohjelmointi, osa 2: Valukustannukset

Tämän Java-suorituskykysarjamme toisen artikkelin kohdalla painopiste siirtyy castingiin - mikä se on, mitä se maksaa ja miten voimme (joskus) välttää sen. Tässä kuussa aloitamme nopealla luokkien, esineiden ja viitteiden perusteiden tarkastelulla, jonka jälkeen tarkastelemme joitain vakavimpia suorituskykyä koskevia lukuja (sivupalkissa, jotta ei loukata rypälöitä!) Ja ohjeita tietyntyyppiset toiminnot, jotka todennäköisesti aiheuttavat Java Virtual Machine (JVM) -häiriöitä. Lopuksi lopetamme perusteellisen tarkastelun siitä, miten voimme välttää yleisimmät luokan jäsentävät vaikutukset, jotka voivat aiheuttaa valun.

Java-suorituskyvyn ohjelmointi: Lue koko sarja!

  • Osa 1. Opi vähentämään ohjelman yleiskustannuksia ja parantamaan suorituskykyä ohjaamalla objektien luomista ja roskien keräystä
  • Osa 2. Vähennä yleisiä ja suoritusvirheitä tyyppiturvallisella koodilla
  • Osa 3. Katso, kuinka kokoelmavaihtoehdot mittaavat suorituskykyä, ja selvitä, miten saat kaiken irti kustakin tyypistä

Objekti- ja viittaustyypit Javassa

Viime kuussa keskustelimme primitiivisten tyyppien ja Java-objektien välisestä peruserosta. Kielimääritys vahvistaa sekä primitiivisten tyyppien lukumäärän että niiden väliset suhteet (etenkin muunnokset tyyppien välillä). Objektit ovat toisaalta rajoittamattomia ja voivat liittyä mihin tahansa määrään muita tyyppejä.

Jokainen Java-ohjelman luokan määritelmä määrittää uuden tyyppisen objektin. Tämä sisältää kaikki Java-kirjastojen luokat, joten mikä tahansa ohjelma voi käyttää satoja tai jopa tuhansia erityyppisiä objekteja. Muutamilla näistä tyyppeistä Java-kielen määritelmä määrittelee olevan tiettyjä erityisiä käyttötarkoituksia tai käsittelyä (kuten java.lang.StringBuffer varten java.lang.String ketjutusoperaatiot). Näiden muutamien poikkeusten lisäksi Java-kääntäjä ja ohjelman suorittamiseen käytetty JVM käsittelevät kaikkia tyyppejä periaatteessa samalla tavalla.

Jos luokan määritelmässä ei määritetä ( ulottuu lauseke luokan määritelmän otsikossa) toisen luokan vanhempana tai yläluokana, se implisiittisesti laajentaa java.lang.objekti luokassa. Tämä tarkoittaa, että jokainen luokka lopulta laajenee java.lang.objekti, joko suoraan tai yhden tai useamman emoluokan tason sekvenssin kautta.

Objektit itse ovat aina luokkien ja esineiden esiintymiä tyyppi on luokka, jonka se on esiintymä. Jaavassa emme kuitenkaan koskaan käsittele suoraan esineitä; työskentelemme viittausten kanssa esineisiin. Esimerkiksi rivi:

 java.awt.Komponentti myComponent; 

ei luo java.awt.Komponentti esine; se luo tyypin viitemuuttujan java.lang.Komponentti. Vaikka viitteillä on tyyppejä aivan kuten esineillä, viite- ja objektityyppien välillä ei ole tarkkaa vastaavuutta - viite-arvo voi olla tyhjä, objektin, joka on samantyyppinen kuin viite, tai minkä tahansa alaluokan (ts. luokan, josta laskeutuu) objektin, johon viitetyyppi kuuluu. Tässä erityisessä tapauksessa java.awt.Komponentti on abstrakti luokka, joten tiedämme, että saman tyyppistä objektia kuin viitteemme ei voi koskaan olla, mutta varmasti voi olla saman tyyppisiä alaluokkia.

Polymorfismi ja valu

Viitteen tyyppi määrittää, miten viitattu objekti - eli objektia, joka on viitteen arvo - voidaan käyttää. Esimerkiksi yllä olevassa esimerkissä koodaa käyttämällä myComponent voisi käyttää mitä tahansa luokan määrittelemää menetelmää java.awt.Komponenttitai mikä tahansa sen yläluokista viitatulle objektille.

Puhelun tosiasiallisesti suorittaman menetelmän ei kuitenkaan määrää itse viitteen tyyppi, vaan pikemminkin viitatun objektin tyyppi. Tämä on polymorfismi - alaluokat voivat ohittaa emoluokassa määritellyt menetelmät erilaisen käyttäytymisen toteuttamiseksi. Esimerkkimuuttujamme tapauksessa, jos viitattu objekti oli itse asiassa esimerkki java.awt.Button, tilan muutos, joka johtuu a setLabel ("Push Me") puhelu olisi erilainen kuin tuloksena, jos viitattu objekti olisi java.awt.Label.

Luokkamääritysten lisäksi Java-ohjelmat käyttävät myös rajapintamääritelmiä. Ero käyttöliittymän ja luokan välillä on se, että rajapinta määrittää vain joukon käyttäytymismalleja (ja joissakin tapauksissa vakioita), kun taas luokka määrittelee toteutuksen. Koska rajapinnat eivät määrittele toteutuksia, objektit eivät koskaan voi olla käyttöliittymän esiintymiä. Ne voivat kuitenkin olla luokkien esimerkkejä, jotka toteuttavat käyttöliittymän. Viitteet voi olla liitäntätyyppejä, jolloin viitatut objektit voivat olla minkä tahansa luokan rajapintoja, jotka toteuttavat rajapinnan (joko suoraan tai jonkin esi-isäluokan kautta).

Valu käytetään muuntamaan tyyppien välillä - etenkin viitetyyppien välillä - sen tyyppiseen valuoperaatioon, josta olemme kiinnostuneita täällä. Upcast-toiminnot (kutsutaan myös tuloksen laajentaminen Java-kielimäärityksessä) muuntaa alaluokaviittauksen esi-luokan viitteeksi. Tämä suoratoisto on normaalisti automaattinen, koska se on aina turvallista ja kääntäjä voi toteuttaa sen suoraan.

Downcast-toiminnot (kutsutaan myös kaventamalla tuloksia Java-kielimäärityksessä) muuntaa esi-luokan viittauksen alaluokaviitteeksi. Tämä suoratoisto luo suorituksen yleiskustannuksia, koska Java edellyttää, että suoratoisto tarkistetaan ajon aikana sen pätevyyden varmistamiseksi. Jos viitattu objekti ei ole näyttelijän kohdetyypin tai tämän tyyppisen alaluokan esiintymä, heittoyritys ei ole sallittu, ja sen on heitettävä java.lang.ClassCastException.

esiintymä Java-operaattorin avulla voit määrittää, onko tietty suoratoisto sallittu vai ei, yrittämättä sitä itse. Koska tarkastuksen suorituskustannukset ovat paljon pienemmät kuin luvattoman näyttökokeen tuottaman poikkeuksen kustannukset, on yleensä viisasta käyttää esiintymä testaa milloin tahansa, et ole varma, että viittaustyyppi on haluamasi. Ennen kuin teet niin, sinun on kuitenkin varmistettava, että sinulla on kohtuullinen tapa käsitellä ei-toivotun tyyppistä viittausta - muuten voit myös vain antaa poikkeuksen heittää ja käsitellä sitä korkeammalla tasolla koodissasi.

Varovaisuutta tuulille

Casting sallii yleisen ohjelmoinnin käytön Java-sovelluksessa, jossa koodi kirjoitetaan toimimaan kaikkien joidenkin perusluokkien (usein java.lang.objekti, hyötyluokille). Castingin käyttö aiheuttaa kuitenkin ainutlaatuisia ongelmia. Seuraavassa osassa tarkastellaan vaikutusta suorituskykyyn, mutta tarkastellaan ensin vaikutusta itse koodiin. Tässä on näyte, joka käyttää yleistä java.lang. vektori kokoelma luokka:

 yksityinen vektori someNumbers; ... public void doSomething () {... int n = ... Kokonaisluku = (Kokonaisluku) someNumbers.elementAt (n); ...} 

Tämä koodi aiheuttaa mahdollisia ongelmia selkeyden ja ylläpidettävyyden suhteen. Jos joku muu kuin alkuperäinen kehittäjä muuttaisi koodia jossain vaiheessa, hän voisi kohtuudella ajatella voivansa lisätä a java.lang.Tupla että jotkutNumerot kokoelmista, koska tämä on java.lang.Määrä. Kaikki sujuisi hyvin, jos hän yritti tätä, mutta jossain määrittelemättömässä toteutushetkessä hän todennäköisesti saisi java.lang.ClassCastException heitetään, kun yritys heittää a java.lang.I kokonaisluku teloitettiin lisäarvonsa vuoksi.

Ongelmana on, että suoratoiston käyttö ohittaa Java-kääntäjän sisäänrakennetut turvatarkastukset; ohjelmoija päätyy etsimään virheitä suorituksen aikana, koska kääntäjä ei löydä niitä. Tämä ei ole sinänsä katastrofaalista, mutta tämäntyyppinen käyttövirhe piiloutuu usein melko taitavasti testatessasi koodia, vain paljastamaan itsensä, kun ohjelma otetaan käyttöön.

Ei ole yllättävää, että tuki tekniikalle, jonka avulla kääntäjä pystyy havaitsemaan tämäntyyppiset käyttövirheet, on yksi eniten pyydetyistä Java-parannuksista. Java-yhteisöprosessissa on nyt käynnissä projekti, joka tutkii juuri tämän tuen lisäämistä: projektinumero JSR-000014, Lisää yleisiä tyyppejä Java-ohjelmointikielelle (katso lisätietoja alla olevasta Resurssit-osiosta.) Tämän artikkelin jatkeessa ensi kuussa tulevaisuudessa tarkastelemme tätä projektia yksityiskohtaisemmin ja keskustelemme siitä, miten se todennäköisesti auttaa ja mihin se todennäköisesti jättää meidät haluamaan enemmän.

Suorituskykyongelma

On jo pitkään tunnustettu, että suoratoisto voi vahingoittaa Java-suorituskykyä ja että voit parantaa suorituskykyä minimoimalla voimakkaasti käytetyn koodin suoratoiston. Menetelmäkutsuja, erityisesti rajapintakutsuja, mainitaan usein myös mahdollisina suorituskyvyn pullonkauloina. Nykyinen JVM-sukupolvi on kuitenkin edennyt pitkälle edeltäjiltään, ja on syytä tarkistaa, kuinka hyvin nämä periaatteet pitävät kiinni tänään.

Tätä artikkelia varten kehitin sarjan testejä selvittääkseen, kuinka tärkeitä nämä tekijät ovat nykyisten JVM-laitteiden suorituskykyyn. Testitulokset on koottu sivupalkin kahteen taulukkoon, taulukko 1, joka esittää menetelmäpuhelun yleiskustannukset ja taulukko 2 heittokustannukset. Testiohjelman täydellinen lähdekoodi on saatavana myös verkossa (lisätietoja on alla olevassa Resurssit-osiossa).

Yhteenvetona näistä johtopäätöksistä lukijoille, jotka eivät halua kahlata taulukoiden yksityiskohtia, tietyntyyppiset menetelmäpuhelut ja suoratoistot ovat edelleen melko kalliita, joissakin tapauksissa kestää melkein yhtä kauan kuin yksinkertainen objektin allokointi. Mahdollisuuksien mukaan tämän tyyppisiä toimintoja tulisi välttää koodissa, joka on optimoitava suorituskyvyn saavuttamiseksi.

Erityisesti ohitettujen menetelmien (menetelmien, jotka ohitetaan missä tahansa ladatussa luokassa, ei vain objektin todellisessa luokassa), kutsuminen ja liittymien kautta soittaminen ovat huomattavasti kalliimpia kuin yksinkertaiset menetelmäpuhelut. Testissä käytetty HotSpot Server JVM 2.0 -beta muuntaa jopa monet yksinkertaiset menetelmäpuhelut linjakoodiksi välttäen tällaisten toimintojen yleiskustannuksia. Kuitenkin HotSpot näyttää huonoimman suorituskyvyn testattujen JVM: ien joukossa ohitetuille menetelmille ja puheluille rajapintojen kautta.

Suoratoistoa (tietysti downcastingia varten) testatut JVM: t pitävät yleensä suorituskyvyn kohtuullisella tasolla. HotSpot tekee tämän kanssa poikkeuksellisen suuren työn useimmissa vertailutesteissä, ja, kuten menetelmän kutsujenkin, pystyy monissa yksinkertaisissa tapauksissa melkein kokonaan eliminoimaan heittokustannukset. Monimutkaisemmissa tilanteissa, kuten suoratoisto, jota seuraa kutsu ohitettuihin menetelmiin, kaikki testatut JVM: t osoittavat huomattavaa suorituskyvyn heikkenemistä.

Testattu HotSpot-versio osoitti myös erittäin heikkoa suorituskykyä, kun objekti heitettiin peräkkäin eri viitetyyppeihin (sen sijaan, että se heitettäisiin aina samaan kohdetyyppiin). Tämä tilanne syntyy säännöllisesti kirjastoissa, kuten Swingissä, joissa käytetään syvää luokkien hierarkiaa.

Useimmissa tapauksissa molempien menetelmäpuhelujen ja suoratoiston yleiskustannukset ovat pienet verrattuna objektien allokointiaikoihin, joita tarkasteltiin viime kuukauden artikkelissa. Näitä toimintoja käytetään kuitenkin usein paljon useammin kuin objektien allokointeja, joten ne voivat silti olla merkittävä suorituskykyongelmien lähde.

Tämän artikkelin loppuosassa keskustelemme joistakin erityisistä tekniikoista koodin suoratoistotarpeen vähentämiseksi. Tarkastelemme erityisesti sitä, miten valu syntyy usein siitä, miten alaluokat ovat vuorovaikutuksessa perusluokkien kanssa, ja tutkitaan joitain tekniikoita tämäntyyppisen valu poistamiseksi. Ensi kuussa tämän näyttämisen toisessa osassa tarkastelemme toista yleistä syyttä heittämiseen, geneeristen kokoelmien käyttöä.

Perusluokat ja valu

Suoratoistoa on useita yleisiä käyttötapoja Java-ohjelmissa. Esimerkiksi valua käytetään usein joidenkin perusluokan toimintojen yleiseen käsittelyyn, jota voidaan laajentaa useilla alaluokilla. Seuraava koodi esittää jonkin verran keksitty kuvaa tästä käytöstä:

 // yksinkertainen perusluokka alaluokilla public abstract class BaseWidget {...} public class SubWidget extends BaseWidget {... public void doSubWidgetSomething () {...}} ... // perusluokka alaluokilla, käyttäen aikaisempaa sarjaa luokkien julkinen abstrakti luokka BaseGorph {// tähän Gorphin yksityiseen BaseWidget myWidgetiin liittyvä widget; ... // aseta tähän Gorphiin liittyvä Widget (sallittu vain alaluokille) suojattu void setWidget (BaseWidget-widget) {myWidget = widget; } // hanki tähän Gorphin julkiseen BaseWidgetiin liittyvä Widget getWidget () {return myWidget; } ... // palauta Gorph, jolla on jonkin verran yhteyttä tähän Gorphiin // tämä on aina saman tyyppinen kuin sitä kutsutaan, mutta voimme vain // palauttaa perusluokan julkisen abstraktin BaseGorph-esiintymän otherGorph () {. ..}} // Gorph-aliluokka, joka käyttää Widget-aliluokan julkista luokkaa SubGorph laajentaa BaseGorphia {// palauttaa Gorphin, jolla on jokin suhde tähän Gorphin julkiseen BaseGorphiin otherGorph () {...} ... public void anyMethod () {.. . // aseta Widget, jota käytämme SubWidget-widget = ... setWidget (widget); ... // käytä Widgetiä ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // käytä meidän otherGorph SubGorph other = (SubGorph) otherGorph (); ...}}