Deep Dive Into the New Java JIT Compiler - Graal

1. Prehľad

V tomto výučbe sa pozrieme hlbšie na nový kompilátor Java Just-In-Time (JIT) s názvom Graal.

Uvidíme, aký je projekt Graal, a popíšeme jednu z jeho častí, vysoko výkonný dynamický kompilátor JIT.

2. Čo je a JIT Kompilátor?

Najprv si vysvetlíme, čo robí kompilátor JIT.

Keď zostavujeme náš program Java (napr. Pomocou javac príkaz), skončíme s naším zdrojovým kódom skompilovaným do binárnej reprezentácie nášho kódu - bajkódom JVM. Tento bytecode je jednoduchší a kompaktnejší ako náš zdrojový kód, ale bežné procesory v našich počítačoch ho nemôžu vykonať.

Aby bolo možné spustiť program Java, interpretuje JVM bytecode. Pretože tlmočníci sú zvyčajne oveľa pomalší ako natívny kód vykonávajúci na skutočnom procesore, JVM môže spustiť ďalší kompilátor, ktorý teraz skompiluje náš bytecode do strojového kódu, ktorý môže spustiť procesor. Tento takzvaný just-in-time kompilátor je oveľa sofistikovanejší ako javac kompilátor a vykonáva komplexné optimalizácie na generovanie vysokokvalitného strojového kódu.

3. Podrobnejší pohľad do kompilátora JIT

Implementácia JDK spoločnosťou Oracle je založená na projekte OpenJDK s otvoreným zdrojom. Patria sem: Virtuálny stroj HotSpot, k dispozícii od verzie Java 1.3. To obsahuje dva bežné kompilátory JIT: klientsky kompilátor, nazývaný tiež C1 a serverový kompilátor, nazývaný opto alebo C2.

C1 je navrhnutý tak, aby bežal rýchlejšie a produkoval menej optimalizovaný kód, zatiaľ čo C2 trvá spustenie naopak trochu dlhšie, ale produkuje lepšie optimalizovaný kód. Klientský kompilátor sa lepšie hodí pre desktopové aplikácie, pretože nechceme mať dlhé pauzy pre kompiláciu JIT. Serverový kompilátor je lepší pre dlho bežiace serverové aplikácie, ktoré môžu kompilácii venovať viac času.

3.1. Viacúrovňová kompilácia

Inštalácia Java dnes používa oba kompilátory JIT počas bežného vykonávania programu.

Ako sme už spomenuli v predchádzajúcej časti, náš program Java, ktorý zostavil javac, spustí jeho vykonávanie v interpretovanom režime. JVM sleduje každú často nazývanú metódu a zostavuje ich. Na tento účel používa na kompiláciu C1. HotSpot však stále sleduje budúce volania týchto metód. Ak sa počet hovorov zvýši, JVM tieto metódy ešte raz prekompiluje, tentokrát však pomocou C2.

Toto je predvolená stratégia, ktorú používa HotSpot, tzv odstupňovaná kompilácia.

3.2. Serverový kompilátor

Poďme sa teraz trochu zamerať na C2, pretože je z nich najkomplexnejšia. C2 bol extrémne optimalizovaný a produkuje kód, ktorý dokáže konkurovať C ++ alebo byť ešte rýchlejší. Samotný kompilátor servera je napísaný v špecifickom dialekte jazyka C ++.

Prichádza však s niektorými problémami. Kvôli možným poruchám segmentácie v C ++ môže spôsobiť zlyhanie VM. V kompilátore tiež neboli za posledných niekoľko rokov implementované žiadne zásadné vylepšenia. Kód v C2 sa stal ťažko udržiavateľným, takže sme pri súčasnom dizajne nemohli čakať nové veľké vylepšenia. Z tohto dôvodu sa v projekte s názvom GraalVM vytvára nový kompilátor JIT.

4. Projekt GraalVM

Projekt GraalVM je výskumný projekt vytvorený spoločnosťou Oracle. Na Graal sa môžeme pozerať ako na niekoľko prepojených projektov: nový kompilátor JIT, ktorý stavia na HotSpote, a nový virtuálny stroj polyglot. Ponúka komplexný ekosystém podporujúci veľkú skupinu jazykov (Java a ďalšie jazyky založené na JVM; JavaScript, Ruby, Python, R, C / C ++ a ďalšie jazyky založené na LLVM).

Samozrejme sa zameriame na Javu.

4.1. Graal - kompilátor JIT napísaný v jazyku Java

Graal je vysoko výkonný kompilátor JIT. Prijíma bytový kód JVM a vytvára strojový kód.

Existuje niekoľko kľúčových výhod písania kompilátora v Jave. V prvom rade bezpečnosť, to znamená žiadne pády, ale skôr výnimky a žiadne skutočné úniky pamäte. Ďalej budeme mať dobrú podporu IDE a budeme môcť používať debuggery alebo profilovače alebo iné pohodlné nástroje. Kompilátor tiež môže byť nezávislý na HotSpote a bude schopný vytvoriť rýchlejšiu verziu kompilácie JIT.

Kompilátor Graal bol vytvorený s ohľadom na tieto výhody. Na komunikáciu s VM využíva nové rozhranie JVM Compiler Interface - JVMCI. Aby sme umožnili použitie nového kompilátora JIT, musíme pri spustení Javy z príkazového riadku nastaviť nasledujúce možnosti:

-XX: + UnlockExperimentalVMOptions -XX: + EnableJVMCI -XX: + UseJVMCICompiler

Čo to znamená, je to môžeme spustiť jednoduchý program tromi rôznymi spôsobmi: s bežnými odstupňovanými kompilátormi, s verziou Graal v JVMCI verzie Java 10 alebo so samotným GraalVM.

4.2. Rozhranie kompilátora JVM

JVMCI je súčasťou OpenJDK od JDK 9, takže na spustenie Graalu môžeme použiť akýkoľvek štandardný OpenJDK alebo Oracle JDK.

Čo nám vlastne JVMCI umožňuje, je vylúčiť štandardnú odstupňovanú kompiláciu a zapojiť náš úplne nový kompilátor (t. J. Graal) bez toho, aby bolo potrebné v JVM niečo meniť.

Rozhranie je celkom jednoduché. Keď Graal zostavuje metódu, odovzdá bytecode tejto metódy ako vstup do JVMCI '. Ako výstup dostaneme skompilovaný strojový kód. Vstup aj výstup sú iba bajtové polia:

rozhranie JVMCICompiler {byte [] compileMethod (byte [] bytecode); }

V scenároch z reálneho života budeme zvyčajne potrebovať ďalšie informácie, ako je počet lokálnych premenných, veľkosť zásobníka a informácie zhromaždené z profilovania v tlmočníkovi, aby sme vedeli, ako kód beží v praxi.

V podstate, keď voláte na compileMethod() z JVMCICompiler rozhranie, budeme musieť zložiť a CompilationRequest objekt. Potom vráti metódu Java, ktorú chceme skompilovať, a v tejto metóde nájdeme všetky potrebné informácie.

4.3. Grál v akcii

Samotný Graal je vykonávaný VM, takže bude najskôr interpretovaný a kompilovaný JIT, keď bude horúci. Pozrime sa na príklad, ktorý možno nájsť aj na oficiálnej stránke GraalVM:

verejná trieda CountUppercase {statická konečná int ITERÁCIE = Math.max (Integer.getInteger ("iterácie", 1), 1); public static void main (String [] args) {Reťazec vety = String.join ("", args); for (int iter = 0; iter <ITERATIONS; iter ++) {if (ITERATIONS! = 1) {System.out.println ("- iterácia" + (iter + 1) + "-"); } long total = 0, start = System.currentTimeMillis (), last = start; pre (int i = 1; i <10_000_000; i ++) {celkom + = veta .chars () .filter (Character :: isUpperCase) .count (); if (i% 1_000_000 == 0) {dlho teraz = System.currentTimeMillis (); System.out.printf ("% d (% d ms)% n", i / 1_000_000, teraz - posledný); posledný = teraz; }} System.out.printf ("total:% d (% d ms)% n", total, System.currentTimeMillis () - štart); }}}

Teraz to skompilujeme a spustíme:

javac CountUppercase.java java -XX: + UnlockExperimentalVMOptions -XX: + EnableJVMCI -XX: + UseJVMCICompiler

Výsledkom bude výstup podobný tomuto:

1 (1581 ms) 2 (480 ms) 3 (364 ms) 4 (231 ms) 5 (196 ms) 6 (121 ms) 7 (116 ms) 8 (116 ms) 9 (116 ms) celkom: 59999994 (3436 pani)

To vidíme na začiatku to chce viac času. Tento čas na zahriatie závisí od rôznych faktorov, napríklad od množstva viacvláknového kódu v aplikácii alebo od počtu vlákien, ktoré VM používa. Ak je jadier menej, môže byť čas na zahriatie dlhší.

Ak chceme vidieť štatistiku kompilácií Graal, musíme pri spustení nášho programu pridať nasledujúci príznak:

-Dgraal.PrintCompilation = true

Zobrazia sa údaje týkajúce sa skompilovanej metódy, čas, spracované bajtkódy (ktoré zahŕňajú aj vložené metódy), veľkosť vyprodukovaného strojového kódu a veľkosť pamäte pridelenej počas kompilácie. Výstup vykonania zaberá pomerne veľa priestoru, takže ho tu neukážeme.

4.4. V porovnaní s kompilátorom najvyššej úrovne

Poďme teraz porovnať vyššie uvedené výsledky s vykonaním toho istého programu, ktorý je kompilovaný namiesto toho s kompilátorom najvyššej úrovne. Aby sme to dosiahli, musíme VM povedať, aby nepoužíval kompilátor JVMCI:

java -XX: + UnlockExperimentalVMOptions -XX: + EnableJVMCI -XX: -UseJVMCICompiler 1 (510 ms) 2 (375 ms) 3 (365 ms) 4 (368 ms) 5 (348 ms) 6 (370 ms) 7 (353 ms ) 8 (348 ms) 9 (369 ms) celkom: 59999994 (4004 ms)

Vidíme, že medzi jednotlivými časmi je menší rozdiel. Výsledkom je tiež kratší počiatočný čas.

4.5. Dátová štruktúra za Graalom

Ako sme už povedali, Graal v podstate premení bajtové pole na iné bajtové pole. V tejto časti sa zameriame na to, čo stojí za týmto procesom. Nasledujúce príklady sa opierajú o prednášku Chrisa Seatona na JokerConf 2017.

Základnou úlohou prekladača je vo všeobecnosti konať podľa nášho programu. To znamená, že ho musí symbolizovať vhodnou dátovou štruktúrou. Graal na tento účel používa graf, takzvaný graf závislosti programu.

V jednoduchom scenári, kde chceme pridať dve lokálne premenné, t.j. x + r, mali by sme jeden uzol na načítanie každej premennej a ďalší uzol na ich pridanie. Popri tom, mali by sme tiež dva okraje predstavujúce tok údajov:

Hrany toku údajov sú zobrazené modrou farbou. Poukazujú na to, že keď sa načítajú lokálne premenné, výsledok ide do operácie pridania.

Poďme sa teraz predstaviť iný typ hrán, tie, ktoré popisujú riadiaci tok. Aby sme to dosiahli, rozšírime náš príklad volaním metód na získanie našich premenných namiesto ich priameho čítania. Keď to robíme, musíme sledovať poradie volaných metód. Túto objednávku budeme reprezentovať červenými šípkami:

Tu vidíme, že uzly sa vlastne nezmenili, ale máme pridané kontrolné hrany toku.

4.6. Aktuálne grafy

Skutočné grafy Grálu môžeme preskúmať pomocou nástroja IdealGraphVisualiser. Na jeho spustenie používame mx igv príkaz. Musíme tiež nakonfigurovať JVM nastavením -Dgraal.Dump vlajka.

Pozrime sa na jednoduchý príklad:

int priemer (int a, int b) {návrat (a + b) / 2; }

Toto má veľmi jednoduchý tok údajov:

Na vyššie uvedenom grafe vidíme zreteľné znázornenie našej metódy. Parametre P (0) a P (1) prúdia do operácie sčítania, ktorá vstupuje do operácie delenia s konštantou C (2). Nakoniec sa výsledok vráti.

Teraz zmeníme predchádzajúci príklad tak, aby bol použiteľný pre pole čísel:

int priemer (hodnoty int []) {int suma = 0; pre (int n = 0; n <values.length; n ++) {sum + = hodnoty [n]; } návratová suma / hodnoty.dĺžka; }

Vidíme, že pridanie slučky nás priviedlo k oveľa zložitejšiemu grafu:

Čo si môžeme všimnúť tu sú:

  • uzly začiatočnej a koncovej slučky
  • uzly predstavujúce čítanie poľa a čítanie dĺžky poľa
  • dáta a riadiť hrany toku, rovnako ako predtým.

Táto dátová štruktúra sa niekedy nazýva more uzlov alebo polievka uzlov. Musíme spomenúť, že kompilátor C2 používa podobnú dátovú štruktúru, takže nejde o niečo nové, inovované výlučne pre Graal.

Je potrebné spomenúť, že Graal optimalizuje a zostavuje náš program úpravou vyššie uvedenej dátovej štruktúry. Vidíme, prečo bola skutočne dobrá voľba napísať kompilátor Graal JIT v Jave: graf nie je nič iné ako množina objektov s referenciami spájajúcimi ich ako hrany. Táto štruktúra je úplne kompatibilná s objektovo orientovaným jazykom, ktorým je v tomto prípade Java.

4.7. Predbežný režim kompilátora

Je tiež dôležité spomenúť to môžeme tiež použiť kompilátor Graal v režime kompilátora Ahead-of-Time v Jave 10. Ako sme už povedali, kompilátor Graal bol napísaný úplne od začiatku. Vyhovuje novému čistému rozhraniu JVMCI, ktoré nám umožňuje jeho integráciu s HotSpotom. To však neznamená, že kompilátor je s ním viazaný.

Jedným zo spôsobov použitia kompilátora je použitie prístupu založeného na profiloch na zostavenie iba horúcich metód, ale môžeme tiež použiť program Graal na vykonanie úplnej kompilácie všetkých metód v režime offline bez vykonania kódu. Toto je takzvaná „kompilácia pred časom“, JEP 295, ale nebudeme sa tu hlboko venovať kompilačnej technológii AOT.

Hlavným dôvodom, prečo by sme Graal používali týmto spôsobom, je urýchlenie času spustenia, kým sa ho ujme bežný prístup s odstupňovanou kompiláciou v HotSpote.

5. Záver

V tomto článku sme preskúmali funkcionalitu nového kompilátora Java JIT ako súčasť projektu Graal.

Najprv sme opísali tradičné kompilátory JIT a potom sme diskutovali o nových funkciách Graalu, najmä o novom rozhraní kompilátora JVM. Potom sme ilustrovali, ako obaja kompilátori fungujú, a porovnali ich výkony.

Potom sme hovorili o dátovej štruktúre, ktorú Graal používa na manipuláciu s našim programom, a nakoniec o režime kompilátora AOT ako ďalšom spôsobe použitia Graalu.

Ako vždy, zdrojový kód nájdete na GitHub. Pamätajte, že JVM je potrebné nakonfigurovať pomocou konkrétnych príznakov, ktoré sú tu popísané.


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