Programovanie

Optimalizácia výkonu JVM, časť 2: Kompilátory

Kompilátory Java sa dostávajú do centra pozornosti v tomto druhom článku v sérii optimalizácie výkonu JVM. Eva Andreasson predstavuje rôzne plemená kompilátora a porovnáva výsledky výkonu z klientskej, serverovej a viacúrovňovej kompilácie. Na záver uvádza prehľad bežných optimalizácií JVM, ako je eliminácia mŕtveho kódu, inlining a optimalizácia slučiek.

Kompilátor Java je zdrojom slávnej nezávislosti platformy Java. Softvérový vývojár napíše najlepšiu Java aplikáciu, akú dokáže, a potom kompilátor pracuje v zákulisí na produkcii efektívneho a dobre fungujúceho exekučného kódu pre cieľovú platformu. Rôzne druhy prekladačov vyhovujú rôznym potrebám aplikácií, a tak poskytujú konkrétne požadované výkonnostné výsledky. Čím viac toho o kompilátoroch pochopíte, pokiaľ ide o to, ako fungujú a aké druhy sú k dispozícii, tým viac budete schopní optimalizovať výkon aplikácií Java.

Tento druhý článok v Optimalizácia výkonu JVM séria zdôrazňuje a vysvetľuje rozdiely medzi rôznymi kompilátormi virtuálnych strojov Java. Budem diskutovať aj o niekoľkých bežných optimalizáciách používaných kompilátormi Just-In-Time (JIT) pre Javu. (Prehľad a úvod do série nájdete v časti „Optimalizácia výkonu JVM, 1. časť“.)

Čo je to kompilátor?

Jednoducho povedané a zostavovateľ berie programovací jazyk ako vstup a produkuje spustiteľný jazyk ako výstup. Jeden všeobecne známy prekladač je javac, ktorý je súčasťou všetkých štandardných vývojových súprav Java (JDK). javac vezme Java kód ako vstup a preloží ho do bytecode - spustiteľného jazyka pre JVM. Bajtový kód je uložený do súborov .class, ktoré sa načítajú do modulu runtime Java pri spustení procesu Java.

Bytecode nie je možné prečítať štandardnými procesormi a je potrebné ho preložiť do jazyka pokynov, ktorému rozumie podkladová platforma vykonania. Komponent v JVM, ktorý je zodpovedný za preklad bytecode do pokynov spustiteľnej platformy, je ešte ďalším kompilátorom. Niektoré kompilátory JVM zvládajú niekoľko úrovní prekladu; napríklad kompilátor môže vytvoriť rôzne úrovne sprostredkovanej reprezentácie bytecode skôr, ako sa zmení na skutočné strojové pokyny, posledný krok prekladu.

Bytecode a JVM

Ak sa chcete dozvedieť viac o bytecode a JVM, prečítajte si „Základné informácie o bytecode“ (Bill Venners, JavaWorld).

Z hľadiska platformy-agnostiky chceme čo najviac zachovať nezávislosť kódu od platformy, aby posledná úroveň prekladu - od najnižšej reprezentácie po skutočný strojový kód - bola krokom, ktorý uzamkne vykonanie architektúry procesora konkrétnej platformy . Najvyššia úroveň oddelenia je medzi statickými a dynamickými prekladačmi. Odtiaľ máme možnosti v závislosti od toho, na aké prostredie vykonávania sa zameriavame, aké výsledky výkonu požadujeme a aké obmedzenia zdrojov musíme spĺňať. Stručne som hovoril o statických a dynamických prekladačoch v 1. časti tejto série. V nasledujúcich častiach vysvetlím trochu viac.

Statická vs dynamická kompilácia

Príkladom statického kompilátora je vyššie uvedené javac. Pri statických prekladačoch je vstupný kód interpretovaný raz a výstupný spustiteľný súbor je vo forme, ktorá sa použije pri spustení programu. Pokiaľ nezmeníte pôvodný zdroj a nezkompilujete kód (pomocou kompilátora), bude mať výstup vždy rovnaký výsledok; je to preto, že vstup je statický vstup a prekladač je statický prekladač.

V statickej kompilácii nasledujúci kód Java

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

by malo za následok niečo podobné ako tento bajtkód:

iload0 bipush 7 iadd ireturn

Dynamický prekladač prekladá z jedného jazyka do druhého dynamicky, čo znamená, že k nemu dochádza pri vykonávaní kódu - počas behu programu! Dynamická kompilácia a optimalizácia poskytujú runtime výhodu v schopnosti prispôsobiť sa zmenám v zaťažení aplikácie. Dynamické kompilátory sú veľmi vhodné pre runtime Java, ktoré sa bežne spúšťajú v nepredvídateľných a neustále sa meniacich prostrediach. Väčšina JVM používa dynamický kompilátor, napríklad kompilátor Just-In-Time (JIT). Háčik je v tom, že dynamické prekladače a optimalizácia kódu niekedy potrebujú ďalšie dátové štruktúry, vlákna a prostriedky CPU. Čím pokročilejšia je optimalizácia alebo analýza kontextu bytecode, tým viac zdrojov sa spotrebuje kompiláciou. Vo väčšine prostredí je réžia stále veľmi malá v porovnaní so značným zvýšením výkonu výstupného kódu.

Odrody JVM a nezávislosť platformy Java

Všetky implementácie JVM majú jednu spoločnú vlastnosť, ktorou je ich pokus o preloženie bytového kódu aplikácie do strojových pokynov. Niektoré JVM interpretujú kód aplikácie pri načítaní a pomocou počítadiel výkonu sa zameriavajú na „horúci“ kód. Niektoré JVM preskočia interpretáciu a spoliehajú sa iba na kompiláciu. Intenzita zdrojov kompilácie môže byť väčším hitom (najmä pre aplikácie na strane klienta), ale umožňuje aj pokročilejšiu optimalizáciu. Ďalšie informácie nájdete v zdrojoch.

Ak ste začiatočníkom v Jave, zložitosť JVM vám bude veľa zabaľovať hlavu. Dobrá správa je, že naozaj nemusíte! JVM riadi kompiláciu a optimalizáciu kódu, takže sa nemusíte obávať strojových pokynov a optimálneho spôsobu zápisu aplikačného kódu pre základnú architektúru platformy.

Od bajtového kódu Java po vykonávanie

Keď máte svoj kód Java skompilovaný do bajtkódu, ďalším krokom je preloženie pokynov bajtkódu do strojového kódu. Môže to urobiť tlmočník alebo prekladač.

Výklad

Najjednoduchšia forma kompilácie bytecode sa nazýva interpretácia. An tlmočník jednoducho vyhľadá hardvérové ​​pokyny pre každú inštrukciu bytecode a odošle ich na vykonanie CPU.

Mohli by ste myslieť na tlmočenie podobne ako pri použití slovníka: pre konkrétne slovo (inštrukcia bytecode) existuje presný preklad (inštrukcia strojového kódu). Pretože tlmočník číta a okamžite vykonáva po jednej inštrukcii bytecode, nie je možné optimalizovať ju cez sadu inštrukcií. Tlmočník musí tiež robiť tlmočenie zakaždým, keď sa vyvolá bytecode, čo ho robí pomerne pomalým. Interpretácia je presný spôsob vykonávania kódu, ale neoptimalizovaná sada výstupných inštrukcií pravdepodobne nebude najvýkonnejšou sekvenciou pre procesor cieľovej platformy.

Kompilácia

A zostavovateľ na druhej strane načíta celý kód, ktorý sa má vykonať, do modulu runtime. Pri preklade bytecode má schopnosť prezerať celý alebo čiastočný kontext modulu runtime a rozhodovať o tom, ako skutočne preložiť kód. Jeho rozhodnutia sú založené na analýze kódových grafov, ako sú rôzne vetvy vykonávania pokynov a dáta za behu.

Keď je sekvencia bajtových kódov preložená do sady inštrukcií strojového kódu a je možné vykonať optimalizáciu pre túto sadu inštrukcií, náhradná sada inštrukcií (napr. Optimalizovaná sekvencia) sa uloží do štruktúry nazývanej kódová vyrovnávacia pamäť. Pri ďalšom spustení bytového kódu môže byť predtým optimalizovaný kód okamžite umiestnený v pamäti cache kódu a použitý na vykonanie. V niektorých prípadoch môže počítadlo výkonu naštartovať a prepísať predchádzajúcu optimalizáciu, v takom prípade kompilátor spustí novú optimalizačnú sekvenciu. Výhodou kódovej medzipamäte je, že výslednú sadu inštrukcií je možné vykonať naraz - nie je potrebné interpretačné vyhľadávanie ani kompilácia! To urýchľuje čas vykonania, najmä pre aplikácie Java, kde sa rovnaké metódy nazývajú viackrát.

Optimalizácia

Spolu s dynamickou kompiláciou prichádza príležitosť vložiť počítadlá výkonu. Kompilátor môže napríklad vložiť a počítadlo výkonu počítať zakaždým, keď bol volaný blok bytecode (napr. zodpovedajúci konkrétnej metóde). Kompilátory používajú údaje o tom, aký „horúci“ je daný bajtkód, aby určili, kde bude optimalizácia kódu najlepšie ovplyvňovať spustenú aplikáciu. Dáta profilovania za behu umožňujú kompilátoru robiť za behu bohatú sadu rozhodnutí o optimalizácii kódu, čo ďalej zlepšuje výkonnosť vykonávania kódu. Keď budú k dispozícii podrobnejšie údaje na profilovanie kódu, je možné ich použiť na ďalšie a lepšie optimalizačné rozhodnutia, ako napríklad: ako lepšie postupovať v inštrukciách v kompilovanom jazyku, či nahradiť sadu inštrukcií efektívnejšou sadou, alebo dokonca či sa majú vylúčiť nadbytočné operácie.

Príklad

Zvážte kód Java:

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

Toto by mohlo staticky zostaviť javac na bytecode:

iload0 bipush 7 iadd ireturn

Keď sa táto metóda volá, blok bytecode sa dynamicky skompiluje podľa strojových pokynov. Keď počítadlo výkonu (ak je k dispozícii pre blok kódu) dosiahne prahovú hodnotu, môže sa tiež optimalizovať. Konečný výsledok by mohol vyzerať ako nasledujúca sada strojových inštrukcií pre danú platformu vykonania:

lea rax, [rdx + 7] ret

Rôzne prekladače pre rôzne aplikácie

Rôzne Java aplikácie majú rôzne potreby. Dlhodobo fungujúce podnikové aplikácie na strane servera by mohli umožniť väčšiu optimalizáciu, zatiaľ čo menšie aplikácie na strane klienta môžu vyžadovať rýchle vykonanie s minimálnou spotrebou zdrojov. Zvážme tri rôzne nastavenia kompilátora a ich príslušné výhody a nevýhody.

Kompilátory na strane klienta

Známym optimalizačným kompilátorom je C1, kompilátor, ktorý je povolený prostredníctvom -zákazník Možnosť spustenia JVM. Ako naznačuje jeho názov pri spustení, C1 je kompilátor na strane klienta. Je určený pre aplikácie na strane klienta, ktoré majú k dispozícii menej zdrojov a sú v mnohých prípadoch citlivé na čas spustenia aplikácie. C1 používa na profilovanie kódu počítadlá výkonu na umožnenie jednoduchých, relatívne nenápadných optimalizácií.

Kompilátory na strane servera

Pre dlhotrvajúce aplikácie, ako sú podnikové aplikácie Java na strane servera, nemusí kompilátor na strane klienta stačiť. Namiesto toho by sa mohol použiť kompilátor na strane servera, ako je C2. C2 je zvyčajne povolený pridaním možnosti spustenia JVM -server do príkazového riadku pri spustení. Pretože sa očakáva, že väčšina programov na strane servera bude fungovať dlho, povolenie C2 znamená, že budete môcť zhromaždiť viac profilovacích údajov, ako by ste získali pomocou krátkodobej ľahkej klientskej aplikácie. Budete teda môcť používať pokročilejšie optimalizačné techniky a algoritmy.

Tip: Zahrejte kompilátor na strane servera

Pre nasadenia na strane servera môže chvíľu trvať, kým kompilátor optimalizuje počiatočné „horúce“ časti kódu, takže nasadenia na strane servera často vyžadujú fázu „zahrievania“. Pred vykonaním akéhokoľvek druhu merania výkonu pri nasadení na strane servera sa uistite, či vaša aplikácia dosiahla ustálený stav! Ak dáte kompilátoru dostatok času na správnu kompiláciu, bude to pre váš prospech! (Viac informácií o zahriatí kompilátora a mechanizmoch profilovania nájdete v článku JavaWorld „Sledujte spustenie kompilátora HotSpot“.)

Kompilátor servera zodpovedá za viac profilovacích údajov ako kompilátor na strane klienta a umožňuje zložitejšiu analýzu vetiev, čo znamená, že zváži, ktorá optimalizačná cesta by bola výhodnejšia. Mať k dispozícii viac profilovacích údajov vedie k lepším výsledkom aplikácie. Vykonanie rozsiahlejšej profilácie a analýzy si samozrejme vyžaduje vynaloženie väčšieho množstva prostriedkov na kompilátor. JVM s povolenou C2 bude používať viac vlákien a viac cyklov CPU, bude vyžadovať väčšiu medzipamäť kódu atď.

Viacúrovňová kompilácia

Viacúrovňová kompilácia kombinuje kompiláciu na strane klienta a servera. Azul najskôr sprístupnil odstupňovanú kompiláciu vo svojom Zing JVM. V poslednej dobe (od verzie Java SE 7) bola prijatá programom Oracle Java Hotspot JVM. Viacúrovňová kompilácia využíva výhody kompilátora klienta aj servera vo vašom JVM. Kompilátor klientov je najaktívnejší počas spustenia aplikácie a spracováva optimalizácie vyvolané nižšími prahovými hodnotami počítadla výkonu. Kompilátor na strane klienta tiež vloží počítadlá výkonu a pripraví sady inštrukcií pre pokročilejšie optimalizácie, ktorým sa bude neskôr venovať kompilátor na strane servera. Viacúrovňová kompilácia je veľmi efektívny spôsob profilovania, ktorý efektívne využíva zdroje, pretože kompilátor je schopný zhromažďovať údaje počas činnosti kompilátora s nízkym dopadom, čo sa dá neskôr použiť pre pokročilejšie optimalizácie. Tento prístup tiež prináša viac informácií, ako získate z použitia samotných počítadiel profilov interpretovaného kódu.

Schéma grafu na obrázku 1 zobrazuje výkonové rozdiely medzi čistou interpretáciou, na strane klienta, na strane servera a viacúrovňovou kompiláciou. Na osi X je zobrazený čas vykonania (časová jednotka) a výkon na osi Y (operačná / časová jednotka).

Obrázok 1. Výkonové rozdiely medzi kompilátormi (kliknutím ich zväčšite)

V porovnaní s čisto interpretovaným kódom vedie použitie kompilátora na strane klienta k približne 5 až 10-krát lepšiemu výkonu vykonávania (v operáciách / s), čím sa zvyšuje výkon aplikácie. Zmeny v zisku samozrejme závisia od toho, aký efektívny je kompilátor, aké optimalizácie sú povolené alebo implementované, a (v menšej miere) to, ako dobre je aplikácia navrhnutá s ohľadom na cieľovú platformu vykonávania. Toto je skutočne niečo, s čím by si vývojár Java nikdy nemal robiť starosti.

V porovnaní s kompilátorom na strane klienta kompilátor na strane servera zvyčajne zvyšuje výkon kódu o merateľných 30 až 50 percent. Vo väčšine prípadov toto zlepšenie výkonu vyváži ďalšie náklady na zdroje.

$config[zx-auto] not found$config[zx-overlay] not found