Prehliadka V8: plný kompilátor

link: http://jayconrod.com/posts/51/a-tour-of-v8-full-compiler

Za posledných päť rokov, JavaScript výkonnosť zvýšila neuveriteľne rýchlo, a to najmä kvôli prechodu z výkladu, aby JIT kompilácie v JavaScript virtuálne stroje. Tento sa výrazne zvýšili užitočnosť JavaScript a webové aplikácie vo všeobecnosti. Ako výsledok, JavaScript je v súčasnosti hybnou silou HTML5, ďalšia vlna webových technológií. Jedným z prvých JavaScript motory vytvárať a realizovať natívny kód bol V8, ktorý sa používa v prehliadači Google Chrome, prehliadač systému Android, WebOS, a iné projekty, napríklad Node.js.

Trochu viac ako pred rokom, som sa pripojil k tímu v mojej spoločnosti, ktorá optimalizuje V8 pre naše vlastné RAMENO microarchitecture. Od tej doby, čo som videl SunSpider výkon dvojité, a V8 benchmark zvýšenie výkonu o 50% z dôvodu príspevkov z oboch hardvéru a softvéru.

V8 je naozaj zaujímavý projekt pracovať, ale bohužiaľ, dokumentácia pre to je trochu málo. V najbližších niekoľkých článkov, ktoré som vám poskytne vysokú úroveň prehľad, ktorý bude dúfajme, že byť zaujímavé pre každého, kto zvedavý vnútornej virtuálnych strojov alebo kompilátory.

Vysokej úrovni architektúry

V8 zhromažďuje všetky JavaScript natívny kód, pred vykonaním. Nie je výklad a č bytecode. Kompilácia sa vykonáva jednu funkciu v čase (na rozdiel od stopových založené na zostavenie, ako sa používa v TraceMonkey, staré FireFox VM). Zvyčajne, funkcie, nie sú vlastne zostavené do prvého času oni sú povolaní, takže ak sa vám patrí veľká knižnica skript, VM nebude strácať čas zostavenia nevyužitých častí.

V8 skutočne používa dva rôzne JavaScript kompilátory. Som rád, že ich ako jednoduchý prekladač a pomocné kompilátor. Celý kompilátor (čo je predmetom tohto článku) je unoptimizing kompilátor. Jeho úlohou je produkovať natívny kód, ako rýchlo, ako je to možné, čo je dôležité pre udržanie časy načítania stránok trefný. Kľukového hriadeľa je optimalizácia kompilátor. V8 zhromažďuje všetko, čo sa najskôr s full kompilátor, potom používa zabudovaný profiler vyberte “horúce” funkcií, ktoré majú byť optimalizované kľukového hriadeľa. Od V8 je väčšinou singel-závitové (ako verzia 3.14), výkon je pozastavené, kým buď kompilátor beží. Z toho vyplýva, obe zostavovatelia sú určené na výrobu kód rýchlo namiesto toho, aby trávil veľa času vyrábajú veľmi efektívne kód. V budúcich verziách V8, kľukového hriadeľa (alebo aspoň ich častí) bude bežať v samostatnom vlákne, súbežne s jazyka JavaScript, umožňujúce drahšie optimalizácia.

Prečo č bytecode?

Väčšina VMs obsahovať bytecode interpreter, ale je to predovšetkým chýba V8. Možno sa budete čudovať, prečo celý kompilátor existuje, keď môže byť jednoduchšie zostaviť do bytecode a spustenie. Ukazuje sa, že zostavovaní na unoptimized natívny kód, nie je v skutočnosti oveľa drahšia ako zostavovaní na bytecode. Zvážte, zostavenie procesov pre oboch:

Bytecode zostavovanie:

  • Syntaktická analýza (analýza)
  • Rozsahu analýzy
  • Prekladať syntax strom bytecode

Natívne zostavenie:

  • Syntaktická analýza (analýza)
  • Rozsahu analýzy
  • Prekladať syntax strom rodák

V oboch prípadoch musíme analyzovať zdrojový kód a vytvárať abstraktný syntaktický strom (AST). Musíme vykonať rozsah analýzy, ktorý nám hovorí, či každý symbol sa vzťahuje na lokálnej premennej, súvislosti premennej (používané uzávery), alebo globálne majetku. Preklad krokom je iba časť, ktorá je iný. Môžete to urobiť veľmi komplikované veci tu, ale ak chcete, kompilátor sa tak rýchlo, ako je to možné, v podstate musíte urobiť, priamy preklad: každý syntaktický strom uzol by sa prekladal rovnakej postupnosti bytecodes alebo natívnu pokyny.

Teraz porozmýšľajte, ako by ste napísať tlmočníka pre bytecode. A na�ve realizácia by byť slučku, ktorá získa ďalší bytecode, vstupuje veľký prepnutie výpoveď, a spustí pevné postupnosť inštrukcií. Existujú rôzne spôsoby, ako zlepšiť na tejto, ale všetci sa scvrkne na rovnakú štruktúru.

Namiesto generovania bytecode a pomocou tlmočníka slučky, čo ak sme len emitované vhodné pevné postupnosť príkazov pre každú operáciu? Ako sa to stane, je to presne tak, ako celý prekladač pracuje. Týmto sa odstráni potreba tlmočníka a zjednodušuje prechody medzi unoptimized a optimalizovaný kód.

Vo všeobecnosti, bytecode je užitočné v situáciách, v ktorých si môžete urobiť nejaké kompilátor práce v predstihu. Toto nie je prípad vo vnútri webový prehliadač, tak naplno kompilátor dáva väčší zmysel pre V8.

Inline cache: zrýchlenie unoptimized kód

Ak sa pozriete na ECMAScript spec, zistíte, že väčšina operácií sú absurdly zložité. Vezmite + operátora, napríklad. Ak oboch operandov sú čísla, vykonáva navyše. Ak jeden operand je reťazec, vykonáva string zreťazenie. Ak operandov sú niečo iné ako čísla alebo reťazce, niektoré komplikované (prípadne používateľom definované) konverzia na primitívne vyskytuje pred buď okrem číslo alebo reťazec zreťazenie. Len pri pohľade na zdrojovom kóde, nie je tam žiadny spôsob, ako zistiť to, čo pokyny by mali byť vysielané. Majetku zaťaženie (príklad: o.x) sú dobrým príkladom iného potenciálne zložité operácie. Z pohľadu kód, nemôžete povedať, či ste nakladanie normálne vlastníctva v rámci predmetu (“vlastné” majetok), vlastnosť prototyp objektu, getter metóda, alebo niektoré magic prehliadač definované spät. hovor. Nehnuteľnosť ani nemusí existovať. Ak by ste mali zvládnuť všetky možné prípady, v plnom rozsahu-skompilovaný kód, aj táto jednoduchá obsluha by si vyžiadalo stovky pokyny.

Inline cache (ICs) poskytujú elegantné riešenie tohto problému. Inline cache je v podstate funkciu s viacerými možných implementácií (zvyčajne generovaný on the fly), ktoré možno nazvať zvládnuť konkrétnu prevádzku. Predtým som písal o polymorfné inline cache pre funkcie volania. V8 používa ICs pre oveľa širší súbor operácií: plný používa kompilátor ICs realizovať načíta, ukladá, hovory, binárne, unárne a porovnanie operátorov, rovnako ako ToBoolean implicitné prevádzky.

Vykonávanie IC sa nazýva čapu. Výhonky správať ako funkcie v tom zmysle, že budete volať ich, a oni sa vrátia, ale nemusia nutne nastavenie zásobníka rám a postupujte podľa plný volania dohovoru. Výhonky sú zvyčajne vytvorené na muchu, ale pre bežné prípady, že môže byť uložený do vyrovnávacej pamäte a znovu použiť viacero ICs. Čapu, ktorý implementuje IC zvyčajne obsahuje optimalizovaný kód, ktorý sa zaoberá typy operandov, že konkrétny IC narazila v minulosti (čo je dôvod, prečo sa to volá cache). Ak čapu stretne prípade to nie je pripravený zvládnuť, “minie” a vyzýva C++ runtime kód. Runtime rukoväte prípad, potom vygeneruje nový výhonok, ktorý zvládne neprijaté prípade (ako aj v predchádzajúcich prípadoch). Hovor na staré čapu v plnej skompilovaný kód je prepísané volať nový výhonok, a realizácia pokračuje, ako keby čapu bol nazývaný normálne.

Zoberme si jednoduchý príklad: vlastníctva zaťaženie.

function f(o) {
  return o.x;
}

Pri full kompilátor prvý generuje kód pre túto funkciu, bude používať IC pre náklad. IC začína v uninitialized štátu, pomocou triviálne čapu, ktorý nedokáže zvládnuť všetky prípady. Tu je postup, ako celý skompilovaný kód hovory čapu.

  ;; full compiled call site
  ldr   r0, [fp, #+8]     ; load parameter "o" from stack
  ldr   r2, [pc, #+84]    ; load string "x" from constant pool
  ldr   ip, [pc, #+84]    ; load uninitialized stub from constant pool
  blx   ip                ; call the stub
  ...
  dd    0xabcdef01        ; address of stub loaded above
                          ; this gets replaced when the stub misses

(Ospravedlňujem sa, ak nie ste oboznámení s RAMENOM montáž. Dúfajme, že komentáre, aby bolo jasné, čo sa deje.)

Tu je kód pre uninitialized čapu:

  ;; uninitialized stub
  ldr   ip,  [pc, #8]   ; load address of C++ runtime "miss" function
  bx    ip              ; tail call it
  ...

Prvýkrát tento čapu sa nazýva, bude to “miss”, a runtime bude generovať kód na spracovanie bez ohľadu prípade skutočne spôsobené miss. V V8, najbežnejší spôsob, ako ukladať nehnuteľnosť je na pevné posun v rámci objektu, tak si to pozri príklad. Každý objekt má ukazovateľ mapa, ktoré je väčšinou nemenné štruktúry, ktorá popisuje štruktúru objektu. V objekte zaťaženie čapu bude skontrolujte objekt je mapa proti známa mapa (jeden vidieť, keď uninitialized čapu neprijaté) rýchlo overiť objekt má požadovanú vlastnosť v správnej polohe. Táto mapa nechajte nám umožňuje vyhnúť drahé hash tabuľke vyhľadávania.

  ;; monomorphic in-object load stub
  tst   r0,   #1          ; test if receiver is an object
  beq   miss              ; miss if not
  ldr   r1,   [r0, #-1]   ; load object's map
  ldr   ip,   [pc, #+24]  ; load expected map
  cmp   r1,   ip          ; are they the same?
  bne   miss              ; miss if not
  ldr   r0,   [r0, #+11]  ; load the property
  bx    lr                ; return
miss:
  ldr   ip,   [pc, #+8]   ; load code to call the C++ runtime
  bx    ip                ; tail call it
  ...

Tak dlho, ako tento výraz, len sa musí vysporiadať s v-objekt, vlastnosť zaťaženie, zaťaženie môže byť vykonaná rýchlo bez dodatočné generovanie kódu. Od IC môže spracovať len jeden prípad, je to v monomorphic štátu. Ak inom prípade príde, a IC minie znova, megamorphic čap budú generované ktorý je všeobecnejší.

Pokračovanie…

Ako môžete vidieť, celý kompilátor splní svoj cieľ rýchlo generovať dostatočne dobre vykonanie východiskovej kód. Od ICs sú hojne používaný, plne skompilovaný kód je veľmi všeobecný, ktorý je plný kompilátor veľmi jednoduché. ICs, aby kód, veľmi flexibilný, schopný zvládnuť akýkoľvek prípad.

V ďalšom článku sa pozrieme na to, ako V8 predstavuje objektov v pamäti, umožňujúce O(1) prístup vo väčšine prípadov bez akejkoľvek štrukturálnej špecifikácia od programátora (ako napríklad class prehlásenie).