Programování

Programování výkonu v Javě, část 2: Náklady na casting

U tohoto druhého článku v naší sérii o výkonu Java se pozornost přesouvá na casting - co to je, co to stojí a jak se tomu můžeme (někdy) vyhnout. Tento měsíc začneme rychlou revizí základů tříd, objektů a odkazů, poté pokračujeme pohledem na některé tvrdé údaje o výkonu (v postranním panelu, abychom neurazili hloupý!) A pokyny k typy operací, které s největší pravděpodobností způsobí trávení vašemu Java Virtual Machine (JVM). Nakonec skončíme podrobným pohledem na to, jak se můžeme vyhnout běžným efektům strukturování tříd, které mohou způsobit casting.

Programování výkonu v Javě: Přečtěte si celou sérii!

  • Část 1. Zjistěte, jak snížit režii programu a zlepšit výkon řízením vytváření objektů a uvolňování paměti
  • Část 2. Snižte chyby režie a provádění pomocí kódu bezpečného pro typ
  • Část 3. Podívejte se, jak alternativní kolekce zvyšují výkon, a zjistěte, jak z každého typu vytěžit maximum

Typy objektů a odkazů v Javě

Minulý měsíc jsme diskutovali o základním rozlišení mezi primitivními typy a objekty v Javě. Počet primitivních typů i vztahy mezi nimi (zejména převody mezi typy) jsou pevně stanoveny v definici jazyka. Objekty jsou na druhé straně neomezeného typu a mohou souviset s libovolným počtem dalších typů.

Každá definice třídy v programu Java definuje nový typ objektu. To zahrnuje všechny třídy z knihoven Java, takže jakýkoli daný program může používat stovky nebo dokonce tisíce různých typů objektů. Některé z těchto typů jsou v definici jazyka Java specifikovány jako určité speciální použití nebo manipulace (například použití java.lang.StringBuffer pro řetězec java.lang zřetězení). Kromě těchto několika výjimek se však se všemi typy zachází v zásadě stejně jako u kompilátoru Java a JVM použitého ke spuštění programu.

Pokud definice třídy neurčuje (pomocí rozšiřuje klauzule v záhlaví definice třídy) jinou třídu jako nadřazenou nebo nadtřídu implicitně rozšiřuje java.lang.Object třída. To znamená, že každá třída se nakonec rozšiřuje java.lang.Object, buď přímo, nebo prostřednictvím sekvence jedné nebo více úrovní nadřazených tříd.

Samotné objekty jsou vždy instancemi tříd a objektů typ je třída, jejíž instance je. V Javě však nikdy nebudeme jednat přímo s objekty; pracujeme s odkazy na objekty. Například řádek:

 java.awt.Component myComponent; 

nevytváří java.awt.Component objekt; vytvoří referenční proměnnou typu java.lang.Component. Přestože odkazy mají typy stejně jako objekty, neexistuje přesná shoda mezi typy odkazů a typů - může být referenční hodnota nula, objekt stejného typu jako reference, nebo objekt jakékoli podtřídy (tj. třídy pocházející z) typu odkazu. V tomto konkrétním případě java.awt.Component je abstraktní třída, takže víme, že nikdy nemůže existovat objekt stejného typu jako naše reference, ale určitě mohou existovat objekty podtříd tohoto referenčního typu.

Polymorfismus a lití

Typ odkazu určuje, jak odkazovaný objekt - to znamená objekt, který je hodnotou odkazu - lze použít. Například ve výše uvedeném příkladu použijte kód myComponent mohl vyvolat kteroukoli z metod definovaných třídou java.awt.Component, nebo některá z jeho nadtříd, na odkazovaný objekt.

Metoda skutečně spuštěná voláním však není určena typem samotného odkazu, ale spíše typem odkazovaného objektu. Toto je základní princip polymorfismus - podtřídy mohou přepsat metody definované v nadřazené třídě za účelem implementace odlišného chování. V případě naší ukázkové proměnné, pokud byl odkazovaný objekt ve skutečnosti instancí Tlačítko java.awt, změna stavu vyplývající z a setLabel („Push Me“) volání by se lišilo od výsledku, pokud by odkazovaný objekt byl instancí java.awt.Label.

Kromě definic tříd používají programy Java také definice rozhraní. Rozdíl mezi rozhraním a třídou spočívá v tom, že rozhraní určuje pouze sadu chování (a v některých případech konstanty), zatímco třída definuje implementaci. Protože rozhraní nedefinují implementace, objekty nikdy nemohou být instancemi rozhraní. Mohou to však být instance tříd, které implementují rozhraní. Reference umět být typu rozhraní, v takovém případě mohou být odkazovanými objekty instance jakékoli třídy, která implementuje rozhraní (buď přímo, nebo prostřednictvím nějaké třídy předků).

Casting se používá k převodu mezi typy - zejména mezi referenčními typy, pro typ operace odlévání, o kterou se zde zajímáme. Upcast operace (také zvaný rozšiřování konverzí ve specifikaci jazyka Java) převeďte odkaz na podtřídu na odkaz na předchůdce třídy. Tato operace castingu je obvykle automatická, protože je vždy bezpečná a může být implementována přímo kompilátorem.

Skandální operace (také zvaný zúžení konverzí ve specifikaci jazyka Java) převeďte odkaz na třídu předků na odkaz na podtřídu. Tato operace přetypování vytváří režii provádění, protože Java vyžaduje, aby byla kontrola za běhu zkontrolována, aby se ujistil, že je platná. Pokud odkazovaný objekt není instancí cílového typu pro cast nebo podtřídy tohoto typu, pokus o cast není povolen a musí hodit java.lang.ClassCastException.

The instanceof operátor v Javě umožňuje určit, zda je povolena konkrétní operace odlévání, aniž by se o operaci skutečně pokusila. Protože náklady na výkon kontroly jsou mnohem nižší než náklady na výjimku generovanou nepovoleným pokusem o seslání, je obecně moudré použít instanceof kdykoli si nejste jisti, že typ odkazu je takový, jaký chcete. Než tak učiníte, měli byste se ujistit, že máte rozumný způsob, jak se vypořádat s odkazem na nežádoucí typ - jinak můžete také jen nechat výjimku vyvolat a zpracovat ji na vyšší úrovni v kódu.

Věnujte opatrnost větrům

Casting umožňuje použití obecného programování v Javě, kde je psán kód pro práci se všemi objekty tříd pocházejících z nějaké základní třídy (často java.lang.Object, pro třídy služeb). Použití odlévání však způsobuje jedinečnou sadu problémů. V další části se podíváme na dopad na výkon, ale pojďme nejprve zvážit dopad na samotný kód. Tady je ukázka používající generikum java.lang. vektor třída sbírky:

 soukromá Vektorová některá čísla; ... public void doSomething () {... int n = ... Celé číslo = (Celé číslo) someNumbers.elementAt (n); ...} 

Tento kód představuje potenciální problémy z hlediska jasnosti a udržovatelnosti. Pokud by někdo v jiném okamžiku než původní vývojář kód upravil, mohl by si rozumně myslet, že by mohl přidat a java.lang.Double do některá čísla sbírek, protože se jedná o podtřídu java.lang.Number. Všechno by se zkompilovalo, kdyby to zkusil, ale v určitém neurčitém okamžiku popravy by pravděpodobně dostal java.lang.ClassCastException hozen při pokusu o seslání na a java.lang.Integer byl popraven za jeho přidanou hodnotu.

Problém je v tom, že použití castingu obchází bezpečnostní kontroly zabudované do kompilátoru Java; programátor skončí s hledáním chyb během provádění, protože je kompilátor nezachytí. To není samo o sobě katastrofální, ale tento typ chyby použití se často při testování kódu skrývá docela chytře, jen aby se odhalil, když je program uveden do výroby.

Není divu, že podpora techniky, která by kompilátoru umožnila detekovat tento typ chyby použití, je jedním z nejžádanějších vylepšení Java. V komunitním procesu Java právě probíhá projekt, který zkoumá přidání právě této podpory: číslo projektu JSR-000014, Přidat obecné typy do programovacího jazyka Java (další podrobnosti najdete v části Zdroje). V pokračování tohoto článku příští měsíc se na tento projekt podíváme podrobněji a probereme, jak je pravděpodobné, že pomůže, a kde je pravděpodobné, že nás nechá chtít víc.

Problém s výkonem

Již dlouho se uznává, že casting může být na újmu výkonu v Javě, a že můžete zlepšit výkon minimalizací castingu v často používaném kódu. Volání metod, zejména volání přes rozhraní, jsou také často zmiňována jako potenciální úzká místa výkonu. Současná generace JVM prošla od svých předchůdců dlouhou cestu a stojí za to zkontrolovat, jak dobře tyto principy dnes drží.

Pro tento článek jsem vyvinul řadu testů, abych zjistil, jak důležité jsou tyto faktory pro výkon se současnými JVM. Výsledky testu jsou shrnuty do dvou tabulek v postranním panelu, Tabulka 1 zobrazující režii volání metody a Tabulka 2 přetypování režie. Celý zdrojový kód pro testovací program je k dispozici také online (další podrobnosti najdete v části Zdroje níže).

Abychom shrnuli tyto závěry pro čtenáře, kteří se nechtějí procházet podrobnostmi v tabulkách, jsou určité typy volání metod a přetypování stále poměrně drahé, v některých případech to trvá téměř stejně dlouho jako jednoduché přidělení objektu. Pokud je to možné, těmto typům operací by se mělo v kódu, který je třeba optimalizovat pro výkon, vyhnout.

Zejména volání přepsaných metod (metody, které jsou přepsány v jakékoli načtené třídě, nejen ve skutečné třídě objektu) a volání prostřednictvím rozhraní jsou podstatně nákladnější než jednoduchá volání metod. HotSpot Server JVM 2.0 beta použitý v testu dokonce převede mnoho jednoduchých volání metod na vložený kód, čímž se zabrání režii pro takové operace. HotSpot však vykazuje nejhorší výkon mezi testovanými JVM pro přepsané metody a volání přes rozhraní.

Pro casting (samozřejmě downcasting) testované JVM obecně udržují výkonnost na rozumné úrovni. HotSpot s tím ve většině testů benchmarku dělá výjimečnou práci a stejně jako u volání metod je v mnoha jednoduchých případech schopen téměř úplně eliminovat režii castingu. U složitějších situací, jako jsou casts následované voláním přepsaných metod, vykazují všechny testované JVM znatelné snížení výkonu.

Testovaná verze HotSpot také ukázala extrémně špatný výkon, když byl objekt postupně přetypován na různé referenční typy (místo toho, aby byl vždy přetypován na stejný cílový typ). Tato situace pravidelně nastává v knihovnách, jako je Swing, které používají hlubokou hierarchii tříd.

Ve většině případů je režie obou volání metody a přetypování malá ve srovnání s časy alokace objektů, na které se podíváme v článku z minulého měsíce. Tyto operace se však často používají mnohem častěji než přidělení objektů, takže mohou být stále významným zdrojem problémů s výkonem.

Ve zbývající části tohoto článku probereme několik konkrétních technik, jak snížit potřebu castingu ve vašem kódu. Konkrétně se podíváme na to, jak casting často vzniká ze způsobu, jakým podtřídy interagují se základními třídami, a prozkoumáme některé techniky pro eliminaci tohoto typu castingu. Příští měsíc v druhé části tohoto pohledu na casting budeme uvažovat o další běžné příčině castingu, použití obecných sbírek.

Základní třídy a casting

Castování v programech Java existuje několik běžných způsobů. Například casting se často používá pro obecnou manipulaci s některými funkcemi v základní třídě, kterou lze rozšířit o několik podtříd. Následující kód ukazuje poněkud nepřirozený obrázek tohoto použití:

 // jednoduchá základní třída s podtřídami veřejná abstraktní třída BaseWidget {...} veřejná třída SubWidget rozšiřuje BaseWidget {... public void doSubWidgetSomething () {...}} ... // základní třída s podtřídami, používá předchozí sadu tříd public abstract class BaseGorph {// Widget přidružený k tomuto soukromému Gorph BaseWidget myWidget; ... // nastavit Widget přidružený k této Gorph (povoleno pouze pro podtřídy) chráněný void setWidget (BaseWidget widget) {myWidget = widget; } // získat Widget přidružený k tomuto veřejnému Gorph BaseWidget getWidget () {return myWidget; } ... // vrátit Gorpha s určitým vztahem k tomuto Gorphovi // toto bude vždy stejný typ, na jaký je vyvolán, ale můžeme // vrátit pouze instanci našeho abstraktu veřejné třídy BaseGorph otherGorph () {. ..}} // Gorph subclass using a Widget subclass public class SubGorph extends BaseGorph {// return a Gorph with some relationship to this Gorph public BaseGorph otherGorph () {...} ... public void anyMethod () {.. . // nastavíme Widget, který používáme SubWidget widget = ... setWidget (widget); ... // použijte náš Widget ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // použijte náš otherGorph SubGorph other = (SubGorph) otherGorph (); ...}} 
$config[zx-auto] not found$config[zx-overlay] not found