iX 11/2018
S. 130
Praxis
Compiler
Aufmacherbild

JavaScript optimieren mit TurboFan

Zur rechten Zeit

Wer JavaScript-Code schreiben möchte, den der JIT-Compiler gut optimieren kann, sollte mit der internen Repräsentation von Datenstrukturen und Algorithmen vertraut sein.

Any application that can be written in JavaScript, will eventually be written in JavaScript.“ Dieses von Jeff Atwood, dem Gründer von StackOverflow, postulierte Atwood’s Law (siehe ix.de/ix1811130) verdeutlicht auf nicht ganz ernst gemeinte, aber treffende Weise die heutigen Ansprüche an die Ausführung von JavaScript: Unabhängig von Architektur und Anwendungsgebiet soll sie performant erfolgen. Das Mittel der Wahl dazu ist in allen heutigen JavaScript-Engines der Einsatz eines JIT-Compilers (Just-in-Time).

Die folgenden Erläuterungen beziehen sich auf Googles V8-Engine, die die Grundlage sowohl für die Chromium-Familie der Browser als auch für die Node.js-Plattform zur serverseitigen Ausführung von JS ist. Bemerkenswert ist, dass die Grundstruktur aus mehrstufigem JIT mit dem Verwerten von Profiling-Daten (siehe unten) in allen „großen“ Engines – namentlich SpiderMonkey (Mozilla/Firefox), ChakraCore (Microsoft/Edge) und JavaScriptCore (Apple/Safari beziehungsweise WebKit) – von der Architektur her sehr ähnlich ist. Mit TurboFan verfügt V8 darüber hinaus seit Mai 2017 über einen von Grund auf neu entwickelten optimierenden JIT-Compiler, der hier im Fokus stehen soll.

Sinn und Unsinn von Just-in-Time

Vor dem Hintergrund klassischer kompilierter Programmiersprachen wie C und C++ stellt sich diese Frage, warum man JIT überhaupt benötigt. Wäre es nicht ökonomischer, den Quelltext nur einmal zu übersetzen und Ahead-of-Time Compilation (AOT) zu nutzen? Während das in vielen Fällen „nur“ eine Designentscheidung ist – zum Beispiel in Oracles HotSpot-JVM für Java –, hat man beim Entwurf einer JS-Runtime an dieser Stelle deutlich weniger Freiheit.

JS ist selten monolithisch, sondern kommt auf Webseiten meist in Form vieler kleiner, interagierender Skripte zum Einsatz. Die ausführende Maschinenarchitektur ist vorab nicht bekannt und das schwache, dynamische Typsystem stellt weder Informationen bereit noch macht es irgendwelche Einschränkungen. Zu guter Letzt kann man mit dem eval()-Statement zur Laufzeit beliebigen Quelltext nachladen. Damit fehlt einem AOT-Compiler quasi jede Grundlage für Optimierungen. JIT bietet hier einen Ausweg, indem der Compiler zur Laufzeit im Ausführungskontext so viele Informationen wie möglich sammelt, anhand derer er optimieren kann.

Als „Hello, World!“ der JIT-Betrachtung bei JS hat sich eine einfache Summenfunktion etabliert:

function sum(a, b) {
     return a + b; 

Listing 1: Interpretationen von sum

/*
 * Interpretation 1:
 *
 * "naiv"
 * einfache Summe von Ganzzahlen
 */
int sum1(int a, int b) {
  return a + b;
}

/*
 * Interpretation 2: 
 *
 * "dynamische Typen"
 * zwei unabhängige Typen mit davon
 * abhängigem Rückgabetyp
 */
template<typename A, typename B>
auto sum2(A a, B b) -> decltype(a + b) {
  return a + b;
}

Auf den ersten Blick erscheint sie trivial, weil ein menschlicher Leser dazu neigt, hier einen Kontext zu implizieren, der unter Umständen nicht vorhanden oder sogar falsch ist. Denn der Funktionsname und die einfache Implementierung mit dem +-Operator lassen vermuten, dass seine Funktion der von sum1 in Listing 1 entspricht. Dies ließe sich einfach in optimalen Maschinencode übersetzen, etwa für x86: