Programování

Optimalizace výkonu JVM, Část 2: Překladače

Překladače Java se dostávají do centra pozornosti v tomto druhém článku v sérii optimalizace výkonu JVM. Eva Andreasson představuje různé druhy kompilátorů a porovnává výsledky výkonu z klientské, serverové a odstupňované kompilace. Na závěr uzavírá přehled běžných optimalizací JVM, jako je eliminace mrtvého kódu, vložení a optimalizace smyčky.

Kompilátor Java je zdrojem slavné nezávislosti platformy Java. Softwarový vývojář napíše nejlepší Java aplikaci, kterou dokáže, a poté kompilátor pracuje v zákulisí a vytváří efektivní a dobře fungující prováděcí kód pro zamýšlenou cílovou platformu. Různé druhy překladačů splňují různé aplikační potřeby, čímž poskytují konkrétní požadované výsledky výkonu. Čím více toho o kompilátorech pochopíte, pokud jde o to, jak fungují a jaké druhy jsou k dispozici, tím více budete moci optimalizovat výkon aplikací Java.

Tento druhý článek v Optimalizace výkonu JVM řada zdůrazňuje a vysvětluje rozdíly mezi různými kompilátory virtuálních strojů Java. Budu také diskutovat o některých běžných optimalizacích používaných kompilátory Just-In-Time (JIT) pro Javu. (Viz „Optimalizace výkonu JVM, část 1“, kde je uveden přehled JVM a úvod do série.)

Co je to kompilátor?

Jednoduše řečeno a překladač bere programovací jazyk jako vstup a produkuje spustitelný jazyk jako výstup. Jeden běžně známý překladač je javac, který je součástí všech standardních vývojových sad Java (JDK). javac bere Java kód jako vstup a převádí jej do bytecode - spustitelného jazyka pro JVM. Bajtkód je uložen do souborů .class, které jsou načteny do běhového prostředí Java při spuštění procesu Java.

Bytecode nelze číst standardními CPU a je třeba jej přeložit do jazyka instrukcí, kterému základní platforma pro spuštění porozumí. Komponenta v JVM, která je zodpovědná za překlad bytecode do instrukcí spustitelné platformy, je dalším kompilátorem. Některé kompilátory JVM zpracovávají několik úrovní překladu; například kompilátor může vytvořit různé úrovně mezilehlé reprezentace bytecode, než se promění ve skutečné strojové instrukce, poslední krok překladu.

Bytecode a JVM

Pokud se chcete dozvědět více o bytecode a JVM, podívejte se na "Základy Bytecode" (Bill Venners, JavaWorld).

Z platformy-agnostické perspektivy chceme co nejvíce zachovat nezávislost kódu na platformě, takže poslední úroveň překladu - od nejnižší reprezentace po skutečný strojový kód - je krok, který uzamkne provedení na architektuře procesoru konkrétní platformy . Nejvyšší úroveň oddělení je mezi statickými a dynamickými překladači. Odtamtud máme možnosti v závislosti na tom, na jaké prováděcí prostředí se zaměřujeme, jaké výsledky výkonu požadujeme a jaká omezení zdrojů musíme splnit. Stručně jsem diskutoval o statických a dynamických překladačích v části 1 této série. V následujících částech vysvětlím trochu více.

Statická vs dynamická kompilace

Příkladem statického kompilátoru je výše zmíněný javac. U statických překladačů je vstupní kód interpretován jednou a výstupní spustitelný soubor je ve formě, která bude použita při spuštění programu. Pokud neprovedete změny ve svém původním zdroji a nezkompilujete kód (pomocí kompilátoru), výstup bude mít vždy stejný výsledek; je to proto, že vstup je statický vstup a kompilátor je statický kompilátor.

Ve statické kompilaci následující kód Java

static int add7 (int x) {return x + 7; }

by mělo za následek něco podobného tomuto bytecode:

iload0 bipush 7 iadd ireturn

Dynamický překladač se dynamicky překládá z jednoho jazyka do druhého, což znamená, že k němu dochází při provádění kódu - za běhu! Dynamická kompilace a optimalizace dávají modulům runtime výhodu schopnosti přizpůsobit se změnám v zatížení aplikace. Dynamické překladače jsou velmi vhodné pro běhové prostředí Java, které se běžně spouštějí v nepředvídatelných a neustále se měnících prostředích. Většina JVM používá dynamický kompilátor, jako je kompilátor Just-In-Time (JIT). Háček je v tom, že dynamické kompilátory a optimalizace kódu někdy potřebují další datové struktury, podprocesy a prostředky CPU. Čím pokročilejší je optimalizace nebo analýza kontextu bytecode, tím více zdrojů je spotřebováno kompilací. Ve většině prostředí je režie stále velmi malá ve srovnání s významným zvýšením výkonu výstupního kódu.

Odrůdy JVM a nezávislost na platformě Java

Všechny implementace JVM mají jednu společnou věc, kterou je jejich pokus o přeložení bytecode aplikace do instrukcí stroje. Některé JVM interpretují kód aplikace při načítání a používají čítače výkonu k zaměření na „horký“ kód. Některé JVM přeskočí interpretaci a spoléhají se pouze na kompilaci. Intenzita zdrojů kompilace může být větší hit (zejména pro aplikace na straně klienta), ale také umožňuje pokročilejší optimalizace. Další informace najdete v části Zdroje.

Pokud jste v Javě začátečník, složitost JVM bude hodně na to, aby vám zabalila hlavu. Dobrou zprávou je, že to opravdu nemusíte! JVM spravuje kompilaci a optimalizaci kódu, takže se nemusíte starat o strojové pokyny a optimální způsob psaní kódu aplikace pro základní architekturu platformy.

Od Java bytecode po provedení

Jakmile máte svůj kód Java zkompilovaný do bytecode, dalším krokem je překlad instrukcí bytecode do strojového kódu. To lze provést tlumočníkem nebo překladačem.

Výklad

Nejjednodušší forma kompilace bytecode se nazývá interpretace. An tlumočník jednoduše vyhledá hardwarové pokyny pro každou instrukci bajtového kódu a odešle je k provedení CPU.

Můžete myslet výklad podobně jako použití slovníku: pro konkrétní slovo (instrukce bytecode) existuje přesný překlad (instrukce strojového kódu). Vzhledem k tomu, že tlumočník čte a okamžitě provádí jednu instrukci bajtového kódu najednou, není zde žádná příležitost k optimalizaci přes sadu instrukcí. Tlumočník musí také provádět interpretaci pokaždé, když je vyvolán bytecode, což je poměrně pomalé. Interpretace je přesný způsob provádění kódu, ale neoptimalizovaná sada výstupních instrukcí pravděpodobně nebude nejvýkonnější sekvencí pro procesor cílové platformy.

Sestavení

A překladač na druhou stranu načte celý kód, který má být spuštěn, do běhového prostředí. Při překladu bytecode má schopnost podívat se na celý nebo částečný běhový kontext a rozhodovat o tom, jak kód skutečně přeložit. Jeho rozhodnutí jsou založena na analýze grafů kódu, jako jsou různé větve provádění instrukcí a data za běhu.

Když je sekvence bytecode přeložena do sady instrukcí strojového kódu a lze provést optimalizaci pro tuto sadu instrukcí, je nahrazující sada instrukcí (např. Optimalizovaná sekvence) uložena do struktury zvané mezipaměť kódu. Při příštím spuštění bytového kódu může být dříve optimalizovaný kód okamžitě umístěn v mezipaměti kódu a použit k provedení. V některých případech může čítač výkonu nastartovat a přepsat předchozí optimalizaci, v takovém případě kompilátor spustí novou optimalizační sekvenci. Výhodou kódové mezipaměti je, že výsledná sada instrukcí může být spuštěna najednou - není třeba interpretačních vyhledávání ani kompilace! To zrychluje dobu provádění, zejména u aplikací Java, kde jsou stejné metody nazývány vícekrát.

Optimalizace

Spolu s dynamickou kompilací přichází příležitost vložit čítače výkonu. Kompilátor může například vložit a počítadlo výkonu počítat pokaždé, když byl vyvolán blok bytecode (např. odpovídající konkrétní metodě). Kompilátoři používají data o tom, jak „horký“ je daný bajtkód, aby určili, kde v optimalizaci kódu bude mít nejlepší dopad na běžící aplikaci. Data profilování za běhu umožňují kompilátoru provádět bohatou sadu rozhodnutí o optimalizaci kódu za běhu, což dále zlepšuje výkon provádění kódu. Jakmile budou k dispozici propracovanější data pro profilování kódu, lze je použít k provedení dalších a lepších rozhodnutí o optimalizaci, například: jak zlepšit instrukce sekvence v kompilovaném jazyce, zda nahradit sadu pokynů efektivnějšími sadami, nebo zda odstranit nadbytečné operace.

Příklad

Zvažte kód Java:

static int add7 (int x) {return x + 7; }

To by mohlo být staticky kompilováno javac na bytecode:

iload0 bipush 7 iadd ireturn

Když je metoda volána, blok bytecode bude dynamicky kompilován podle instrukcí stroje. Když čítač výkonu (pokud je k dispozici pro blok kódu) dosáhne prahové hodnoty, může se také optimalizovat. Konečný výsledek může vypadat jako následující sada strojových instrukcí pro danou prováděcí platformu:

lea rax, [rdx + 7] ret

Různé překladače pro různé aplikace

Různé aplikace Java mají různé potřeby. Dlouhodobě fungující podnikové aplikace na straně serveru by mohly umožnit více optimalizací, zatímco menší aplikace na straně klienta mohou vyžadovat rychlé spuštění s minimální spotřebou prostředků. Zvažme tři různá nastavení kompilátoru a jejich příslušné výhody a nevýhody.

Překladače na straně klienta

Dobře známý optimalizační kompilátor je C1, kompilátor, který je povolen prostřednictvím - klient Možnost spuštění JVM. Jak naznačuje jeho název při spuštění, C1 je kompilátor na straně klienta. Je určen pro aplikace na straně klienta, které mají k dispozici méně prostředků a jsou v mnoha případech citlivé na dobu spuštění aplikace. C1 pomocí čítačů výkonu pro profilování kódu umožňuje jednoduché, relativně nenápadné optimalizace.

Překladače na straně serveru

U dlouhotrvajících aplikací, jako jsou podnikové aplikace Java na straně serveru, nemusí kompilátor na straně klienta stačit. Místo toho lze použít překladač na straně serveru, jako je C2. C2 je obvykle povoleno přidáním možnosti spuštění JVM -server do spouštěcího příkazového řádku. Protože se očekává, že většina programů na straně serveru poběží dlouhou dobu, povolení C2 znamená, že budete moci shromáždit více profilovacích dat, než byste měli s krátkou spuštěnou lehkou klientskou aplikací. Budete tedy moci použít pokročilejší optimalizační techniky a algoritmy.

Tip: Zahřejte kompilátor na straně serveru

U nasazení na straně serveru může nějakou dobu trvat, než kompilátor optimalizuje počáteční „horké“ části kódu, takže nasazení na straně serveru často vyžadují fázi „zahřívání“. Před provedením jakéhokoli měření výkonu při nasazení na straně serveru se ujistěte, že vaše aplikace dosáhla ustáleného stavu! Necháte-li kompilátoru dostatek času na správnou kompilaci, bude to ve váš prospěch! (Další informace o zahřátí kompilátoru a mechanice profilování najdete v článku JavaWorld „Sledujte spuštění kompilátoru HotSpot“.)

Kompilátor serveru odpovídá za více profilujících dat než kompilátor na straně klienta a umožňuje složitější analýzu větví, což znamená, že zváží, která optimalizační cesta by byla výhodnější. Mající více profilovaných dat k dispozici přináší lepší výsledky aplikace. Samozřejmě provedení rozsáhlejšího profilování a analýzy vyžaduje vynaložení více prostředků na kompilátor. JVM s povolenou C2 bude používat více vláken a více cyklů CPU, bude vyžadovat větší mezipaměť kódu atd.

Víceúrovňová kompilace

Odstupňovaná kompilace kombinuje kompilaci na straně klienta a na straně serveru. Azul nejprve zpřístupnil odstupňovanou kompilaci ve svém Zing JVM. V poslední době (od Java SE 7) byl přijat Oracle Java Hotspot JVM. Víceúrovňová kompilace využívá výhod kompilátoru klienta i serveru ve vašem JVM. Kompilátor klienta je nejaktivnější během spuštění aplikace a zpracovává optimalizace spuštěné nižšími prahovými hodnotami čítače výkonu. Kompilátor na straně klienta také vloží čítače výkonu a připraví sady instrukcí pro pokročilejší optimalizace, které budou později adresovány kompilátorem na straně serveru. Víceúrovňová kompilace je velmi efektivním způsobem profilování, protože kompilátor je schopen shromažďovat data během činnosti kompilátoru s nízkým dopadem, který lze později použít pro pokročilejší optimalizace. Tento přístup také přináší více informací, než kolik získáte ze samotného použití čítačů profilu interpretovaného kódu.

Schéma grafu na obrázku 1 zobrazuje rozdíly ve výkonu mezi čistou interpretací, na straně klienta, na straně serveru a odstupňovanou kompilací. Osa X zobrazuje čas provedení (časová jednotka) a výkon osy Y (jednotka operace / čas).

Obrázek 1. Výkonové rozdíly mezi překladači (kliknutím obrázek zvětšíte)

Ve srovnání s čistě interpretovaným kódem vede použití kompilátoru na straně klienta k přibližně 5 až 10krát lepšímu výkonu provádění (v operačních systémech / s), čímž se zlepší výkon aplikace. Varianta zisku samozřejmě závisí na tom, jak efektivní je kompilátor, jaké optimalizace jsou povoleny nebo implementovány, a (v menší míře) na tom, jak dobře je aplikace navržena s ohledem na cílovou platformu provádění. Toto je opravdu něco, čeho by se vývojář Java nikdy neměl obávat.

Ve srovnání s kompilátorem na straně klienta kompilátor na straně serveru obvykle zvyšuje výkon kódu o měřitelných 30 až 50 procent. Ve většině případů toto zlepšení výkonu vyváží náklady na další zdroje.