Nesnáším srovnávací kód, stejně jako každý člověk (což v tuto chvíli většina diváků tohoto pravděpodobně není ¯\ (ツ) /¯). Je mnohem zábavnější předstírat, že vaše ukládání hodnoty do mezipaměti zvýšilo výkon o 1000 %, než testovat, co to udělalo. Bohužel, benchmarking v JavaScriptu je stále nezbytný, zvláště když se JavaScript používá ( když by neměl? ) v aplikacích citlivějších na výkon. Bohužel, kvůli mnoha svým klíčovým architektonickým rozhodnutím, JavaScript neusnadňuje benchmarking.
Pro ty, kteří neznají kouzlo moderních skriptovacích jazyků, jako je JavaScript, může být jejich architektura docela složitá. Namísto pouhého spouštění kódu přes interpret, který okamžitě chrlí instrukce, využívá většina JavaScriptových enginů architekturu podobnější kompilovanému jazyku, jako je C – integrují více vrstev „kompilátorů“ .
Každý z těchto kompilátorů nabízí jiný kompromis mezi dobou kompilace a výkonem za běhu, takže uživatel nemusí strávit výpočetní optimalizaci kódu, který se spouští jen zřídka, a zároveň využít výhod pokročilejšího kompilátoru v oblasti výkonu pro kód, který je spouštěn nejčastěji ( „horké stezky“). Existují také některé další komplikace, které vznikají při používání optimalizačních kompilátorů, které zahrnují ozdobná programovací slova jako „ monomorfismus funkcí “, ale ušetřím vás a vyhnu se zde o tom mluvit.
Takže… proč je to důležité pro benchmarking? Jak jste možná uhodli, protože benchmarking měří výkon kódu, kompilátor JIT může mít docela velký vliv. Menší části kódu, když jsou testovány, mohou po úplné optimalizaci často zaznamenat 10x+ zlepšení výkonu, což do výsledků vnáší spoustu chyb.
Například ve vašem nejzákladnějším nastavení srovnávání (z několika důvodů nepoužívejte nic jako níže):
for (int i = 0; i<1000; i++) { console.time() // do some expensive work console.timeEnd() }
(Nebojte se, budeme mluvit také o console.time
)
Velká část vašeho kódu bude po několika pokusech uložena do mezipaměti, což výrazně zkrátí čas na operaci. Benchmarkové programy často dělají maximum pro to, aby toto ukládání do mezipaměti/optimalizace eliminovaly, protože také mohou programy testované později v procesu benchmarku vypadat relativně rychleji. Nakonec se však musíte zeptat, zda benchmarky bez optimalizací odpovídají výkonu v reálném světě.
Jistě, v určitých případech, jako jsou málo navštěvované webové stránky, je optimalizace nepravděpodobná, ale v prostředích, jako jsou servery, kde je nejdůležitější výkon, by se optimalizace měla očekávat. Pokud spouštíte kus kódu jako middleware pro tisíce požadavků za sekundu, raději doufejte, že ho V8 optimalizuje.
V zásadě tedy i v rámci jednoho enginu existují 2–4 různé způsoby, jak spustit kód s různou úrovní výkonu. Oh, také je v určitých případech neuvěřitelně obtížné zajistit, aby byly povoleny určité úrovně optimalizace. Bavte se :).
Znáte otisky prstů? Technika, která umožnila použití Do Not Track pro podporu sledování ? Ano, JavaScriptové enginy dělaly maximum, aby to zmírnily. Tato snaha spolu s krokem k zabránění útokům na časování vedla k tomu, že JavaScriptové motory záměrně znepřesnily načasování, takže hackeři nemohou získat přesná měření aktuálního výkonu počítačů nebo toho, jak drahá je určitá operace.
Bohužel to znamená, že bez ladění mají benchmarky stejný problém.
Příklad v předchozí části bude nepřesný, protože se měří pouze v milisekundách. Nyní to přepněte na performance.now()
. Velký.
Nyní máme časová razítka v mikrosekundách!
// Bad console.time(); // work console.timeEnd(); // Better? const t = performance.now(); // work console.log(performance.now() - t);
Kromě... všechny jsou v krocích po 100 μs. Nyní přidáme několik hlaviček , abychom zmírnili riziko útoků načasování. Jejda, stále můžeme zvyšovat pouze 5μs. 5μs je pravděpodobně dostatečná přesnost pro mnoho případů použití, ale budete muset hledat jinde, co vyžaduje větší granularitu. Pokud vím, žádný prohlížeč neumožňuje podrobnější časovače. Node.js ano, ale to má samozřejmě své vlastní problémy.
I když se rozhodnete spustit svůj kód přes prohlížeč a nechat kompilátor, aby udělal svou věc, je jasné, že budete mít stále více bolesti hlavy, pokud chcete přesné načasování. Jo, a ne všechny prohlížeče jsou si rovny.
Miluji Bun za to, co udělal, aby posunul JavaScript na straně serveru vpřed, ale sakra, dělá to srovnávání JavaScriptu pro servery mnohem těžší. Před několika lety byla jedinými prostředími JavaScriptu na straně serveru, o která se lidé zajímali, byly Node.js a Deno , které obě používaly V8 JavaScript engine (stejný v Chrome). Bun místo toho používá JavaScriptCore, engine v Safari, který má úplně jiné výkonnostní charakteristiky.
Tento problém více prostředí JavaScriptu s vlastními výkonnostními charakteristikami je v JavaScriptu na straně serveru relativně nový, ale klienty sužuje již dlouhou dobu. 3 různé běžně používané JavaScriptové enginy, V8, JSC a SpiderMonkey pro Chrome, Safari a Firefox, všechny mohou pracovat výrazně rychleji nebo pomaleji na ekvivalentním kusu kódu.
Jedním z příkladů těchto rozdílů je optimalizace Tail Call Optimization (TCO). TCO optimalizuje funkce, které se opakují na konci jejich těla, jako je tento:
function factorial(i, num = 1) { if (i == 1) return num; num *= i; i--; return factorial(i, num); }
Zkuste porovnat factorial(100000)
v Bun. Nyní zkuste to samé v Node.js nebo Deno. Měli byste dostat chybu podobnou této:
function factorial(i, num = 1) { ^ RangeError: Maximum call stack size exceeded
Ve V8 (a rozšířením Node.js a Deno) pokaždé, když se factorial()
na konci zavolá, engine vytvoří zcela nový kontext funkce pro spuštění vnořené funkce, která je nakonec omezena zásobníkem volání. Ale proč se to neděje v Bunu? JavaScriptCore, který Bun používá, implementuje TCO, které optimalizuje tyto typy funkcí tím, že je přemění na smyčku for podobně jako:
function factorial(i, num = 1) { while (i != 1) { num *= i; i--; } return i; }
Nejenže se výše uvedený návrh vyhýbá limitům zásobníku volání, ale je také mnohem rychlejší, protože nevyžaduje žádné nové kontexty funkcí, což znamená, že funkce jako výše budou testovány velmi odlišně pod různými motory.
Tyto rozdíly v podstatě znamenají, že byste měli porovnávat všechny motory, u kterých očekáváte spouštění kódu, abyste zajistili, že kód, který je rychlý v jednom, nebude pomalý v jiném. Pokud také vyvíjíte knihovnu, u které očekáváte použití na mnoha platformách, nezapomeňte zahrnout esoteričtější nástroje jako Hermes ; mají výrazně odlišné výkonové charakteristiky.
Přál bych si, abych mohl ukázat na balíček npm, který řeší všechny tyto problémy, ale ve skutečnosti žádný neexistuje.
Na serveru to máte o něco jednodušší. Pomocí d8 můžete ručně ovládat úrovně optimalizace, ovládat sběrač odpadu a získat přesné načasování. Samozřejmě k tomu budete potřebovat nějaké Bash-fu, abyste vytvořili dobře navržený benchmark pipeline, protože bohužel d8 není dobře integrován (nebo vůbec integrován) s Node.js.
Můžete také povolit určité příznaky v Node.js, abyste získali podobné výsledky, ale přijdete o funkce, jako je povolení konkrétních úrovní optimalizace.
v8 --sparkplug --always-sparkplug --no-opt [file]
Příklad D8 s povolenou specifickou kompilační vrstvou (zapalovací svíčka). D8 ve výchozím nastavení obsahuje větší kontrolu nad GC a více informací o ladění obecně.
Můžete získat nějaké podobné funkce na JavaScriptCore??? Upřímně řečeno, CLI JavaScriptCore jsem moc nepoužíval a je silně nedostatečně zdokumentováno. Můžete povolit konkrétní úrovně pomocí jejich příznaků příkazového řádku , ale nejsem si jistý, kolik informací o ladění můžete načíst. Bun také obsahuje některé užitečné nástroje pro srovnávání , ale jsou omezeny podobně jako Node.js.
Bohužel to vše vyžaduje základní motor/testovací verzi motoru, kterou může být docela těžké sehnat. Zjistil jsem, že nejjednodušší způsob, jak spravovat motory, je esvu spárovaný s eshost-cli , protože společně výrazně usnadňují správu motorů a spouštění kódu na nich. Samozřejmě je stále potřeba spousta ruční práce, protože tyto nástroje pouze spravují běh kódu napříč různými motory – srovnávací kód si stále musíte napsat sami.
Pokud se jen snažíte co nejpřesněji srovnávat motor s výchozími možnostmi na serveru, existují běžně dostupné nástroje Node.js, jako je mitata , které pomáhají zlepšit přesnost časování a chyby související s GC. Mnoho z těchto nástrojů, jako je Mitata, lze také použít v mnoha motorech; Samozřejmě, stále budete muset nastavit potrubí jako výše.
V prohlížeči je vše mnohem obtížnější. Neznám žádné řešení přesnějšího časování a ovládání motoru je daleko omezenější. Nejvíce informací, které můžete získat v souvislosti s výkonem JavaScriptu za běhu v prohlížeči, pocházejí z devtools Chrome , které nabízejí základní nástroje pro simulaci grafu plamene a zpomalení CPU.
Mnoho ze stejných rozhodnutí o návrhu, která učinila JavaScript (relativně) výkonným a přenosným, činí srovnávání výrazně obtížnějším, než je tomu v jiných jazycích. Existuje mnohem více cílů k porovnání a vy máte mnohem menší kontrolu nad každým cílem.
Doufejme, že řešení jednoho dne mnohé z těchto problémů zjednoduší. Možná bych nakonec vytvořil nástroj pro zjednodušení benchmarkingu mezi jednotlivými stroji a kompilací, ale prozatím vytvoření kanálu k vyřešení všech těchto problémů zabere docela dost práce. Samozřejmě je důležité si uvědomit, že tyto problémy se netýkají každého – pokud váš kód běží pouze v jednom prostředí, neztrácejte čas srovnáváním jiných prostředí.
Ať už se rozhodnete srovnávat jakkoli, doufám, že vám tento článek ukázal některé problémy, které se vyskytují při srovnávání JavaScriptu. Dejte mi vědět, jestli by vám pomohl návod na implementaci některých věcí, které popisuji výše.