Mély merülés az új Java JIT fordítóba - Graal

1. Áttekintés

Ebben az oktatóanyagban mélyebben áttekintjük az új Java Just-In-Time (JIT) fordítót, a Graal nevet.

Meglátjuk, mi a Graal projekt, és leírjuk annak egyik részét, egy nagy teljesítményű dinamikus JIT fordítót.

2. Mi az a JIT Fordítóprogram?

Először magyarázzuk el, mit csinál a JIT fordító.

Amikor lefordítjuk a Java programunkat (például a javac parancs), végül a forráskódunkat fordítjuk a kódunk bináris ábrázolásába - egy JVM bytecode. Ez a bájtkód egyszerűbb és kompaktabb, mint a forráskódunk, de a számítógépeink hagyományos processzorai nem tudják végrehajtani.

Java program futtatásához a JVM értelmezi a bájtkódot. Mivel a tolmácsok általában sokkal lassabbak, mint a valós processzoron végrehajtott natív kódok, a A JVM futtathat egy másik fordítót, amely a bájtkódunkat a processzor által futtatható gépi kódba fordítja.. Ez az úgynevezett just-in-time fordító sokkal kifinomultabb, mint a javac fordítóval, és komplex optimalizálásokat futtat a kiváló minőségű gépi kód előállításához.

3. Részletesebb áttekintés a JIT fordítóban

Az Oracle JDK megvalósítása a nyílt forráskódú OpenJDK projekten alapul. Ide tartozik a HotSpot virtuális gép, elérhető a Java 1.3 verziója óta. Azt két hagyományos JIT-fordítót tartalmaz: az ügyfél fordítót, más néven C1, és a szerver fordítót, az úgynevezett opto vagy C2.

A C1-et úgy tervezték, hogy gyorsabban fusson és kevésbé optimalizált kódot állítson elő, míg a C2-nek viszont egy kicsit több időbe telik a futtatása, de egy jobban optimalizált kódot állít elő. Az ügyfél fordító jobban megfelel az asztali alkalmazásoknak, mivel nem akarunk hosszú szüneteket tartani a JIT-fordításhoz. A szerver fordító jobban alkalmazható olyan hosszú ideig futó kiszolgáló alkalmazások számára, amelyek több időt tölthetnek a fordítással.

3.1. Többszintű összeállítás

Ma a Java telepítése mindkét JIT fordítót használja a normál programfuttatás során.

Amint azt az előző szakaszban említettük, a Java programunk, amelyet javac, értelmezett módban kezdi meg a végrehajtását. A JVM nyomon követi az egyes gyakran hívott módszereket és összeállítja azokat. Ennek érdekében a C1-et használja az összeállításhoz. De a HotSpot továbbra is figyelemmel kíséri e módszerek jövőbeli hívásait. Ha a hívások száma növekszik, a JVM újra összeállítja ezeket a módszereket, de ezúttal a C2 használatával.

Ez az alapértelmezett stratégia, amelyet a HotSpot használ többszintű összeállítás.

3.2. A szerver fordító

Most koncentráljunk egy kicsit a C2-re, mivel ez a kettő közül a legösszetettebb. A C2 rendkívül optimalizált és olyan kódot állít elő, amely képes versenyezni a C ++ - val vagy még gyorsabb. Maga a szerver fordító a C ++ sajátos dialektusával van megírva.

Ez azonban néhány kérdéssel jár. A C ++ rendszerben lehetséges szegmentálási hibák miatt a virtuális gép összeomolhat. Ezenkívül az utóbbi években nem hajtottak végre jelentős fejlesztéseket a fordítóban. A C2 kódját nehéz fenntartani, ezért a jelenlegi kialakítással nem számíthattunk új jelentős fejlesztésekre. Ezt szem előtt tartva, az új JIT fordító készül a GraalVM nevű projektben.

4. GraalVM projekt

A GraalVM projekt az Oracle által létrehozott kutatási projekt. Több összekapcsolt projektnek tekinthetjük a Graalt: egy új JIT fordítót, amely a HotSpot-ra épül, és egy új poliglot virtuális gépet. Átfogó ökoszisztémát kínál, amely számos nyelvet támogat (Java és más JVM-alapú nyelvek; JavaScript, Ruby, Python, R, C / C ++ és más LLVM-alapú nyelvek).

Természetesen a Java-ra koncentrálunk.

4.1. Graal - JIT fordító Java nyelven írva

A Graal egy nagy teljesítményű JIT fordító. Elfogadja a JVM bájtkódot és előállítja a gép kódját.

A fordító Java-ba történő írásának számos fő előnye van. Először is, a biztonság, vagyis nem összeomlik, hanem kivételek, és nincs igazi memóriaszivárgás. Ezenkívül jó IDE támogatással rendelkezünk, és képesek leszünk hibakeresőket, profilokat vagy más kényelmes eszközöket használni. A fordító független lehet a HotSpot-tól, és képes lenne előállítani egy gyorsabb, a JIT által lefordított verziót.

A Graal fordító ezeket az előnyöket szem előtt tartva jött létre. Az új JVM Compiler Interface - JVMCI segítségével kommunikál a virtuális géppel. Az új JIT fordító használatának engedélyezéséhez a Java parancssorból történő futtatásakor a következő beállításokat kell megadnunk:

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

Ez azt jelenti, hogy egyszerű programot futtathatunk háromféleképpen: a szokásos többszintű fordítókkal, a Graal JVMCI verziójával a Java 10-en vagy magával a GraalVM-mel.

4.2. JVM Compiler Interface

A JVMCI az OpenJDK része a JDK 9 óta, így bármilyen szokásos OpenJDK-t vagy Oracle JDK-t használhatunk a Graal futtatására.

Amit a JVMCI valójában lehetővé tesz számunkra, az az, hogy kizárjuk a szokásos többszintű összeállítást és bekapcsoljuk a vadonatúj fordítónkat (azaz a Graal-t) anélkül, hogy bármit is változtatnunk kellene a JVM-ben.

A kezelőfelület meglehetősen egyszerű. Amikor Graal metódust állít össze, akkor a metódus byte-kódját adja át a JVMCI-nek. Kimenetként megkapjuk a lefordított gépi kódot. A bemenet és a kimenet is csak bájtos tömbök:

interfész JVMCICompiler {byte [] compileMethod (byte [] bytecode); }

Valódi élethelyzetekben általában szükségünk lesz még néhány információra, például a helyi változók számára, a verem méretére és a tolmácsban történő profilalkotás során összegyűjtött információkra, hogy tudjuk, hogyan működik a kód a gyakorlatban.

Lényegében a compileMethod() JVMCICompiler felületen, át kell adnunk a CompilationRequest tárgy. Ezután visszaadja a fordítani kívánt Java-módszert, és ebben a módszerben megtaláljuk az összes szükséges információt.

4.3. Graal akcióban

Magát a Graal-t a virtuális gép hajtja végre, így először akkor kell értelmezni és a JIT-t összeállítani, amikor felforrósodik. Nézzünk meg egy példát, amely a GraalVM hivatalos oldalán is megtalálható:

public class CountUppercase {static final int ITERÁCIÓK = Math.max (Integer.getInteger ("iterációk", 1), 1); public static void main (Karakterlánc [] args) {Karaktersorozat = Karakterlánc.csatlakozás ("", args); mert (int iter = 0; iter <ITERÁCIÓK; iter ++) {if (ITERÁCIÓK! = 1) {System.out.println ("- iteráció" + (iter + 1) + "-"); } long total = 0, start = System.currentTimeMillis (), last = start; for (int i = 1; i <10_000_000; i ++) {összesen + = mondat .chars () .filter (Character :: isUpperCase) .count (); if (i% 1_000_000 == 0) {régóta = System.currentTimeMillis (); System.out.printf ("% d (% d ms)% n", i / 1_000_000, most - utolsó); utolsó = most; }} System.out.printf ("összesen:% d (% d ms)% n", összesen, System.currentTimeMillis () - start); }}}

Most összeállítjuk és futtatjuk:

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

Ennek eredménye a következőhöz hasonló kimenet lesz:

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) összesen: 59999994 (3436) Kisasszony)

Ezt láthatjuk kezdetben több időbe telik. Ez a bemelegedési idő számos tényezőtől függ, például az alkalmazásban lévő többszálas kód mennyiségétől vagy a virtuális gép által használt szálak számától. Ha kevesebb mag van, a bemelegedési idő hosszabb lehet.

Ha meg akarjuk tekinteni a Graal-összeállítások statisztikáit, a program végrehajtásakor hozzá kell adnunk a következő jelölőt:

-Dgraal.PrintCompilation = true

Ez megmutatja a lefordított módszerrel kapcsolatos adatokat, az igénybe vett időt, a feldolgozott bytecode-okat (amely magában foglalja a beillesztett módszereket is), az előállított gépi kód méretét és a fordítás során lefoglalt memória mennyiségét. A végrehajtás kimenete elég sok helyet foglal el, ezért itt nem fogjuk megmutatni.

4.4. Összehasonlítva a Top Tier Compiler-rel

Most hasonlítsuk össze a fenti eredményeket ugyanazon program végrehajtásával, amelyet a legfelső szintű fordítóval fordítottunk le. Ehhez meg kell mondanunk a virtuális gépnek, hogy ne használja a JVMCI fordítót:

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) összesen: 59999994 (4004 ms)

Láthatjuk, hogy kisebb különbség van az egyes idők között. Rövidebb kezdési időt is eredményez.

4.5. Az adatstruktúra a Graal mögött

Mint korábban mondtuk, Graal alapvetően egy bájt tömböt alakít át egy másik bájt tömbgé. Ebben a részben arra fogunk koncentrálni, hogy mi áll a folyamat hátterében. A következő példák Chris Seaton beszédére támaszkodnak a JokerConf 2017 rendezvényen.

A fordító alapvető feladata általában a programunk szerint cselekedni. Ez azt jelenti, hogy megfelelő adatszerkezettel kell szimbolizálnia. Graal ilyen célra grafikont használ, az úgynevezett programfüggőség-gráfot.

Egyszerű forgatókönyv esetén, ahol két helyi változót akarunk hozzáadni, azaz x + y, lenne egy csomópontunk minden változó betöltésére, és egy másik csomópont hozzáadásra. Emellett, két élünk is van az adatfolyamot ábrázolva:

Az adatfolyam élek kék színnel jelennek meg. Arra hívják fel a figyelmet, hogy a helyi változók betöltésekor az eredmény az összeadási műveletbe megy.

Most mutassuk be egy másik típusú él, azok, amelyek leírják a szabályozási folyamatot. Ehhez kibővítjük példánkat azzal, hogy metódusokat hívunk meg a változók lekérésére ahelyett, hogy közvetlenül elolvasnánk őket. Amikor ezt megtesszük, nyomon kell követnünk a sorrendet hívó módszereket. Ezt a sorrendet a piros nyilakkal ábrázoljuk:

Itt láthatjuk, hogy a csomópontok nem változtak valójában, de hozzáadtuk a vezérlő áramlási éleket.

4.6. Tényleges grafikonok

A valós Graal-grafikonokat az IdealGraphVisualiser segítségével vizsgálhatjuk meg. A futtatásához a mx igv parancs. Be kell állítanunk a JVM-et is a -Dgraal.Dump zászló.

Nézzünk meg egy egyszerű példát:

int átlag (int a, int b) {hozam (a + b) / 2; }

Ennek nagyon egyszerű adatáramlása van:

A fenti grafikonon látható a módszerünk világos ábrázolása. A P (0) és P (1) paraméterek az összeadási műveletbe áramlanak, amely az osztási műveletbe a C (2) állandóval lép. Végül az eredmény visszatér.

Most megváltoztatjuk az előző példát, hogy alkalmazható legyen egy tömb számra:

int átlag (int [] értékek) {int összeg = 0; for (int n = 0; n <értékek.hossz; n ++) {összeg + = értékek [n]; } visszatérési összeg / értékek.hossz; }

Láthatjuk, hogy egy hurok hozzáadása a sokkal összetettebb grafikonra vezetett minket:

Amit észrevehetünk itt vannak:

  • a kezdő és a vég hurok csomópontok
  • a tömbolvasást és a tömbhosszt leolvasó csomópontok
  • adatok és vezérlési folyamat élek, csakúgy, mint korábban.

Ezt az adatszerkezetet néha csomópontok tengerének vagy csomópontok levesének nevezik. Meg kell említenünk, hogy a C2 fordító hasonló adatstruktúrát használ, tehát nem valami új, kizárólag a Graal számára fejlesztették ki.

Figyelemre méltó, hogy Graal a fent említett adatszerkezet módosításával optimalizálja és állítja össze programunkat. Láthatjuk, miért volt jó választás a Graal JIT fordító Java-ra írása: a gráf nem más, mint objektumok halmaza, referenciákkal összekötve őket élként. Ez a szerkezet tökéletesen kompatibilis az objektum-orientált nyelvvel, amely jelen esetben a Java.

4.7. Időt megelőző fordító mód

Fontos megemlíteni azt is használhatjuk a Graal fordítót a Java 10 előtti fordító módban is. Mint már mondtuk, a Graal fordítót a semmiből írták. Megfelel egy új tiszta felületnek, a JVMCI-nek, amely lehetővé teszi számunkra, hogy integráljuk a HotSpot-ba. Ez nem azt jelenti, hogy a fordító mégis kötődne hozzá.

A fordító használatának egyik módja az, hogy profilvezérelt megközelítéssel csak a forró módszereket állítja össze, de használhatjuk a Graal-t is, hogy offline módban összesített módszert fordítsunk össze a kód végrehajtása nélkül. Ez egy úgynevezett „Ahead-of-Time Compilation”, JEP 295, de itt nem mélyedünk el az AOT fordítási technológiában.

A Graal ilyen módon történő használatának fő oka az indítási idő felgyorsítása, amíg a HotSpot rendszeres Tiered Compilation megközelítése átveszi az irányítást.

5. Következtetés

Ebben a cikkben az új Java JIT fordító funkcióit vizsgáltuk a Graal projekt részeként.

Először ismertettük a hagyományos JIT fordítókat, majd megvitattuk a Graal új funkcióit, különös tekintettel az új JVM Compiler felületre. Ezután szemléltettük, hogy mindkét fordító hogyan működik, és összehasonlítottuk teljesítményüket.

Ezt követően beszéltünk arról az adatszerkezetről, amelyet a Graal használ a programunk manipulálására, és végül az AOT fordító módról, mint a Graal használatának másik módjáról.

Mint mindig, a forráskód megtalálható a GitHubon. Ne feledje, hogy a JVM-et az itt leírt speciális jelzőkkel kell konfigurálni.