Programavimas

JVM efektyvumo optimizavimas. 2 dalis. Kompiliatoriai

Šiame antrame JVM našumo optimizavimo serijos straipsnyje „Java“ kompiliatoriai užima pagrindinę vietą. Eva Andreasson pristato skirtingas kompiliatorių veisles ir palygina kliento, serverio ir pakopinio kompiliavimo našumo rezultatus. Ji baigia įprastų JVM optimizavimų, tokių kaip negyvo kodo pašalinimas, įtraukimas ir ciklo optimizavimas, apžvalgą.

„Java“ kompiliatorius yra garsiosios „Java“ platformos nepriklausomybės šaltinis. Programinės įrangos kūrėjas parašo geriausią „Java“ programą, kokią tik gali, o tada kompiliatorius dirba užkulisiuose, kad sukurtų efektyvų ir gerai veikiantį numatytos tikslinės platformos vykdymo kodą. Skirtingi kompiliatorių tipai tenkina įvairius taikymo poreikius, todėl duoda konkrečių norimų našumo rezultatų. Kuo daugiau suprasite apie kompiliatorius, kalbant apie jų veikimą ir galimas jų rūšis, tuo daugiau galėsite optimizuoti „Java“ programų našumą.

Šis antrasis straipsnis JVM našumo optimizavimas serija pabrėžia ir paaiškina įvairių „Java“ virtualių mašinų kompiliatorių skirtumus. Taip pat aptarsiu keletą paprastų optimizavimų, kuriuos „Just-In-Time“ (JIT) kompiliatoriai naudoja „Java“. (Žr. „JVM našumo optimizavimas, 1 dalis“, kur rasite JVM apžvalgą ir įvadą į seriją.)

Kas yra kompiliatorius?

Paprasčiau tariant a sudarytojas ima programavimo kalbą kaip įvestį ir sukuria vykdomąją kalbą kaip išvestį. Vienas dažniausiai žinomas kompiliatorius yra javac, kuris yra įtrauktas į visus standartinius „Java“ kūrimo rinkinius (JDK). javac ima „Java“ kodą kaip įvestį ir paverčia jį baitiniu kodu - JVM vykdoma kalba. Baitų kodas saugomas .class failuose, kurie įkeliami į „Java“ vykdymo laiką, kai pradedamas „Java“ procesas.

Standartiniai procesoriai negali nuskaityti „Bytecode“, todėl jį reikia išversti į instrukcijų kalbą, kurią gali suprasti pagrindinė vykdymo platforma. JVM komponentas, kuris yra atsakingas už baitkodo vertimą į vykdomosios platformos instrukcijas, yra dar vienas kompiliatorius. Kai kurie JVM kompiliatoriai tvarko kelis vertimo lygius; pavyzdžiui, kompiliatorius gali sukurti įvairius tarpinio baitkodo atvaizdavimo lygius, kol jis virsta faktinėmis mašinų instrukcijomis, paskutiniu vertimo žingsniu.

„Bytecode“ ir JVM

Jei norite sužinoti daugiau apie baitekodą ir JVM, žr. „Bytecode pagrindai“ (Billas Vennersas, „JavaWorld“).

Žvelgiant iš platformos-agnostikos perspektyvos, mes norime, kad kodas būtų kuo labiau nepriklausomas nuo platformos, kad paskutinis vertimo lygis - nuo žemiausio vaizdavimo iki faktinio mašininio kodo - būtų žingsnis, kuris užrakina vykdymą konkrečios platformos procesoriaus architektūroje . Aukščiausias skirtumas tarp statinių ir dinaminių kompiliatorių. Iš ten mes turime galimybes, priklausomai nuo to, kokią vykdymo aplinką taikome, kokių našumo rezultatų norime ir kokių išteklių apribojimų turime laikytis. Trumpai aptariau statinius ir dinaminius kompiliatorius šios serijos 1 dalyje. Tolesniuose skyriuose paaiškinsiu šiek tiek daugiau.

Statinis ir dinaminis kompiliavimas

Statinio kompiliatoriaus pavyzdys yra anksčiau minėtas javac. Naudojant statinius kompiliatorius, įvesties kodas interpretuojamas vieną kartą, o išvesties vykdomasis failas yra tokios formos, kuri bus naudojama vykdant programą. Jei nepakeisite pirminio šaltinio ir nekompiliuosite kodo (naudodami kompiliatorių), išvestis visada duos tą patį rezultatą; taip yra todėl, kad įvestis yra statinė įvestis, o kompiliatorius yra statinis kompiliatorius.

Statiniame rinkinyje nurodykite šį „Java“ kodą

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

gautų kažką panašaus į šį baitekodą:

iload0 dvipusis 7 iadd grįžimas

Dinamiškas kompiliatorius dinamiškai verčia iš vienos kalbos į kitą, tai reiškia, kad tai vyksta vykdant kodą - vykdymo metu! Dinaminis kompiliavimas ir optimizavimas suteikia pranašumą vykdymui, nes jie gali prisitaikyti prie programos apkrovos pokyčių. Dinaminiai kompiliatoriai labai tinka „Java“ vykdymo laikams, kurie paprastai vykdomi nenuspėjamoje ir nuolat besikeičiančioje aplinkoje. Dauguma JVM naudoja dinaminį kompiliatorių, pvz., „Just-In-Time“ (JIT) kompiliatorių. Svarbiausia yra tai, kad dinaminiams kompiliatoriams ir kodo optimizavimui kartais reikia papildomų duomenų struktūrų, gijų ir procesoriaus išteklių. Kuo pažangesnė optimizavimo ar baitų kodo konteksto analizė, tuo daugiau išteklių sunaudojama kompiliavimui. Daugumoje aplinkų pridėtinės išlaidos vis dar yra labai mažos, palyginti su reikšmingu išvesties kodo našumu.

JVM veislės ir „Java“ platformos nepriklausomumas

Visi JVM diegimai turi vieną bendrą bruožą, tai yra bandymas pasiekti, kad programos baitkodas būtų išverstas į mašinos instrukcijas. Kai kurie JVM aiškina aplikacijos kodą ir naudoja našumo skaitiklius, kad sutelktų dėmesį į „karštą“ kodą. Kai kurie JVM praleidžia aiškinimą ir remiasi vien tik kompiliacija. Kompiliavimo išteklių intensyvumas gali būti didesnis smūgis (ypač kliento programoms), tačiau jis taip pat leidžia pažangesnes optimizavimo galimybes. Daugiau informacijos žr. Ištekliai.

Jei esate „Java“ pradedantysis, JVM subtilybėms bus daugybė galvą apgaubti. Geros naujienos yra tai, kad jums to tikrai nereikia! JVM valdo kodų kompiliavimą ir optimizavimą, todėl jums nereikės jaudintis dėl mašinos instrukcijų ir optimaliausio pagrindinės platformos architektūros programos kodo rašymo būdo.

Nuo „Java“ baitkodo iki vykdymo

Kai sukursite „Java“ kodą į baitų kodą, kiti veiksmai yra išversti baito kodo instrukcijas į mašininį kodą. Tai gali padaryti vertėjas arba kompiliatorius.

Interpretacija

Paprasčiausia bytecode kompiliavimo forma vadinama interpretacija. An vertėjas paprasčiausiai ieško aparatinės įrangos kiekvienos baitekodo instrukcijos ir siunčia jas vykdyti procesoriui.

Galėjai pagalvoti interpretacija panašiai kaip naudojant žodyną: tam tikram žodžiui (baitkodo instrukcija) yra tikslus vertimas (mašininio kodo instrukcija). Kadangi vertėjas skaito ir nedelsdamas vykdo vieną baito kodo komandą vienu metu, nėra galimybės optimizuoti naudojant nurodytas instrukcijas. Vertėjas taip pat turi atlikti vertimą kiekvieną kartą, kai iškviečiamas baitų kodas, todėl jis gana lėtas. Aiškinimas yra tikslus kodo vykdymo būdas, tačiau neoptimizuotas išvesties komandų rinkinys greičiausiai nebus efektyviausias tikslinės platformos procesoriaus seka.

Kompiliacija

A sudarytojas kita vertus, į vykdymo laiką įkeliamas visas vykdytinas kodas. Verčiant baitų kodą, jis gali pažvelgti į visą arba dalinį vykdymo laiko kontekstą ir priimti sprendimus, kaip iš tikrųjų išversti kodą. Jos sprendimai yra pagrįsti kodo grafikų, tokių kaip skirtingos instrukcijų vykdymo šakos ir vykdymo laiko konteksto duomenys, analize.

Kai baitų kodų seka paverčiama mašininio kodo komandų rinkiniu ir galima optimizuoti šį komandų rinkinį, pakeičiantis komandų rinkinys (pvz., Optimizuota seka) įrašomas į struktūrą, vadinamą kodo talpykla. Kitą kartą, kai bus vykdomas baitkodas, anksčiau optimizuotą kodą galima iškart rasti kodo talpykloje ir naudoti vykdymui. Kai kuriais atvejais našumo skaitiklis gali įveikti ir nepaisyti ankstesnio optimizavimo, tokiu atveju kompiliatorius vykdys naują optimizavimo seką. Kodo talpyklos pranašumas yra tas, kad gautą instrukcijų rinkinį galima vykdyti vienu metu - nereikia aiškinamųjų paieškų ar kompiliavimo! Tai pagreitina vykdymo laiką, ypač „Java“ programose, kur tie patys metodai yra vadinami kelis kartus.

Optimizavimas

Kartu su dinamišku kompiliavimu atsiranda galimybė įterpti našumo skaitiklius. Kompiliatorius gali, pavyzdžiui, įterpti a našumo skaitiklis suskaičiuoti kiekvieną kartą, kai buvo iškviestas baito kodo blokas (pvz., atitinkantis konkretų metodą). Kompiliatoriai naudoja duomenis apie tai, kaip „karštas“ yra nurodytas baitų kodas, kad nustatytų, kur kodo optimizavimas geriausiai paveiks veikiančią programą. Vykdymo laiko profiliavimo duomenys leidžia kompiliatoriams priimti daugybę kodo optimizavimo sprendimų rinkinio, dar labiau pagerinant kodo vykdymo našumą. Kai gaunama patobulintų kodų profiliavimo duomenų, juos galima naudoti priimant papildomus ir geresnius sprendimus dėl optimizavimo, pavyzdžiui: kaip geriau sekti instrukcijas sukompiliuota kalba, ar pakeisti instrukcijų rinkinį efektyvesniais rinkiniais, ar net ar pašalinti nereikalingas operacijas.

Pavyzdys

Apsvarstykite „Java“ kodą:

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

Tai galėtų statiškai sukompiliuoti javac prie baitkodo:

iload0 dvipusis 7 iadd grįžimas

Kai metodas bus vadinamas, baitų kodo blokas bus dinamiškai kompiliuojamas pagal mašinos instrukcijas. Kai našumo skaitiklis (jei yra kodo bloke) pasiekia ribą, jis taip pat gali būti optimizuotas. Galutinis rezultatas gali būti panašus į nurodytą vykdymo platformos mašinų instrukcijų rinkinį:

lea rax, [rdx + 7] ret

Skirtingi kompiliatoriai skirtingoms programoms

Skirtingos „Java“ programos turi skirtingus poreikius. Ilgai veikiančios įmonės serverio programos gali leisti daugiau optimizuoti, o mažesnėms kliento programos gali reikėti greitai vykdyti ir sunaudoti minimaliai išteklius. Panagrinėkime tris skirtingus kompiliatoriaus nustatymus ir jų privalumus bei trūkumus.

Kliento pusės kompiliatoriai

Gerai žinomas optimizavimo kompiliatorius yra C1 - kompiliatorius, įgalinamas per -klienui JVM paleidimo parinktis. Kaip rodo jo paleidimo pavadinimas, C1 yra kliento pusės kompiliatorius. Jis skirtas kliento programoms, turinčioms mažiau prieinamų išteklių ir daugeliu atvejų jautriai reaguojančių į programos paleidimo laiką. C1 naudoja našumo skaitiklius koduojant profilius, kad būtų galima atlikti paprastą, palyginti neįkyrų optimizavimą.

Serverio pusės kompiliatoriai

Ilgai veikiančioms programoms, tokioms kaip serverio įmonės įmonės „Java“ programos, kliento pusės kompiliatoriaus gali nepakakti. Vietoj to galėtų būti naudojamas serverio pusės kompiliatorius, pvz., C2. C2 paprastai įgalinamas pridedant JVM paleidimo parinktį - serveris į paleisties komandų eilutę. Kadangi tikimasi, kad dauguma serverio programų bus vykdomos ilgą laiką, įgalinus „C2“, galėsite surinkti daugiau profiliavimo duomenų nei tai padarytumėte naudodami trumpai veikiančią lengvą kliento programą. Taigi galėsite pritaikyti pažangesnes optimizavimo technikas ir algoritmus.

Patarimas: sušildykite serverio kompiliatorių

Diegiant serverio pusėje, gali prireikti šiek tiek laiko, kol kompiliatorius optimizuos pradines „karštas“ kodo dalis, todėl serverio pusės diegimui dažnai reikia „apšilimo“ fazės. Prieš atlikdami bet kokį našumo matavimą serverio pusėje, įsitikinkite, kad jūsų programa pasiekė pastovią būseną! Jei sudarysite pakankamai laiko kompiliatoriui tinkamai sukompiliuoti, tai bus naudinga jums! (Norėdami sužinoti daugiau apie kompiliatoriaus sušilimą ir profiliavimo mechaniką, žr. „JavaWorld“ straipsnį „Stebėkite, kaip vyksta„ HotSpot “kompiliatorius.)

Serverio kompiliatorius sudaro daugiau profilių duomenų nei kliento kompiliatorius, ir leidžia atlikti sudėtingesnę šakos analizę, o tai reiškia, kad jis apsvarstys, kuris optimizavimo kelias būtų naudingesnis. Turint daugiau turimų profiliavimo duomenų, gaunami geresni taikymo rezultatai. Žinoma, norint atlikti platesnį profiliavimą ir analizę, kompiliatoriui reikia išleisti daugiau išteklių. JVM su įgalintu C2 naudos daugiau gijų ir daugiau procesoriaus ciklų, reikės didesnės kodo talpyklos ir pan.

Pakopinis kompiliavimas

Pakopinis kompiliavimas sujungia kliento ir serverio kompiliaciją. „Azul“ pirmą kartą padarė pakopinį rinkinį savo „Zing JVM“. Visai neseniai (nuo „Java SE 7“) jį priėmė „Oracle Java Hotspot JVM“. Pakopinis kompiliavimas naudoja jūsų kliento ir serverio kompiliatoriaus pranašumus jūsų JVM. Kliento kompiliatorius yra aktyviausias paleidžiant programą ir tvarko optimizavimą, kurį sukelia žemesnės našumo skaitiklio ribos. Kliento pusės kompiliatorius taip pat įterpia našumo skaitiklius ir parengia instrukcijų rinkinius pažangesnėms optimizacijoms, kurias vėliau serverio kompiliatorius spręs. Pakopinis kompiliavimas yra labai efektyvus išteklių formavimo būdas, nes kompiliatorius sugeba surinkti duomenis mažo poveikio kompiliatoriaus veiklos metu, kurį vėliau galima naudoti pažangesniam optimizavimui. Šis metodas taip pat suteikia daugiau informacijos, nei gausite naudodamiesi vien interpretuotų kodų profilių skaitikliais.

1 paveiksle pateiktoje diagramos schemoje pavaizduoti grynojo interpretavimo, kliento, serverio ir pakopinio kompiliavimo našumo skirtumai. X ašyje rodomas vykdymo laikas (laiko vienetas) ir Y ašies veikimas (ops / laiko vienetas).

1 pav. Kompiliatorių našumo skirtumai (spustelėkite, jei norite padidinti)

Palyginti su grynai interpretuotu kodu, naudojant kliento kompiliatorių, apytiksliai 5–10 kartų geresnis vykdymo našumas (op / s), taip pagerinant programos našumą. Pelno kitimas, žinoma, priklauso nuo to, koks yra kompiliatoriaus efektyvumas, kokie optimizavimai įgalinti ar įgyvendinami, ir (kiek mažiau) nuo to, kaip gerai sukurta programa, atsižvelgiant į tikslinę vykdymo platformą. Tačiau pastaruoju atveju „Java“ kūrėjui niekada nereikėtų jaudintis.

Palyginti su kliento kompiliatoriumi, serverio kompiliatorius paprastai padidina kodo našumą išmatuojamais 30–50 procentais. Daugeliu atvejų, pagerinus našumą, bus subalansuotos papildomos išteklių sąnaudos.