Programmering

Programmering af Java-ydeevne, del 2: Omkostningerne ved casting

For denne anden artikel i vores serie om Java-ydeevne skifter fokus til casting - hvad det er, hvad det koster, og hvordan vi (nogle gange) kan undgå det. Denne måned starter vi med en hurtig gennemgang af det grundlæggende i klasser, objekter og referencer, hvorefter vi følger op med et kig på nogle hardcore-præstationstal (i et sidebjælke for ikke at fornærme det uhyggelige!) Og retningslinjer for typer operationer, der mest sandsynligt giver din Java Virtual Machine (JVM) fordøjelsesbesvær. Endelig slutter vi med et dybtgående kig på, hvordan vi kan undgå almindelige klassestrukturerende effekter, der kan forårsage støbning.

Java ydeevne programmering: Læs hele serien!

  • Del 1. Lær, hvordan du reducerer programomkostninger og forbedrer ydeevnen ved at kontrollere oprettelse af objekter og affaldssamling
  • Del 2. Reducer overhead- og udførelsesfejl gennem typesikker kode
  • Del 3. Se, hvordan kollektionalternativer måles i ydeevne, og find ud af, hvordan du får mest muligt ud af hver type

Objekt- og referencetyper i Java

Sidste måned diskuterede vi den grundlæggende skelnen mellem primitive typer og objekter i Java. Både antallet af primitive typer og forholdet mellem dem (især konverteringer mellem typer) er fastlagt af sprogdefinitionen. Objekter er derimod af ubegrænsede typer og kan være relateret til et hvilket som helst antal andre typer.

Hver klassedefinition i et Java-program definerer en ny type objekt. Dette inkluderer alle klasser fra Java-bibliotekerne, så ethvert givet program bruger muligvis hundreder eller endda tusinder af forskellige typer objekter. Et par af disse typer er specificeret af Java-sprogdefinitionen som at have visse specielle anvendelser eller håndtering (såsom brugen af java.lang.StringBuffer til java.lang.Streng sammenkædningsoperationer). Bortset fra disse få undtagelser behandles alle typer dog stort set ens af Java-kompilatoren, og JVM'en, der blev brugt til at udføre programmet.

Hvis en klassedefinition ikke specificerer (ved hjælp af strækker sig klausul i klassedefinitionens overskrift) en anden klasse som forælder eller superklasse, udvider den implicit java.lang.Objekt klasse. Dette betyder, at hver klasse i sidste ende strækker sig java.lang.Objekt, enten direkte eller via en sekvens af et eller flere niveauer af forældreklasser.

Objekter i sig selv er altid forekomster af klasser og et objekt type er den klasse, som det er et eksempel på. I Java beskæftiger vi os aldrig direkte med objekter; vi arbejder med referencer til objekter. For eksempel linjen:

 java.awt.Komponent minKomponent; 

skaber ikke en java.awt.Komponent objekt; det opretter en referencevariabel af typen java.lang.Komponent. Selvom referencer har typer, ligesom objekter har, er der ikke en præcis matchning mellem reference og objekttyper - en referenceværdi kan være nul, et objekt af samme type som referencen eller et objekt af en hvilken som helst underklasse (dvs. klasse nedstammer fra) referencens type. I dette særlige tilfælde java.awt.Komponent er en abstrakt klasse, så vi ved, at der aldrig kan være et objekt af samme type som vores reference, men der kan bestemt være genstande i underklasser af denne referencetype.

Polymorfisme og støbning

Typen af ​​en reference bestemmer, hvordan refereret objekt - det vil sige det objekt, der er referencens værdi - kan bruges. For eksempel, i eksemplet ovenfor, kode ved hjælp af minKomponent kunne påberåbe sig en af ​​de metoder, der er defineret af klassen java.awt.Komponenteller nogen af ​​dens superklasser på det refererede objekt.

Metoden, der faktisk udføres af et opkald, bestemmes dog ikke af selve referencetypen, men snarere af typen af ​​det refererede objekt. Dette er det grundlæggende princip for polymorfisme - underklasser kan tilsidesætte metoder, der er defineret i overordnet klasse for at implementere forskellige adfærd. I tilfælde af vores eksempelvariabel, hvis det refererede objekt faktisk var en forekomst af java.awt.Knapændringen i tilstand som følge af a setLabel ("Push Me") opkald ville være forskelligt fra det resulterende, hvis det refererede objekt var en forekomst af java.awt.Label.

Udover klassedefinitioner bruger Java-programmer også interface-definitioner. Forskellen mellem en grænseflade og en klasse er, at en grænseflade kun specificerer et sæt adfærd (og i nogle tilfælde konstanter), mens en klasse definerer en implementering. Da grænseflader ikke definerer implementeringer, kan objekter aldrig være forekomster af en grænseflade. De kan dog være forekomster af klasser, der implementerer en grænseflade. Referencer kan være af grænsefladetyper, i hvilket tilfælde de refererede objekter kan være forekomster af en hvilken som helst klasse, der implementerer grænsefladen (enten direkte eller gennem en forfædreklasse).

Støbning bruges til at konvertere mellem typer - især mellem referencetyper til den type støbning, som vi er interesseret i her. Upcast-operationer (også kaldet udvidelse af konverteringer i Java Language Specification) konverterer en underklassereference til en forfaderklassereference. Denne casting-operation er normalt automatisk, da den altid er sikker og kan implementeres direkte af compileren.

Nedstødte operationer (også kaldet indsnævring af konverteringer i Java Language Specification) konverterer en forfædreklassereference til en underklassereference. Denne casting-handling skaber udførelsesomkostninger, da Java kræver, at castet kontrolleres ved kørsel for at sikre, at det er gyldigt. Hvis det refererede objekt ikke er en forekomst af hverken måltypen for rollebesætningen eller en underklasse af den type, er det forsøgte rollebesætning ikke tilladt og skal kaste et java.lang.ClassCastException.

Det forekomst af operatør i Java giver dig mulighed for at afgøre, om en bestemt casting-handling er tilladt eller ej uden faktisk at forsøge operationen. Da præstationsomkostningerne ved en check er meget mindre end den undtagelse, der genereres af et uautoriseret rollebesøg, er det generelt klogt at bruge en forekomst af test når som helst, du er ikke sikker på, at typen af ​​en reference er, hvad du gerne vil have den. Inden du gør det, skal du dog sørge for, at du har en rimelig måde at håndtere en reference af en uønsket type - ellers kan du lige så godt lade undtagelsen smide og håndtere den på et højere niveau i din kode.

Kaster forsigtighed mod vinden

Casting tillader brug af generisk programmering i Java, hvor kode skrives for at arbejde med alle objekter i klasser, der stammer fra en eller anden basisklasse (ofte java.lang.Objekt, til hjælpeklasser). Imidlertid forårsager brugen af ​​støbning et unikt sæt problemer. I det næste afsnit ser vi på indvirkningen på ydeevnen, men lad os først overveje effekten på selve koden. Her er en prøve ved hjælp af det generiske java.lang.Vector samling klasse:

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

Denne kode præsenterer potentielle problemer med hensyn til klarhed og vedligeholdelse. Hvis en anden end den oprindelige udvikler skulle ændre koden på et eller andet tidspunkt, kunne han med rimelighed tro, at han kunne tilføje en java.lang. dobbelt til nogle numre samlinger, da dette er en underklasse af java.lang.Nummer. Alt ville kompilere fint, hvis han prøvede dette, men på et ubestemt tidspunkt i udførelsen ville han sandsynligvis få en java.lang.ClassCastException kastet, da det forsøgte kastede til en java.lang. heltal blev henrettet for hans merværdi.

Problemet her er, at brugen af ​​casting omgår sikkerhedskontrol, der er indbygget i Java-kompilatoren; programmøren ender med at jage efter fejl under udførelse, da compileren ikke fanger dem. Dette er ikke katastrofalt i sig selv, men denne type brugsfejl skjuler ofte ganske klogt, mens du tester din kode, kun for at afsløre sig selv, når programmet sættes i produktion.

Ikke overraskende er understøttelse af en teknik, der gør det muligt for compileren at opdage denne type brugsfejl, en af ​​de mere krævede forbedringer af Java. Der er et projekt i gang i Java Community-processen, der undersøger tilføjelse af netop denne support: projektnummer JSR-000014, Tilføj generiske typer til Java-programmeringssprog (se afsnittet Ressourcer nedenfor for flere detaljer.) I fortsættelsen af ​​denne artikel, kommende næste måned vil vi se nærmere på dette projekt og diskutere både, hvordan det sandsynligvis vil hjælpe, og hvor det sandsynligvis vil lade os ønske mere.

Performance-problemet

Det har længe været anerkendt, at casting kan være skadeligt for ydeevne i Java, og at du kan forbedre ydeevnen ved at minimere casting i meget brugt kode. Metodeopkald, især opkald via grænseflader, nævnes ofte også som potentielle præstationsflaskehalse. Den nuværende generation af JVM'er er dog langt fra deres forgængere, og det er værd at tjekke for at se, hvor godt disse principper holder op i dag.

Til denne artikel udviklede jeg en række tests for at se, hvor vigtige disse faktorer er for ydeevnen med nuværende JVM'er. Testresultaterne er opsummeret i to tabeller i sidebjælken, tabel 1 viser metodeopkaldsoverhead og tabel 2 casting overhead. Den fulde kildekode til testprogrammet er også tilgængelig online (se afsnittet Ressourcer nedenfor for flere detaljer).

For at opsummere disse konklusioner for læsere, der ikke ønsker at vade gennem detaljerne i tabellerne, er visse typer metodeopkald og -afgivelser stadig ret dyre, i nogle tilfælde tager det næsten lige så lang tid som en simpel objektallokering. Hvor det er muligt, bør disse typer operationer undgås i kode, der skal optimeres til ydeevne.

Især opkald til tilsidesatte metoder (metoder, der tilsidesættes i enhver indlæst klasse, ikke kun objektets faktiske klasse) og opkald via grænseflader er betydeligt dyrere end enkle metodeopkald. HotSpot Server JVM 2.0 beta, der bruges i testen, konverterer endda mange enkle metodeopkald til inline-kode og undgår overhead til sådanne operationer. Imidlertid viser HotSpot den dårligste ydeevne blandt de testede JVM'er til tilsidesatte metoder og opkald gennem grænseflader.

Til casting (downcasting, selvfølgelig) holder de testede JVM'er generelt ydeevnen ramt til et rimeligt niveau. HotSpot gør en enestående opgave med dette i de fleste af benchmarktestene, og er som i metoden opkald i mange enkle tilfælde næsten fuldstændigt i stand til at eliminere omkostningerne ved casting. For mere komplicerede situationer, såsom rollebesætninger efterfulgt af opkald til tilsidesatte metoder, viser alle de testede JVM'er mærkbar ydelsesforringelse.

Den testede version af HotSpot viste også ekstremt dårlig ydeevne, når et objekt blev kastet til forskellige referencetyper i rækkefølge (i stedet for altid at blive kastet til den samme måltype). Denne situation opstår regelmæssigt i biblioteker som f.eks. Gynger, der bruger et dybt hierarki af klasser.

I de fleste tilfælde er omkostningerne ved begge metodeopkald og casting lille i sammenligning med de objekttildelingstider, der blev set på i sidste måneds artikel. Disse operationer vil dog ofte blive brugt langt oftere end objektallokeringer, så de kan stadig være en væsentlig kilde til ydeevneproblemer.

I den resterende del af denne artikel vil vi diskutere nogle specifikke teknikker til at reducere behovet for casting i din kode. Specifikt vil vi se på, hvordan casting ofte opstår fra den måde, subklasser interagerer med basisklasser på, og udforske nogle teknikker til eliminering af denne type casting. Næste måned, i anden del af dette kig på casting, overvejer vi en anden almindelig årsag til casting, brugen af ​​generiske samlinger.

Baseklasser og støbning

Der er flere almindelige anvendelser af casting i Java-programmer. For eksempel bruges casting ofte til generisk håndtering af nogle funktioner i en basisklasse, der kan udvides med et antal underklasser. Følgende kode viser en noget konstrueret illustration af denne brug:

 // simpel baseklasse med underklasser offentlig abstrakt klasse BaseWidget {...} offentlig klasse SubWidget udvider BaseWidget {... offentlig ugyldighed doSubWidgetSomething () {...}} ... // baseklasse med underklasser ved hjælp af det forudgående sæt af klasser offentlig abstrakt klasse BaseGorph {// den widget, der er knyttet til denne Gorph private BaseWidget myWidget; ... // indstil den widget, der er knyttet til denne Gorph (kun tilladt for underklasser) beskyttet ugyldig setWidget (BaseWidget-widget) {myWidget = widget; } // få widgeten tilknyttet denne Gorph offentlige BaseWidget getWidget () {return myWidget; } ... // returner en Gorph med en vis relation til denne Gorph // dette vil altid være af samme type, som den kaldes på, men vi kan kun // returnere en forekomst af vores basisklasse offentlig abstrakt BaseGorph otherGorph () {. ..}} // Gorph-underklasse ved hjælp af en offentlig widget-underklasse SubGorph udvider BaseGorph {// returnerer en Gorph med en vis relation til denne Gorph-offentlige BaseGorph otherGorph () {...} ... offentlig ugyldig anyMethod () {.. . // indstil den widget, vi bruger SubWidget-widget = ... setWidget (widget); ... // brug vores Widget ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // brug vores otherGorph SubGorph anden = (SubGorph) otherGorph (); ...}}