V8: JavaScript uvnitř Google Chrome

Dnes nahlédneme pod pokličku V8 – interpretu JavaScriptu uvnitř Google Chrome. Podíváme se na tři jeho klíčové vlastnosti – kompilaci do nativního kódu, skryté třídy a garbage collector. Na závěr si ukážeme, jak V8 implementuje JavaScript z velké části v JavaScriptu samém.

Seriál: Do hlubin implementací JavaScriptu (14 dílů)

  1. Do hlubin implementací JavaScriptu: 1. díl – úvod 30.10.2008
  2. Do hlubin implementací JavaScriptu: 2. díl – dynamičnost a výkon 6.11.2008
  3. Do hlubin implementací JavaScriptu: 3. díl – výkonnostně nepříjemné konstrukce 13.11.2008
  4. Do hlubin implementací JavaScriptu: 4. díl – implementace v prohlížečích 20.11.2008
  5. Do hlubin implementací JavaScriptu: 5. díl – implementace mimo prohlížeče 27.11.2008
  6. SquirrelFish: reprezentace hodnot JavaScriptu a virtuální stroj 4.12.2008
  7. SquirrelFish: optimalizace vykonávání instrukcí a nativní kód 11.12.2008
  8. SquirrelFish: regulární výrazy, vlastnosti objektů a budoucnost 18.12.2008
  9. SpiderMonkey: zpracování JavaScriptu ve Firefoxu 8.1.2009
  10. SpiderMonkey: rychlá kompilace JavaScriptu do nativního kódu 15.1.2009
  11. V8: JavaScript uvnitř Google Chrome 22.1.2009
  12. Rhino: na rozhraní JavaScriptu a Javy 29.1.2009
  13. Velký test rychlosti JavaScriptu v prohlížečích 5.2.2009
  14. Javascriptové novinky: souboj o nejrychlejší engine pokračuje 19.3.2009

Motivace

Když Google před dvěma lety začínal vyvíjet svůj prohlížeč Chrome, rozhodl se v něm využít renderovací jádro WebKit (používané v prohlížeči Safari a vyvíjené firmou Apple). Bylo dobře napsané, efektivní a snadno upravitelné. Implementace JavaScriptu ve WebKitu se ale Googlu nelíbila, a nutno říci, že oprávněně – tehdejší engine JavaScriptCore (předchůdce dnešního SquirrelFish) byl relativně pomalý a neefektivní. Google se proto rozhodl napsat implementaci vlastní, a tak se zrodil V8.

Vývoj „osmiválce“ Google poněkud neobvykle svěřil své dánské pobočce. Vedoucím odpovědného týmu se stal Lars Bak. Ten není v oblasti virtuálních strojů žádným nováčkem, vedl mimo jiné vývoj známého a úspěšného javovského virtuálního stroje HotSpot. O virtuálních strojích také publikuje a spolu s kolegou z Googlu Kasperem Lundem o nich i přednáší na univerzitě v Arhusu.

Tři pilíře V8

Podle dokumentace Googlu stojí V8 po technické stránce na třech důležitých pilířích:

  • kompilace do nativního kódu
  • skryté třídy
  • inline keš a garbage collector

Pojďme se na ně podrobněji podívat.

Kompilace do nativního kódu

V8 na rozdíl od ostatních interpretů JavaScriptu v prohlížečích nereprezentuje skript bajtkódem a nemá žádný virtuální stroj. Kód skriptu je po parsování uložen v podobě AST (strom reprezentující jednotlivé konstrukce jazyka) a dle potřeby se kompiluje přímo do nativního kódu. Kompilátor je jednoprůchodový a v tuto chvíli se příliš nestará o optimalizaci vygenerovaného kódu. Zpracovává se vždy jen jedna funkce, a to v okamžiku jejího prvního zavolání. Kód, který nebude nikdy spuštěn, se tak vůbec nekompiluje, což šetří čas.

Absence virtuálního stroje omezuje použití V8 pouze na platformy, které podporuje generátor nativního kódu – v současnosti x86 a ARM (ta se používá na mobilních zařízeních). Oproti ostatním interpretům, které virtuální stroj mají, je to konkurenční nevýhoda. Na druhou stranu, pro webové prohlížeče jsou dnes podstatné pouze platformy, které již V8 podporuje, plus x86–64, jejíž podporu nebude pro Google těžké doplnit.

Skryté třídy a inline cache

Pro urychlení přístupu k vlastnostem objektů (a tedy i pro urychlení volání metod) používá V8 princip skrytých tříd a inline keše. Použitá technika je prakticky stejná jako u SquirrelFish a SpiderMonkey a již jsme si ji popisovali – nebudeme to zde tedy dělat znovu. Jen připomeňme, že její princip spočíval v průběžné evidenci informací o struktuře objektů v kódu skriptu a dynamické náhradě kódu přistupujícího k vlastnostem objektů za rychlejší na základě evidovaných dat.

Garbage collector

Garbage collectorům jednotlivých interpretů jsme se v seriálu vůbec nevěnovali – nebyly totiž nijak zajímavé. U garbage collectoru V8 se ale pozastavíme, protože je poměrně vyspělý. Konkrétněji je stop-the-world, kopírující, inkrementální a vícegenerační. Co jednotlivé pojmy znamenají?

Když se V8 rozhodne „uklidit“, zastaví se vykonávání programu (odtud „stop-the-world“), aby se nestalo, že se garbage collector a program budou navzájem rušit. Část paměti, kde leží alokované objekty JavaScriptu, se prohlásí za starou a připraví se oblast nová. Následně se začnou procházet všechny objekty ze staré oblasti, na které se dá z běžícího programu „dosáhnout“, a postupně se překopírují do nové oblasti (odtud „kopírující“). Ukazatele na ně se přitom zaktualizují. Když jsou všechny dostupné objekty zkopírovány, stará oblast je odalokována. Všechny objekty, které nebyly z běžícího skriptu dostupné, tak zaniknou. (Toto je ve skutečnosti jen jeden z možných průběhů garbage collection, ale podrobnější rozbor by zamlžil náš další výklad.)

Garbage collection

Při garbage collection se do nové oblasti kopírují jen dosažitelné objekty. Ukazatele v tabulce proměnných se zaktualizují.

Protože proces garbage collection dlouho trvá a program po tuto dobu neběží, vše se ve skutečnosti děje po částech (odtud „inkrementální“). Garbage collector tedy nezkoumá všechny objekty najednou, ale vždy jen určité omezené množství a zas na chvíli spustí program. To se opakuje, dokud není celý proces dokončen. Výsledkem je rozložení jedné dlouhé pauzy (která by mohla být pro uživatele prohlížeče nepříjemná) na několik menších (které uživatel ani nepostřehne). Cenou je pochopitelně zesložitění logiky, která musí počítat s tím, že se mezi jednotlivými fázemi například alokují nové objekty.

Většina objektů zaniká rychle po jejich vzniku (typicky lokální proměnné). Na druhou stranu, pokud už objekt existuje delší dobu, je pravděpodobné, že bude globálního charakteru a jen tak nezmizí. Tato dvě pozorování vedla k rozdělení objektů do generací (odtud „vícegenerační“). Každý objekt začne svůj život v mladé generaci a pokud přežije několik běhů garbage collection, je přesunut do starší generace. Garbage collector při běžném průchodu zkoumá jen objekty z mladé generace a na starší generaci se dívá pouze „jednou za čas“.

Výše popsané techniky nesou nijak převratné a používají se řadu let. U některých je to ale poprvé, co byly použity u interpretu JavaScriptu v prohlížeči, a často nejsou implementovány ani u interpretů jiných dynamických skriptovacích jazyků. Pravděpodobně je to dáno tím, že napsat dobrý garbage collector je poměrně těžké a zdaleka ne každý implementátor je ochoten vynaložit potřebné úsilí.

Garbage collector ve V8 jsme zde popsali jen stručně a neúplně. Detailnější popis najdete ve videu a slajdechGoogle Developer Day 2008 v Praze.

VÍCE K TÉMATU: Jak proběhl Google Developer Day 2008 v Praze

Reprezentace hodnot

V předcházejících dílech seriálu jsme si ukazovali, jak interprety uvnitř reprezentují hodnoty různých javascriptových typů (viz popis SquirrelFish a SpiderMonkey. Pro pořádek bychom to měli u V8 udělat také.

Všechny interprety reprezentují hodnoty složitějších typů (String, Object, částečně Number) ukazatelem na nějakou datovou strukturu obsahující potřebné údaje. Hodnoty jednoduchých typů (Undefined, Null, Boolean, částečně Number) se snaží pomocí „bitové chytristiky“ vměstnat přímo do ukazatele. To je možné díky tomu, že nízké bity ukazatelů nejsou na dnešních architekturách využívány a můžou být použity k zakódování dalších informací. Důsledkem je snížení počtu nutných dereferencí ukazatelů a tedy i zrychlení.

Reprezentace hodnot ve V8 je v principu podobná ostatním interpretům, je ale trochu jednodušší. V8 se přímo do ukazatelů snaží vměstnat jen část hodnot typu Number (31-bitová celá čísla), vše ostatní je odkaz na složitější strukturu. U Undefined, Null a Boolean ale existuje jen jedna či dvě instance daného typu (hodnoty undefined, null, true a false), takže ukazatele vždy vedou na stejná místa. Zjistit, zda daný ukazatel reprezentuje např. hodnotu true, je tak otázka jeho porovnání s předem známou hodnotou (adresou jedné ze dvou instancí typu Boolean). Není tedy třeba ukazatel dereferencovat (stejně jako u SquirrelFish a SpiderMonkey). Ač je tedy reprezentace hodnot ve V8 malinko odlišná od SpiderMonkey a SquirrelFish, efektivitou vyjde prakticky nastejno.

Reprezentace hodnot

Ukazatele v tabulce proměnných s typem Boolean ukazují všechny na tutéž instanci.

Runtime

Velice zajímavou součástí V8 je jeho runtime. Ten zahrnuje především implementaci standardních objektů a metod jazyka, jako třeba Array.sort nebo String.split.

JavaScript v JavaScriptu

Tvůrci V8 zvolili zajímavou taktiku – místo aby runtime psali v jazyce C nebo C++ (tak tomu je v ostatních interpretech), rozhodli se ho napsat v JavaScriptu samotném. Důvodem je větší pohodlí při programování (kdo raději programuje v C než v JavaScriptu, ať zvedne ruku…) a snadnost úprav. Rychlostní zpomalení oproti C/C++ není velké, protože kód runtime je stejně jako jakýkoliv jiný javascriptový kód kompilován do nativního kódu.

Přesto ale zní celá myšlenka dost nesmyslně – jak je možné psát součást JavaScriptu v JavaScriptu? Jak lze v JavaScriptu napsat třeba třídu String bez toho, aby už byla k dispozici? Jak se bude třeba alokovat paměť pro její instance, když na to JavaScript nemá jazykové prostředky? Není to celé problém typu slepice a vejce?

Odpověď na tyto otázky je jednoduchá – malinko jsem lhal. Většina runtime opravdu je napsaná v JavaScriptu, ale určité primitivní funkce jsou implementovány v C++. Tyto funkce poskytují především základní operace s datovými strukturami (jako jsou pole nebo řetězec) a javascriptový kód je může snadno volat. Kód runtime je tedy taková obálka nad těmito primitivními funkcemi, zapouzdřující je do objektově-prototypové podoby.

Pokud by vás zajímalo, jak kód runtime v JavaScriptu vypadá, můžete se podívat přímo do zdrojového kódu V8 a vyhledat si všechny javascriptové soubory (*.js) v adresáři /src. Poměrně dobrou představu o kódu vám dá například implementace operátoru + nebo funkce String.substring. Volání funkcí prefixované znakem % v souborech runtime jsou ve skutečnosti volání primitivních funkcí napsaných v C++. Ty jsou implementovány v souboru runtime.cc.

Předkompilace

Protože kompilace celého runtime do nativního kódu docela trvá a runtime se nijak nemění, je při kompilaci V8 předkompilován a je uložen jeho snapshot. Tento snapshot je pak při inicializaci V8 natažen a propojen s běhovým prostředím. Rychlost startu V8 se díky této optimalizaci podařilo snížit z cca 30 ms až na 4–8 ms (zdroj bohužel neuvádí, na jakém stroji).

Co nás čeká příště

Náš seriál se pomalu, ale jistě blíží k závěru – zbývají už jen dva díly. Ten příští budeme věnovat interpretu Rhino, který je zajímavý tím, že je napsán v Javě. Právě integrace mezi JavaScriptem a Javou bude to, co nás bude zajímat nejvíc.

Zdroje

Autor je vývojář se zájmem o programovací jazyky, webové aplikace a problémy programování jako takového. Vystudoval informatiku na MFF UK a během studií zde i trochu učil. Aktuálně pracuje v SUSE.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 47

Přehled komentářů

deda.jabko RE: V8: JavaScript uvnitř Google Chrome
alblaho RE: V8: JavaScript uvnitř Google Chrome
David Majda RE: V8: JavaScript uvnitř Google Chrome
beer RE: V8: JavaScript uvnitř Google Chrome
David Majda RE: V8: JavaScript uvnitř Google Chrome
David Majda RE: V8: JavaScript uvnitř Google Chrome
ZdenekJi RE: V8: JavaScript uvnitř Google Chrome
Keff RE: V8: JavaScript uvnitř Google Chrome
ZdenekJi RE: V8: JavaScript uvnitř Google Chrome
deda.jabko RE: V8: JavaScript uvnitř Google Chrome
Ladislav Thon RE: V8: JavaScript uvnitř Google Chrome
deda.jabko RE: V8: JavaScript uvnitř Google Chrome
Ladislav Thon RE: V8: JavaScript uvnitř Google Chrome
deda.jabko RE: V8: JavaScript uvnitř Google Chrome
Ladislav Thon RE: V8: JavaScript uvnitř Google Chrome
MyOwnClone Super clanek
Anonym Re: Super clanek
David Majda Re: Super clanek
Brut4r Garbage collector
David Majda Re: Garbage collector
Roger Re: Garbage collector
David Majda Re: Garbage collector
P Re: Garbage collector
Brut4r Re: Garbage collector
Ladislav Thon Re: Garbage collector
Anonym Re: Garbage collector
Ladislav Thon Re: Garbage collector
David Majda Re: Garbage collector
P Reprezentace hodnot
David Majda Re: Reprezentace hodnot
P Re: objekty
P Re: objekty
Ladislav Thon Re: objekty
P Re: objekty
Standa Re: objekty
Ladislav Thon Re: objekty
P Re: objekty
Ladislav Thon Re: objekty
P Re: objekty
David Majda Re: objekty
P Re: objekty
Anonym tiscript
maertien(notloggedin) C vs Javascript
mol Re: C vs Javascript
jard Re: C vs Javascript
Jarek JavaScript uvnitř Google Chrome.
Martin Hassman Re: JavaScript uvnitř Google Chrome.
Zdroj: https://www.zdrojak.cz/?p=2919