Duboko zaronite u novi Java JIT kompajler - Graal

1. Pregled

U ovom uputstvu dublje ćemo pogledati novi Java Java Just-In-Time (JIT) prevodilac, nazvan Graal.

Vidjet ćemo što je projekt Graal i opisat ćemo jedan od njegovih dijelova, dinamički JIT kompajler visokih performansi.

2. Što je a JIT Sastavljač?

Prvo objasnimo što JIT prevodilac radi.

Kada kompajliramo naš Java program (npr., Koristeći javac naredba), završit ćemo s našim izvornim kodom kompiliranim u binarni prikaz našeg koda - JVM bytecode. Ovaj je bytecode jednostavniji i kompaktniji od našeg izvornog koda, ali konvencionalni procesori na našim računalima ne mogu ga izvršiti.

Da bi mogao pokrenuti Java program, JVM tumači bytecode. Budući da su tumači obično puno sporiji od izvornog koda koji se izvršava na stvarnom procesoru, JVM može pokrenuti još jedan kompajler koji će sada kompajlirati naš bytecode u strojni kod koji može izvršiti procesor. Ovaj takozvani prevodilac upravo u vrijeme mnogo je sofisticiraniji od javac prevodilac i izvodi složene optimizacije za generiranje visokokvalitetnog strojnog koda.

3. Detaljniji uvid u JIT kompajler

Oracleova implementacija JDK temelji se na projektu OpenJDK otvorenog koda. To uključuje HotSpot virtualni stroj, dostupno od Java verzije 1.3. To sadrži dva konvencionalna JIT-kompajlera: klijentski kompajler, koji se također naziva C1 i poslužiteljski kompajler, nazvan opto ili C2.

C1 je dizajniran da radi brže i proizvodi manje optimizirani kôd, dok C2, s druge strane, treba malo više vremena za pokretanje, ali daje bolje optimizirani kôd. Klijentski kompajler je bolji za desktop aplikacije jer ne želimo imati duge pauze za JIT-kompilaciju. Kompajler poslužitelja bolji je za dugotrajne poslužiteljske aplikacije koje mogu potrošiti više vremena na kompilaciju.

3.1. Slojevita kompilacija

Danas Java instalacija koristi oba JIT kompajlera tijekom normalnog izvršavanja programa.

Kao što smo spomenuli u prethodnom odjeljku, naš program Java, koji je sastavio javac, započinje njegovo izvršavanje u interpretiranom načinu. JVM prati svaku često zvanu metodu i sastavlja ih. Da bi to učinio, koristi C1 za kompilaciju. Ali, HotSpot i dalje motri na buduće pozive tih metoda. Ako se broj poziva poveća, JVM će ponovno sastaviti ove metode, ali ovaj put pomoću C2.

Ovo je zadana strategija koju koristi HotSpot, tzv stupnjevita kompilacija.

3.2. Kompajler poslužitelja

Usredotočimo se sada malo na C2, jer je to najsloženije od njih dvoje. C2 je izuzetno optimiziran i proizvodi kôd koji se može natjecati sa C ++ ili biti još brži. Sam prevoditelj poslužitelja napisan je na određenom dijalektu C ++.

Međutim, dolazi s nekim problemima. Zbog mogućih grešaka u segmentaciji u C ++-u, to može dovesti do pada VM-a. Također, u kompajleru tijekom posljednjih nekoliko godina nisu primijenjena značajnija poboljšanja. Kôd u C2 postalo je teško održavati, pa nismo mogli očekivati ​​nova velika poboljšanja s trenutnim dizajnom. Imajući to na umu, novi JIT kompajler kreira se u projektu nazvanom GraalVM.

4. Projekt GraalVM

Projekt GraalVM istraživački je projekt koji je kreirao Oracle. Graala možemo gledati kao nekoliko povezanih projekata: novi JIT kompajler koji se nadovezuje na HotSpot i novi virtualni stroj poliglot. Nudi sveobuhvatan ekosustav koji podržava velik skup jezika (Java i drugi jezici temeljeni na JVM-u; JavaScript, Ruby, Python, R, C / C ++ i drugi jezici temeljeni na LLVM-u).

Mi ćemo se naravno usredotočiti na Javu.

4.1. Graal - JIT kompajler Napisan na Javi

Graal je JIT-ov kompajler visokih performansi. Prihvaća JVM bytecode i proizvodi strojni kod.

Postoji nekoliko ključnih prednosti pisanja kompajlera u Javi. Prije svega sigurnost, što znači da nema rušenja, već iznimke i da nema stvarnih curenja memorije. Nadalje, imat ćemo dobru IDE podršku i moći ćemo koristiti programe za uklanjanje pogrešaka ili profile ili druge prikladne alate. Također, kompajler može biti neovisan o HotSpotu i mogao bi stvoriti bržu verziju kompiliranu od JIT-a.

Kompajler Graal stvoren je s tim prednostima na umu. Koristi novo sučelje JVM kompajlera - JVMCI za komunikaciju s VM-om. Da bismo omogućili upotrebu novog JIT kompajlera, trebamo postaviti sljedeće opcije prilikom pokretanja Jave iz naredbenog retka:

-XX: + OtključajEksperimentalneVMOptions -XX: + OmogućiJVMCI -XX: + KoristiJVMCICompiler

Što ovo znači je to možemo pokrenuti jednostavan program na tri različita načina: s uobičajenim složenim kompajlerima, s JVMCI verzijom Graal na Javi 10 ili sa samim GraalVM.

4.2. JVM sučelje kompajlera

JVMCI je dio OpenJDK-a od JDK 9, tako da za pokretanje Graala možemo koristiti bilo koji standardni OpenJDK ili Oracle JDK.

Ono što nam JVMCI zapravo omogućuje jest da izuzmemo standardnu ​​stupnjastu kompilaciju i priključimo naš potpuno novi kompajler (tj. Graal) bez potrebe da se bilo što mijenja u JVM-u.

Sučelje je prilično jednostavno. Kada Graal kompajlira metodu, proslijedit će bytecode te metode kao ulaz u JVMCI '. Kao izlaz dobit ćemo sastavljeni strojni kôd. I ulaz i izlaz su samo bajtni nizovi:

sučelje JVMCICompiler {byte [] compileMethod (byte [] bytecode); }

U stvarnim scenarijima obično će nam trebati neke dodatne informacije poput broja lokalnih varijabli, veličine sloga i podataka prikupljenih profiliranjem u interpretatoru kako bismo znali kako kôd radi u praksi.

U osnovi, kada zovete compileMethod() od JVMCICompiler sučelje, trebat ćemo proslijediti a Zahtjev za kompilacijom objekt. Zatim će vratiti Java metodu koju želimo kompajlirati i u toj ćemo metodi pronaći sve potrebne podatke.

4.3. Graal na djelu

VM izvršava sam Graal, pa će se prvo protumačiti i JIT-kompajlirati kad postane vruće. Provjerimo primjer koji se također može naći na službenoj stranici GraalVM-a:

javna klasa CountUppercase {static final int ITERATIONS = Math.max (Integer.getInteger ("iteracije", 1), 1); javna statička void main (String [] args) {String rečenica = String.join ("", args); for (int iter = 0; iter <ITERATIONS; iter ++) {if (ITERATIONS! = 1) {System.out.println ("- iteracija" + (iter + 1) + "-"); } ukupno ukupno = 0, start = System.currentTimeMillis (), zadnji = start; za (int i = 1; i <10_000_000; i ++) {ukupno + = rečenica .chars () .filter (Character :: isUpperCase) .count (); if (i% 1_000_000 == 0) {long now = System.currentTimeMillis (); System.out.printf ("% d (% d ms)% n", i / 1_000_000, sada - zadnji); zadnji = sada; }} System.out.printf ("ukupno:% d (% d ms)% n", ukupno, System.currentTimeMillis () - početak); }}}

Sad ćemo ga kompajlirati i pokrenuti:

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

To će rezultirati izlazom sličnim sljedećem:

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) ukupno: 59999994 (3436 ms)

To možemo vidjeti u početku treba više vremena. Vrijeme zagrijavanja ovisi o raznim čimbenicima, poput količine višenitnog koda u aplikaciji ili broja niti koje VM koristi. Ako je manje jezgri, vrijeme zagrijavanja moglo bi biti duže.

Ako želimo vidjeti statistiku Graalovih kompilacija, trebamo dodati sljedeću zastavicu prilikom izvršavanja našeg programa:

-Dgraal.PrintCompilation = true

To će prikazati podatke koji se odnose na kompiliranu metodu, potrebno vrijeme, obrađene byte kodove (što uključuje i umetnute metode), veličinu proizvedenog strojnog koda i količinu memorije dodijeljene tijekom kompajliranja. Izlaz izvršenja zauzima prilično puno prostora, pa ga ovdje nećemo prikazivati.

4.4. Usporedba s najprikladnijim sastavljačem

Usporedimo sada gornje rezultate s izvršavanjem istog programa kompajliranog s vršnim kompajlerom. Da bismo to učinili, moramo reći VM-u da ne koristi JVMCI kompajler:

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

Vidimo da postoji manja razlika između pojedinih vremena. To također rezultira kraćim početnim vremenom.

4.5. Struktura podataka koja stoji iza Graala

Kao što smo ranije rekli, Graal u osnovi pretvara bajtni niz u drugi bajtni niz. U ovom ćemo se dijelu usredotočiti na ono što stoji iza ovog postupka. Sljedeći se primjeri oslanjaju na govor Chrisa Seatona na JokerConf 2017.

Posao osnovnog kompajlera je općenito djelovanje prema našem programu. To znači da ga mora simbolizirati odgovarajućom strukturom podataka. Graal u tu svrhu koristi graf, takozvani graf ovisnosti o programu.

U jednostavnom scenariju, gdje želimo dodati dvije lokalne varijable, tj. x + y, imali bismo jedan čvor za učitavanje svake varijable i drugi čvor za njihovo dodavanje. Pored toga, imali bismo i dva ruba koji predstavljaju tok podataka:

Rubovi protoka podataka prikazani su plavom bojom. Ističu da kada se učitaju lokalne varijable, rezultat ide u operaciju zbrajanja.

Uvedimo sada drugi tip bridova, oni koji opisuju regulacijski tok. Da bismo to učinili, proširit ćemo naš primjer pozivanjem metoda za dohvaćanje naših varijabli umjesto da ih izravno čitamo. Kada to učinimo, moramo pratiti metode koje pozivaju redoslijed. Predstavit ćemo ovu narudžbu crvenim strelicama:

Ovdje možemo vidjeti da se čvorovi zapravo nisu promijenili, ali imamo dodane rubove kontrolnog toka.

4.6. Stvarni grafikoni

Stvarne Graalove grafikone možemo ispitati pomoću IdealGraphVisualisera. Da bismo ga pokrenuli, koristimo mx igv naredba. Također moramo konfigurirati JVM postavljanjem -Dgraal.Dump zastava.

Provjerimo jednostavan primjer:

int prosjek (int a, int b) {return (a + b) / 2; }

Ovo ima vrlo jednostavan protok podataka:

Na gornjem grafikonu možemo vidjeti jasan prikaz naše metode. Parametri P (0) i P (1) ulaze u operaciju zbrajanja koja ulazi u operaciju dijeljenja s konstantom C (2). Konačno, rezultat se vraća.

Sada ćemo promijeniti prethodni primjer kako bi bio primjenjiv na niz brojeva:

int prosjek (int [] vrijednosti) {int zbroj = 0; for (int n = 0; n <values.length; n ++) {zbroj + = vrijednosti [n]; } povratna suma / vrijednosti.duljina; }

Vidimo da nas je dodavanje petlje dovelo do mnogo složenijeg grafa:

Ono što možemo primijetiti ovdje su:

  • čvorovi petlje početak i kraj
  • čvorovi koji predstavljaju čitanje niza i čitanje duljine niza
  • podataka i kontrolirati rubove protoka, baš kao i prije.

Ta se struktura podataka ponekad naziva more-čvorovi ili juha-čvorova. Moramo napomenuti da kompajler C2 koristi sličnu strukturu podataka, tako da nije nešto novo, inovirano isključivo za Graal.

Valja napomenuti da Graal optimizira i sastavlja naš program modificirajući gore spomenutu strukturu podataka. Možemo vidjeti zašto je zapravo bio dobar izbor napisati kompajler Graal JIT na Javi: graf nije ništa drugo do skup objekata s referencama koje ih povezuju kao rubove. Ta je struktura savršeno kompatibilna s objektno orijentiranim jezikom, što je u ovom slučaju Java.

4.7. Način kompajlera unaprijed

Također je važno napomenuti da možemo koristiti i Graalov kompajler u načinu kompajlera Ahead-of-Time u Javi 10. Kao što smo već rekli, Graalov kompajler napisan je ispočetka. Prilagođen je novom čistom sučelju, JVMCI, što nam omogućuje integraciju s HotSpotom. To ne znači da je prevoditelj za to ipak vezan.

Jedan od načina korištenja kompajlera je korištenje profila usmjerenog pristupa za kompiliranje samo vrućih metoda, ali također možemo iskoristiti Graal za cjelovitu kompilaciju svih metoda u offline načinu bez izvršavanja koda. Ovo je takozvana „Ahead-of-Time Compilation“, JEP 295, ali ovdje nećemo ulaziti duboko u tehnologiju AOT kompilacije.

Glavni razlog zašto bismo Graal koristili na ovaj način jest ubrzati vrijeme pokretanja dok redoviti pristup Tiered Compilation u HotSpotu ne preuzme.

5. Zaključak

U ovom smo članku istražili funkcionalnosti novog Java JIT kompajlera kao dijela projekta Graal.

Prvo smo opisali tradicionalne JIT kompajlere, a zatim raspravljali o novim značajkama Graala, posebno novom sučelju JVM Compilera. Zatim smo ilustrirali kako rade oba sastavljača i usporedili njihove izvedbe.

Nakon toga, razgovarali smo o strukturi podataka koju Graal koristi za manipulaciju našim programom i, konačno, o načinu AOT kompajlera kao drugom načinu korištenja Graala.

Kao i uvijek, izvorni kod možete pronaći na GitHubu. Imajte na umu da JVM mora biti konfiguriran s određenim zastavicama - koje su ovdje opisane.