Programiranje

Programiranje zmogljivosti Java, 2. del: Stroški igranja

Za ta drugi članek v naši seriji o zmogljivosti Jave se poudarek preusmeri na igranje - kaj je to, kaj stane in kako se mu (včasih) lahko izognemo. Ta mesec začnemo s hitrim pregledom osnov razredov, predmetov in referenc, nato pa si ogledamo nekatere trdne številke uspešnosti (v stranski vrstici, da ne bi užalili grozljivega!) In smernice za vrste operacij, ki bodo najverjetneje poslabšale prebavo v vašem navideznem računalniku Java (JVM). Na koncu zaključimo s poglobljenim vpogledom v to, kako se lahko izognemo običajnim učinkom strukturiranja razredov, ki lahko povzročijo predvajanje.

Programiranje zmogljivosti Java: preberite celotno serijo!

  • Del 1. Naučite se, kako zmanjšati režijske stroške programa in izboljšati zmogljivost z nadzorom ustvarjanja predmetov in zbiranja smeti
  • Del 2. Zmanjšajte režijske stroške in napake pri izvajanju s pomočjo varne kode
  • Del 3. Oglejte si, kako se alternative zbirk merijo po uspešnosti, in ugotovite, kako lahko kar najbolje izkoristite vsako vrsto

Predmetni in referenčni tipi v Javi

Prejšnji mesec smo razpravljali o osnovnem razlikovanju med primitivnimi tipi in predmeti v Javi. Število primitivnih tipov in razmerja med njimi (zlasti pretvorbe med tipi) so določeni z definicijo jezika. Predmeti pa so neomejene vrste in so lahko povezani s poljubnim številom drugih vrst.

Vsaka definicija razreda v programu Java definira novo vrsto predmeta. Sem spadajo vsi razredi iz knjižnic Java, zato lahko kateri koli program uporablja na stotine ali celo tisoče različnih vrst predmetov. Za nekatere od teh vrst je v definiciji jezika Java določeno, da imajo določeno posebno rabo ali ravnanje (na primer uporaba java.lang.StringBuffer za java.lang.String postopki združevanja). Razen teh nekaj izjem pa prevajalnik Java in JVM, ki se uporablja za izvajanje programa, vse tipe obravnavata v bistvu enako.

Če definicija razreda ne določa (s pomočjo podaljša v glavi definicije razreda) drug razred kot nadrejeni ali superrazred, implicitno razširi java.lang.Object razred. To pomeni, da se vsak razred na koncu razširi java.lang.Object, bodisi neposredno bodisi prek zaporedja ene ali več ravni nadrejenih razredov.

Predmeti sami so vedno primerki razredov in predmetov tip je razred, katerega primerek je. V Javi pa nikoli ne delamo neposredno s predmeti; delamo s sklici na predmete. Na primer vrstica:

 java.awt.Component myComponent; 

ne ustvari java.awt.Component predmet; ustvari referenčno spremenljivko tipa java.lang.Component. Čeprav imajo sklici vrste tako kot predmeti, med referencami in vrstami predmetov ni natančnega ujemanja - referenčna vrednost je lahko nič, predmet iste vrste kot sklic ali predmet katerega koli podrazreda (tj. razreda, ki izvira iz) vrste sklica. V tem primeru java.awt.Component je abstrakten razred, zato vemo, da nikoli ne more biti predmeta istega tipa kot naša referenca, zagotovo pa lahko obstajajo predmeti podrazredov tega referenčnega tipa.

Polimorfizem in litje

Vrsta sklica določa, kako predmet, na katerega se sklicuje - torej objekt, ki je vrednost sklica - se lahko uporabi. Na primer, v zgornjem primeru uporabite kodo myComponent lahko prikliče katero koli metodo, ki jo definira razred java.awt.Componentali katerega koli od njegovih super razredov na referenciranem predmetu.

Vendar metoda, ki se dejansko izvede s klicem, ne določa vrsta samega sklica, temveč vrsta referenčnega predmeta. To je osnovno načelo polimorfizem - podrazredi lahko preglasijo metode, definirane v nadrejenem razredu, da bi izvedli drugačno vedenje. V primeru naše vzorčne spremenljivke, če je bil referenčni objekt dejansko primerek java.awt.Button, sprememba stanja, ki je posledica a setLabel ("Push Me") klic bi se razlikoval od tistega, ki bi izhajal, če bi bil referenčni objekt primerek java.awt.Label.

Poleg definicij razredov programi Java uporabljajo tudi definicije vmesnikov. Razlika med vmesnikom in razredom je v tem, da vmesnik določa le nabor vedenj (in v nekaterih primerih konstante), medtem ko razred definira izvedbo. Ker vmesniki ne določajo izvedb, predmeti nikoli ne morejo biti primerki vmesnika. Lahko pa so primerki razredov, ki izvajajo vmesnik. Reference lahko biti tipov vmesnikov, v tem primeru so lahko referenčni predmeti primerki katerega koli razreda, ki izvaja vmesnik (bodisi neposredno bodisi prek nekega razreda prednika).

Casting se uporablja za pretvorbo med vrstami - zlasti med referenčnimi vrstami za vrsto operacije ulivanja, ki nas tukaj zanima. Posodobljene operacije (imenovano tudi razširitev konverzij v specifikaciji jezika Java) pretvori sklic na podrazred v sklic na razred prednika. Ta operacija ulivanja je običajno samodejna, saj je vedno varna in jo lahko neposredno izvede prevajalnik.

Operacije navzdol (imenovano tudi zoženje pretvorb v specifikaciji jezika Java) pretvori sklic na razred prednika v sklic na podrazred. Ta operacija ulivanja ustvari režijske stroške, ker Java zahteva, da se zasedba med izvajanjem preveri, da se prepriča, ali je veljavna. Če referencirani objekt ni primerek ciljne vrste za igralsko zasedbo ali podrazred te vrste, poskus predvajanja ni dovoljen in mora vrniti java.lang.ClassCastException.

The instanceof Operator v Javi vam omogoča, da ugotovite, ali je določena operacija predvajanja dovoljena, ne da bi dejansko poskusila operacijo. Ker so stroški delovanja preverjanja veliko manjši od stroškov izjeme, ki nastane zaradi nedovoljenega poskusa oddaje, je na splošno pametno uporabiti instanceof preskusite kadar koli niste prepričani, da je vrsta reference takšna, kot bi želeli. Pred tem pa se prepričajte, da imate primeren način za obravnavo sklica neželene vrste - v nasprotnem primeru lahko samo pustite, da se izjema vrže in jo obdelate na višji ravni v svoji kodi.

Previdnost pred vetrovi

Casting omogoča uporabo generičnega programiranja v Javi, kjer je koda napisana za delo z vsemi predmeti razredov, ki izvirajo iz nekega osnovnega razreda (pogosto java.lang.Object, za uporabne razrede). Vendar uporaba litja povzroča edinstven nabor težav. V naslednjem poglavju si bomo ogledali vpliv na zmogljivost, najprej pa si oglejmo učinek na samo kodo. Tu je vzorec z uporabo generičnega java.lang.Vector razred zbiranja:

 zasebni Vector nekajNumbers; ... javna praznina doSomething () {... int n = ... Celo število = = (Celo število) someNumbers.elementAt (n); ...} 

Ta koda predstavlja morebitne težave z vidika jasnosti in vzdrževanja. Če bi kdo, ki ni prvotni razvijalec, kdaj spremenil kodo, bi morda razumno mislil, da bi lahko dodal java.lang.Dvojnik do nekajŠtevilk zbirke, saj gre za podrazred java.lang.Number. Če bi to poskusil, bi se vse dobro sestavilo, toda na neki nedoločeni točki izvršbe bi verjetno dobil java.lang.ClassCastException vržen, ko je poskušal zaigrati igralca java.lang.Integer je bila izvršena za njegovo dodano vrednost.

Tukaj je težava v tem, da uporaba vlivanja obide varnostne preglede, vgrajene v prevajalnik Java; programer na koncu lovi napake med izvajanjem, saj jih prevajalnik ne bo ujel. To samo po sebi ni katastrofalno, vendar se ta vrsta napake pri uporabi pogosto skriva precej pametno, medtem ko preizkušate kodo, le da se razkrije, ko program začne delovati.

Ni presenetljivo, da je podpora tehniki, ki bi prevajalniku omogočila, da zazna tovrstno napako pri uporabi, ena izmed najbolj zahtevnih izboljšav Java. Trenutno poteka projekt v skupnosti Java, ki preiskuje dodajanje samo te podpore: številka projekta JSR-000014, Dodajanje generičnih tipov v programski jezik Java (za podrobnosti glejte spodnji razdelek Viri.) V nadaljevanju tega članka prihodnji mesec bomo ta projekt podrobneje preučili in razpravljali o tem, kako bo verjetno pomagal in kje nas bo verjetno pustil, če želimo še več.

Vprašanje uspešnosti

Že dolgo je znano, da lahko predvajanje škoduje uspešnosti v Javi in ​​da lahko izboljšate zmogljivost tako, da zmanjšate predvajanje v zelo uporabljeni kodi. Pogosto se kot potencialna ozka grla omenjajo tudi klici metod, zlasti klici prek vmesnikov. Sedanja generacija JVM pa je daleč od svojih predhodnikov, zato je vredno preveriti, kako dobro se ta načela držijo danes.

Za ta članek sem razvil vrsto testov, da bi ugotovil, kako pomembni so ti dejavniki za delovanje s trenutnimi JVM-ji. Rezultati testa so povzeti v dve tabeli v stranski vrstici, tabela 1 prikazuje režijske stroške klicev metode in tabelo 2 režijskih stroškov. Celotna izvorna koda za testni program je na voljo tudi na spletu (za več podrobnosti glejte spodnji razdelek Viri).

Če povzamemo te zaključke za bralce, ki se ne želijo prebirati po podrobnostih v tabelah, so nekatere vrste klicev in oddaj metode še vedno precej drage, v nekaterih primerih pa trajajo skoraj tako dolgo kot preprosta dodelitev predmetov. Kjer je mogoče, se je treba takim vrstam operacij izogibati v kodi, ki jo je treba optimizirati za delovanje.

Zlasti klici preglašenih metod (metode, ki so prepisane v katerem koli naloženem razredu, ne le dejanskem razredu predmeta) in klici prek vmesnikov so precej dražji od preprostih klicev metod. HotSpot Server JVM 2.0 beta, uporabljen v testu, bo celo številne preproste klice metode pretvoril v vstavljeno kodo, pri čemer se bo izognil kakršnim koli režijskim stroškom za takšne operacije. Vendar HotSpot kaže najslabše delovanje med preizkušenimi JVM-ji za razveljavljene metode in klice prek vmesnikov.

Preizkušeni JVM za predvajanje (seveda prenašanje navzdol) običajno zadrži uspešnost na razumni ravni. HotSpot s tem opravi izjemno delo pri večini primerjalnih preizkusov in je, tako kot pri klicih metod, v mnogih preprostih primerih skoraj v celoti odpravil režijske stroške. V bolj zapletenih situacijah, kot so predvajanja, ki jim sledijo klici preglašenih metod, imajo vsi preizkušeni JVM opazno poslabšanje zmogljivosti.

Preizkušena različica HotSpot je pokazala tudi izjemno slabo delovanje, ko je bil objekt zaporedoma predan različnim referenčnim vrstam (namesto da bi bil vedno oddan na isti ciljni tip). Ta položaj se redno pojavlja v knjižnicah, kot je Swing, ki uporabljajo globoko hierarhijo razredov.

V večini primerov so režijski stroški tako klicev metode kot vlivanja majhni v primerjavi s časom dodeljevanja predmetov, ki smo si ga ogledali v prejšnjem mesecu. Vendar se bodo te operacije pogosto uporabljale veliko pogosteje kot dodelitve predmetov, zato so lahko še vedno pomemben vir težav z zmogljivostjo.

V nadaljevanju tega članka bomo razpravljali o nekaterih posebnih tehnikah za zmanjšanje potrebe po predvajanju kode. Natančneje, preučili bomo, kako oddajanje pogosto izhaja iz načina interakcije podrazredov z osnovnimi razredi, in raziskali nekatere tehnike za odpravo te vrste ulitkov. Naslednji mesec bomo v drugem delu tega pogleda na kasting obravnavali še en pogost vzrok kastinga, uporabo generičnih zbirk.

Osnovni razredi in vlivanje

Obstaja več pogostih načinov predvajanja v programih Java. Na primer, vlivanje se pogosto uporablja za generično obdelavo nekaterih funkcij v osnovnem razredu, ki jih je mogoče razširiti s številnimi podrazredi. Naslednja koda prikazuje nekoliko namišljeno ponazoritev te uporabe:

 // preprost osnovni razred s podrazredi javni abstraktni razred BaseWidget {...} javni razred SubWidget razširi BaseWidget {... public void doSubWidgetSomething () {...}} ... // osnovni razred s podrazredi, z uporabo predhodnega niza razredov javni abstraktni razred BaseGorph {// gradnik, povezan s tem Gorphovim zasebnim BaseWidget myWidget; ... // nastavimo pripomoček, povezan s tem Gorphom (dovoljen samo za podrazrede), zaščiten void setWidget (pripomoček BaseWidget) {myWidget = widget; } // dobimo pripomoček, povezan s tem Gorphovim javnim BaseWidgetom getWidget () {return myWidget; } ... // vrnemo Gorph-a z neko zvezo s tem Gorph-om // to bo vedno istega tipa, kot je bilo poklicano, lahko pa // vrnemo samo primerek našega javnega povzetka osnovnega razreda BaseGorph otherGorph () {. ..}} // podrazred Gorph z uporabo podrazreda pripomočka javni razred SubGorph razširi BaseGorph {// vrne Gorph z neko povezavo s tem Gorph javnim BaseGorph otherGorph () {...} ... public void anyMethod () {.. . // nastavimo pripomoček, ki ga uporabljamo pripomoček SubWidget = ... setWidget (pripomoček); ... // uporabimo naš pripomoček ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // uporabimo naš otherGorph SubGorph other = (SubGorph) otherGorph (); ...}} 
$config[zx-auto] not found$config[zx-overlay] not found