Do hlubin implementací JavaScriptu: 2. díl – dynamičnost a výkon

Ve druhém dílu seriálu o implementacích JavaScriptu se podíváme na to, proč není snadné JavaScript rychle interpretovat, a které vlastnosti tohoto jazyka mají největší negativní vliv na jeho výkon. Zaměříme se přitom na problémy způsobené jeho dynamičností.

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

Výkon interpretu JavaScriptu nejvíce ovlivňuje fakt, že se jedná o dynamický jazyk. Většina problémů, se kterými se interpret musí potýkat, není ale pro JavaScript nikterak specifická a interprety ostatních dynamických jazyků musí podobné problémy řešit také. Právě na tyto obecnější problémy se dnes zaměříme.

Před samotným ponořením do problematiky je dobré si ujasnit, co je vlastně myšleno pojmem dynamický jazyk? V praxi se tento termín totiž používá poměrně vágně a označují se jím různé věci. V kontextu této série článků jím budeme mít na mysli především dynamické typování a možnost manipulace se základními elementy jazyka za běhu programu (v případě JavaScriptu jsou těmito elementy objekty, v jiných jazycích sem často spadají ještě třídy). Obojí bude podrobněji rozebráno dále v článku.

Pojďme se tedy podívat, které zajímavé problémy ovlivňující výkon musí každý javascriptový interpret řešit.

Volání metod

Rychlé volání metod je jeden z klasických úkolů interpretů dynamických jazyků. Abychom pochopili, proč zde vzniká problém, je třeba porozumět tomu, jak je volání metod obvykle implementováno ve statických jazycích.

Statické jazyky

U statických jazyků jako je třeba C++, Java nebo C# je seznam metod třídy pevně určen před samotným spuštěním programu (při kompilaci) a obvykle jsou k dispozici informace o typech proměnných. Díky tomu může kompilátor volání metod v kódu poměrně dobře optimalizovat.

Pokud volaná metoda není virtuální (tj. podtřídy nemůžou změnit její definici), je v místě volání jisté, který kus kódu se bude volat, a volání metody se díky tomu může zkompilovat jako instrukce skoku na její adresu (na místo v paměti, kde začíná její kód). Volání metody v tomto případě není nijak odlišné od volání obyčejné funkce a je velmi rychlé.

Pokud je volaná metoda virtuální (tj. podtřídy můžou změnit její definici), je situace o trochu složitější. U volání takové metody totiž před spuštěním programu nemusíme vědět jistě, kterou z jejích definic v kódu vlastně budeme volat. Obvyklým řešením je tabulka virtuálních metod – každá třída si s sebou nese informaci o tom, které virtuální metody obsahuje a jaké jsou jejich adresy v paměti. Tabulky potomků jsou pak uspořádány tak, aby v nich adresy stejných metod byly vždy na tom samém místě jako u jejich předků (pomíjíme teď případ mnohonásobné dědičnosti, kde je situace poněkud složitější a nebudeme ji zde rozebírat). Místo názvu metody tedy můžeme uvažovat jen její číselný index v tabulce. Ten je známý už při kompilaci.

Důsledkem je, že v místě volání virtuální metody stačí vygenerovat kód, který zjistí třídu objektu, podívá se do tabulky metod této třídy na pevně dané místo (protože zná číselný index metody) a skočí na adresu, kterou v tabulce na tomto místě najde. Vygenerovaný kód se obvykle vejde do několika málo strojových instrukcí a je tak poměrně rychlý (ale o něco pomalejší než u nevirtuálních funkcí).

Uvedený postup se dá v různých případech ještě dále optimalizovat, ale to pro nás není podstatné.

Dynamické jazyky

U dynamických jazyků není seznam metod, které je možné na objektu volat, pevně určen před spuštěním programu, ale může být za běhu programu změněn (metody je obvykle možné přidávat, ubírat či předefinovávat) a také nemáme informace o typech proměnných. To vylučuje optimalizace popsané výše. Při volání metody je tedy třeba ji nalézt podle jejího názvu a také je třeba řešit případ, kdy metoda neexistuje.

Ve většině dynamických jazyků, které znají koncept třídy, se hledání metod řeší tak, že každá třída má seznam svých metod uložený jako hash tabulku (často též nazývanou slovník či asociativní pole). To umožňuje zjistit, zda třída definuje danou metodu, a najít její adresu v konstantním čase (doba hledání tedy nezávisí na počtu metod ve třídě).

Při samotném volání metody se pak nejdříve prohledá tabulka metod třídy příslušného objektu, pak tabulka její nadtřídy atd. Hledání se zastaví ve chvíli, kdy se metoda v některé tabulce najde (pak se zavolá), nebo když se dojde k nejvyšší třídě v hierarchii (pak metoda není nalezena a další postup závisí na konkrétním jazyce – např. se vyvolá chyba).

V JavaScriptu je situace mírně odlišná, protože zde nemáme třídy. Každý objekt obsahuje jen jednu tabulku obsahující dohromady jeho metody i atributy (vlastnosti, chcete-li) a dále odkaz na objekt, který je jeho prototypem. Při hledání metody se pak místo po nadtřídách postupuje po odkazech na prototypy a navíc je vždy třeba kontrolovat, zda je položka nalezená v tabulce objektu metoda nebo jen obyčejný atribut (ten nejde zavolat). Z pohledu náročnosti hledání se ale postup v zásadě neliší od jazyků, které znají třídy.

Je zřejmé, že volání metod, které je v programech poměrně častou operací, je v dynamických jazycích podstatně složitější než v jazycích statických. Je to také proto jedna z nejčastěji optimalizovaných oblastí. Způsoby, jak volání metod urychlit (což znamená především vyhnout se náročnému prohledávání několika hash tabulek) si představíme v některém z dílů, kde se budeme zabývat konkrétními interprety. Jde především o keše metod a koncept skrytých tříd použitý v interpretu V8.

Přístup k atributům objektů

Statické jazyky

U statických jazyků je při definici třídy pevně určeno, jaké atributy obsahuje. Kompilátor tak například může disponovat informací, že ve třídě Point uvedené níže je adresa atributu x stejná, jako adresa instance této třídy a adresa atributu y je o 4 bajty vyšší:

class Point {
  int x;
  int y;
} 

Při přístupu k vlastnosti nějakého objektu pak stačí přičíst nějakou pevnou konstantu k jeho adrese, což je blesková operace.

Dynamické jazyky

Naproti tomu v dynamických jazycích je typické, že atributy objektů jsou před spuštěním programu určeny jen zčásti nebo vůbec a za běhu programu je možné je měnit. Nemůžou tedy mít pevnou pozici v rámci objektu a podobně jako u metod je nutné je ukládat do hash tabulky. A i zde je hledání v tabulce pomalé (u metod ale hledání vadí obecně více, protože je často nutné prohledávat tabulek víc).

V JavaScriptu nám přístup k metodám a atributům objektů splývá, protože obojí je uloženo v jedné tabulce a pro hledání platí stejná pravidla. Obě věci jsou tak obvykle optimalizovány stejnými technikami.

Dynamické typování

Dynamické typování znamená, že v programu nejsou uvedeny informace o datových typech proměnných. Proměnné mají za běhu stejný typ jako hodnoty, které jsou do nich vloženy, přičemž je možné do nich postupně ukládat hodnoty různých typů (typ proměnných se tedy může v čase měnit). Naproti tomu v jazycích, které jsou staticky typované, musí být u každé proměnné typ explicitně uveden. Jsou možné i různé hybridní přístupy (viz třeba typ Variant používaný v COM).

V předchozím odstavci jsme mluvili jen o proměnných, ale to samé platí i pro parametry funkcí, jejich návratové hodnoty, atributy objektů apod. Zkrátka pro všechna místa v programu, kde se uchovávají či předávají hodnoty.

Informace o datových typech ve staticky typovaných jazycích nám poskytuje příležitost k optimalizacím. Jako příklad si můžeme vzít následující jednoduchou funkci (dle osobních preferencí ji můžete považovat za funkci v C, C++, Javě, C# a pravděpodobně mnoha dalších jazycích):

int add(int a, int b) {
  return a + b;
} 

Při generování kódu této funkce můžeme vzít v úvahu, že její parametry i návratová hodnota jsou vždy celá čísla a vygenerovaný kód tak může být přímo instrukce číselného součtu cílového procesoru či virtuálního stroje.

Naproti tomu v dynamicky typovaném jazyce informaci o typech nemáme. Ekvivalentní funkce v JavaScriptu vypadá takto:

function add(a, b) {
  return a + b;
} 

O parametrech funkce zde nemůžeme říct vůbec nic – volající může funkci předat dvě čísla (pak je korektním výsledkem funkce jejich součet), ale také dva řetězce (pak je korektním výsledkem spojení – konkatenace – těchto řetězců) nebo úplně něco jiného. Vygenerovaný kód funkce musí tedy zvládnout mnoho různých případů a musí být podstatně složitější než instrukce celočíselného součtu jako v případě staticky typovaných jazyků. Důsledkem toho je samozřejmě prodloužení doby běhu funkce.

Je zřejmé, že k situacím podobným naší ukázkové funkci dochází v běžných programech na každém kroku a dynamické typování zpomaluje vykonávání kódu. To často vede k přesvědčení programátorů, že dynamické jazyky jsou oproti statickým jazykům nevyhnutelně pomalé. Do jaké míry je toto tvrzení pravdivé a jak se dá absence informací o typech vhodnými optimalizacemi obejít, si ukážeme v některém z následujících dí­lů.

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

Příště se podíváme na další problematická místa z pohledu výkonu (mimo jiné i na funkci eval) a ukážeme si i některé věci specifické pro JavaScript. Slibovanému přehledu implementací JavaScriptu se budeme věnovat později, až probereme obecné problémy interpretace.

Jsou dynamické jazyky oproti statickým jazykům nevyhnutelně pomalé?

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: 11

Přehled komentářů

DevelX properties objektu v dynamických jazykoch
David Majda Re: properties objektu v dynamických jazykoch
Vuk Re: properties objektu v dynamických jazykoch
David Majda Re: properties objektu v dynamických jazykoch
yossarian C#?
David Majda Re: C#?
Anonymní Re: C#?
ladipro Re: C#?
David Majda Re: C#?
Ladislav Thon Re: C#?
ladipro Re: C#?
Zdroj: https://www.zdrojak.cz/?p=2854