Programmering

Java ytelsesprogrammering, del 2: Kostnadene ved casting

For denne andre artikkelen i vår serie om Java-ytelse, skifter fokuset til casting - hva det er, hva det koster, og hvordan vi (noen ganger) kan unngå det. Denne måneden starter vi med en rask gjennomgang av det grunnleggende om klasser, objekter og referanser, og følger opp med en titt på noen hardcore-ytelsestall (i et sidefelt for ikke å fornærme det pysete!) Og retningslinjer for typer operasjoner som mest sannsynlig gir Java Virtual Machine (JVM) fordøyelsesbesvær. Til slutt avslutter vi med en grundig titt på hvordan vi kan unngå vanlige klassestrukturerende effekter som kan forårsake støping.

Java ytelsesprogrammering: Les hele serien!

  • Del 1. Lær hvordan du reduserer programomkostninger og forbedrer ytelsen ved å kontrollere gjenstandsoppretting og søppeloppsamling
  • Del 2. Reduser overhead- og kjøringsfeil gjennom typesikker kode
  • Del 3. Se hvordan samlingsalternativer måler seg i ytelse, og finn ut hvordan du får mest mulig ut av hver type

Objekt- og referansetyper i Java

Forrige måned diskuterte vi det grunnleggende skillet mellom primitive typer og objekter i Java. Både antall primitive typer og forholdet mellom dem (spesielt konverteringer mellom typer) er fastlagt av språkdefinisjonen. Objekter er derimot av ubegrensede typer og kan være relatert til et hvilket som helst antall andre typer.

Hver klassedefinisjon i et Java-program definerer en ny type objekt. Dette inkluderer alle klassene fra Java-bibliotekene, så et gitt program kan bruke hundrevis eller til og med tusenvis av forskjellige typer objekter. Noen få av disse typene er spesifisert av Java-språkdefinisjonen som visse spesielle bruksområder eller håndtering (for eksempel bruk av java.lang.StringBuffer til java.lang.Streng sammenkoblingsoperasjoner). Bortsett fra disse få unntakene, behandles imidlertid alle typene i utgangspunktet de samme av Java-kompilatoren og JVM som ble brukt til å utføre programmet.

Hvis en klassedefinisjon ikke spesifiserer (ved hjelp av strekker paragraf i klassedefinisjonsoverskriften) en annen klasse som foreldre eller superklasse, utvider den implisitt java.lang.Objekt klasse. Dette betyr at hver klasse til slutt utvides java.lang.Objekt, enten direkte eller via en sekvens av ett eller flere nivåer av foreldreklasser.

Objekter i seg selv er alltid forekomster av klasser, og et objekt type er klassen det er en forekomst av. I Java har vi aldri direkte med objekter å gjøre; vi jobber med referanser til objekter. For eksempel linjen:

 java.awt.Komponent minKomponent; 

skaper ikke en java.awt.Komponent gjenstand; den lager en referansevariabel av typen java.lang.Komponent. Selv om referanser har typer akkurat som objekter, er det ikke en nøyaktig samsvar mellom referanse og objekttyper - en referanseverdi kan være null, et objekt av samme type som referansen, eller et objekt av en hvilken som helst underklasse (dvs. klasse nedstammet fra) referansetypen. I dette spesielle tilfellet java.awt.Komponent er en abstrakt klasse, så vi vet at det aldri kan være et objekt av samme type som vår referanse, men det kan absolutt være gjenstander for underklasser av den referansetypen.

Polymorfisme og avstøpning

Type referanse bestemmer hvordan referert objekt - det vil si objektet som er verdien av referansen - kan brukes. For eksempel, i eksemplet ovenfor, kode ved hjelp av minKomponent kunne påberope seg noen av metodene som er definert av klassen java.awt.Komponent, eller noen av dens superklasser, på det refererte objektet.

Metoden som faktisk utføres av et anrop bestemmes imidlertid ikke av selve referansen, men heller av typen det refererte objektet har. Dette er det grunnleggende prinsippet om polymorfisme - underklasser kan overstyre metoder som er definert i foreldreklassen for å implementere ulik oppførsel. Når det gjelder eksempelvariabelen vår, hvis det refererte objektet faktisk var en forekomst av java.awt.-knapp, tilstandsendringen som følge av a setLabel ("Push Me") samtalen ville være annerledes enn den som oppstod hvis det refererte objektet var en forekomst av java.awt.Label.

Foruten klassedefinisjoner, bruker Java-programmer også grensesnittdefinisjoner. Forskjellen mellom et grensesnitt og en klasse er at et grensesnitt bare spesifiserer et sett med atferd (og i noen tilfeller konstanter), mens en klasse definerer en implementering. Siden grensesnitt ikke definerer implementeringer, kan objekter aldri være forekomster av et grensesnitt. De kan imidlertid være forekomster av klasser som implementerer et grensesnitt. Referanser kan være av grensesnitttyper, i hvilket tilfelle de refererte objektene kan være forekomster av hvilken som helst klasse som implementerer grensesnittet (enten direkte eller gjennom en forfaderklasse).

Casting brukes til å konvertere mellom typer - spesielt mellom referansetypene, for den typen støpeoperasjoner vi er interessert i her. Upcast operasjoner (også kalt utvide konverteringer i Java Language Specification) konvertere en underklassehenvisning til en forfaderklassehenvisning. Denne støpeoperasjonen er normalt automatisk, siden den alltid er trygg og kan implementeres direkte av kompilatoren.

Nedstengte operasjoner (også kalt innsnevring av konverteringer i Java Language Specification) konverterer en forfedreklassehenvisning til en underklassereferanse. Denne avstøpningsoperasjonen skaper utførelse overhead, siden Java krever at rollebesetningen sjekkes ved kjøretid for å sikre at den er gyldig. Hvis det refererte objektet ikke er en forekomst av verken måltypen for rollebesetningen eller en underklasse av den typen, er forsøket ikke tillatt og må kaste et java.lang.ClassCastException.

De tilfelle av operatør i Java lar deg bestemme om en spesifikk casting-operasjon er tillatt eller ikke uten å faktisk prøve å operere. Siden ytelseskostnaden for en sjekk er mye mindre enn unntaket som genereres av et ikke-tillatt rollebesetningsforsøk, er det generelt lurt å bruke en tilfelle av test når du er usikker på at typen referanse er slik du vil at den skal være. Før du gjør det, bør du imidlertid sørge for at du har en rimelig måte å håndtere en referanse av en uønsket type - ellers kan du like godt bare la unntaket kastes og håndtere det på et høyere nivå i koden din.

Kaster forsiktighet mot vindene

Casting tillater bruk av generell programmering i Java, der koden skrives for å fungere med alle objekter fra klasser som kommer fra en eller annen basisklasse (ofte java.lang.Objekt, for nytteklasser). Imidlertid forårsaker bruken av støping et unikt sett med problemer. I neste avsnitt ser vi på innvirkningen på ytelsen, men la oss først vurdere effekten på selve koden. Her er et utvalg med generisk java.lang.Vector samlingsklasse:

 private Vector someNumbers; ... public void doSomething () {... int n = ... Integer number = (Integer) someNumbers.elementAt (n); ...} 

Denne koden presenterer potensielle problemer når det gjelder klarhet og vedlikehold. Hvis noen andre enn den opprinnelige utvikleren skulle endre koden på et tidspunkt, kunne han med rimelighet tro at han kunne legge til en java.lang. dobbelt til noenNumre samlinger, siden dette er en underklasse av java.lang.Nummer. Alt ville kompilere fint hvis han prøvde dette, men på et ubestemt tidspunkt i utførelsen ville han sannsynligvis få en java.lang.ClassCastException kastet da forsøket kastet til en java.lang. heltall ble henrettet for sin merverdi.

Problemet her er at bruk av casting omgår sikkerhetskontrollene som er innebygd i Java-kompilatoren; programmereren ender opp med å jakte på feil under utførelse, siden kompilatoren ikke får tak i dem. Dette er ikke katastrofalt i seg selv, men denne typen bruksfeil skjuler ofte ganske smart mens du tester koden din, bare for å avsløre seg når programmet blir satt i produksjon.

Ikke overraskende er støtte for en teknikk som gjør at kompilatoren kan oppdage denne typen bruksfeil, en av de mest etterspurte forbedringene av Java. Det er et prosjekt som nå pågår i Java Community-prosessen som undersøker å legge til akkurat denne støtten: prosjektnummer JSR-000014, Legg til generiske typer til Java-programmeringsspråket (se Ressurser-delen nedenfor for mer informasjon.) I fortsettelsen av denne artikkelen, neste måned, vil vi se nærmere på dette prosjektet og diskutere både hvordan det sannsynligvis vil hjelpe og hvor det sannsynligvis vil gi oss lyst til mer.

Performance-problemet

Det har lenge vært anerkjent at casting kan være skadelig for ytelse i Java, og at du kan forbedre ytelsen ved å minimere casting i mye brukt kode. Metodeanrop, spesielt samtaler via grensesnitt, blir også ofte nevnt som potensielle ytelsesflaskehalser. Den nåværende generasjonen av JVM-er har kommet langt fra sine forgjengere, og det er verdt å sjekke for å se hvor godt disse prinsippene holder i dag.

For denne artikkelen utviklet jeg en serie tester for å se hvor viktige disse faktorene er for ytelse med gjeldende JVM. Testresultatene er oppsummert i to tabeller i sidefeltet, tabell 1 som viser metoden samtaleomkostninger og tabell 2 avstøpningsomkostninger. Hele kildekoden for testprogrammet er også tilgjengelig online (se Ressurser-delen nedenfor for mer informasjon).

For å oppsummere disse konklusjonene for lesere som ikke vil bla gjennom detaljene i tabellene, er visse typer metodesamtaler og kaster fremdeles ganske dyre, i noen tilfeller tar det nesten like lang tid som en enkel objektallokering. Når det er mulig, bør denne typen operasjoner unngås i kode som må optimaliseres for ytelse.

Spesielt er anrop til overstyrte metoder (metoder som overstyres i en hvilken som helst lastet klasse, ikke bare den faktiske klassen til objektet) og samtaler gjennom grensesnitt betydelig dyrere enn enkle metodeanrop. HotSpot Server JVM 2.0 beta brukt i testen vil til og med konvertere mange enkle metodesamtaler til inline-kode, og unngå overhead for slike operasjoner. Imidlertid viser HotSpot den verste ytelsen blant de testede JVM-ene for overstyrte metoder og samtaler gjennom grensesnitt.

For casting (downcasting, selvfølgelig) holder de testede JVM-ene generelt ytelsen på et rimelig nivå. HotSpot gjør en eksepsjonell jobb med dette i det meste av referansetesten, og er, som med metoden kaller, i mange enkle tilfeller i stand til å eliminere kostnadene til casting nesten fullstendig. For mer kompliserte situasjoner, for eksempel rollebesetninger etterfulgt av anrop til overstyrte metoder, viser alle de testede JVM-ene merkbar ytelsesforringelse.

Den testede versjonen av HotSpot viste også ekstremt dårlig ytelse når et objekt ble kastet til forskjellige referansetyper etter hverandre (i stedet for alltid å bli kastet til samme måltype). Denne situasjonen oppstår regelmessig i biblioteker som Swing som bruker et dypt hierarki av klasser.

I de fleste tilfeller er kostnadene for begge metodeanrop og avstøpning liten i forhold til objekttildelingstidene som ble sett på i forrige måneds artikkel. Imidlertid vil disse operasjonene ofte brukes langt oftere enn objektallokeringer, så de kan fortsatt være en betydelig kilde til ytelsesproblemer.

I resten av denne artikkelen vil vi diskutere noen spesifikke teknikker for å redusere behovet for å kaste inn koden din. Spesielt vil vi se på hvordan casting ofte oppstår fra måten underklasser samhandler med basisklasser, og utforske noen teknikker for å eliminere denne typen casting. Neste måned, i andre del av denne titt på casting, vil vi vurdere en annen vanlig årsak til casting, bruk av generiske samlinger.

Baseklasser og avstøpning

Det er flere vanlige bruksområder for casting i Java-programmer. For eksempel brukes avstøpning ofte til generisk håndtering av en eller annen funksjonalitet i en basisklasse som kan utvides med et antall underklasser. Følgende kode viser en noe konstruert illustrasjon av denne bruken:

 // enkel baseklasse med underklasser offentlig abstrakt klasse BaseWidget {...} offentlig klasse SubWidget utvider BaseWidget {... offentlig ugyldig doSubWidgetSomething () {...}} ... // baseklasse med underklasser, ved hjelp av det forrige settet av klasser offentlig abstrakt klasse BaseGorph {// widgeten som er knyttet til denne Gorph private BaseWidget myWidget; ... // angi widgeten som er tilknyttet denne Gorph (kun tillatt for underklasser) beskyttet ugyldighet setWidget (BaseWidget-widget) {myWidget = widget; } // få widgeten som er knyttet til denne Gorph offentlige BaseWidget getWidget () {return myWidget; } ... // returner en Gorph med noe forhold til denne Gorph // dette vil alltid være av samme type som den kalles på, men vi kan bare // returnere en forekomst av vår grunnleggende offentlige abstrakte BaseGorph otherGorph () {. ..}} // Gorph-underklasse ved hjelp av en Widget-underklasse offentlig klasse SubGorph utvider BaseGorph {// returnerer en Gorph med noe forhold til denne Gorph-offentlige BaseGorph otherGorph () {...} ... offentlig ugyldig anyMethod () {.. . // still inn widgeten vi bruker SubWidget-widget = ... setWidget (widget); ... // bruk Widget ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // bruk vår otherGorph SubGorph andre = (SubGorph) otherGorph (); ...}} 
$config[zx-auto] not found$config[zx-overlay] not found