Přejít k navigační liště

Zdroják » JavaScript » Do hlubin implementací JavaScriptu: 2. díl – dynamičnost a výkon

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

Články JavaScript, Různé

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í.

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é?

Komentáře

Subscribe
Upozornit na
guest
11 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
DevelX

"u metod ale hledání vadí obecně více, protože je často nutné prohledávat tabulek víc"

Naozaj? Vrámci objektu je možné pristupovať aj k vlastnostiam, ktoré sú súčasťou objektov prototype – a to s použitím kľúčového slova this. Situácia sa mení keď sa použije priradenie cez this a táto nová hodnota sa uloží do daného objektu a nie do niektorého nadradeného prototypu.

Vuk

Ale ani tak to přece neplatí. V Pythonu například (je to "bežný" dynamický jazyk?) napíšu-li `a.i', může jít o atribut instance a, nebo o atribut její třídy, nebo o atribut nadtřídy, …

yossarian

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)

v tom pripade c# neni staticky jazyk, neb pomoci reflection.emit a extensions umi pridavat/menit uz stavajici zkompilovane metody, klidne i za behu.

Anonymní

> O Reflection.Emit prakticky nic nevím, takže je možné, že nějakým způsobem statičnost jazyka narušuje.

Narušuje, nenarušuje, v .NETu (a v Javě taky) prostě lze generovat kód za běhu aplikace, taky ho hned zavádět a vykonávat (na tomhle principu např. funguje obvyklý způsob integrace skriptů v Groovy do programů v Javě). Jak moc to mění nebo nemění "statičnost" jazyka je otázka interpretace, každopádně to má docela zajímavý vliv na optimalizace prováděné virtuálním strojem :-)

ladipro

Ref.Emit neumi manipulovat s existujicimi typy, takze na staticnosti nic nemeni. Stale plati, ze ve chvili, kdy se JITtuje metoda, je k dispozici plna typova informace. Podobne jako na staticnosti C nic nemeni to, ze si muzu do kusu pameti nagenerovat instrukce a pres function pointer je zavolat.

Ani feature nazyvana "Edit & Continue", ktera umoznuje delat jiste zmeny behem debuggovani bez nutnosti restartovat program, nepovoli nic nebezpecneho jako napr. pridani fieldu do typu.

Ladislav Thon

> Stale plati, ze ve chvili, kdy se JITtuje metoda, je k dispozici plna typova informace.

Záleží na tom, co myslíte "plnou typovou informací" :-) JIT například z principu nemůže znát úplnou hierarchii tříd. Se "statičností" jazyka jako takového to nemá nic moc společného, ale runtime musí být mnohem dynamičtější.

ladipro

JIT ma vsechny informace o tride, jejiz metodu JITtuje jakozto i vsech jejich base tridach. Autor v clanku naznacil, ze virtual dispatch je slaby odvarek a rozhodne nedela jazyk dynamickym – jestli to je to, na co narazite. Nicmene, pokud se napriklad pokusite udelat call na neexistujici metodu, dostanete vyjimku pri JITovani, zadny by-name resolution za run-time se konat nebude.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.