Emscripten

Ein LLVM nach JavaScript Compiler


Titel | Inhalt | Einleitung | Grundlegendes | Arbeitsweise | Der Relooper | Grenzen | Performanz | Fazit | Quellen

Einschränkungen & Grenzen

Emscriptens Ansatz ist es, möglichst 'natürliches' JavaScript zu erzeugen, damit der erzeugte Code in den JavaScript-Engines eine gute Leistung erbringt. Insbesondere sollen dabei die normalen JavaScript Operationen zur Addition, Multiplikation, usw. genutzt werden.
Es existiert jedoch nicht zu jeder Operation oder jedem Datentypen in LLVM ein entsprechendes Gegenstück in JavaScript. Wird eine solche Operation trotdem mit einer einfachen JavaScript Operation abgebildet, gibt es semantische Unterschiede. Kommen solche Fälle in einem Programm zum Tragen, wird sich das nach JavaScript übersetzte Programm anders verhalten und ist damit eventuell nicht zu gebrauchen. Des Weiteren fehlen JavaScript einige Möglichkeiten, die andere Sprachen bereitstellen, wie z.B. Multithreading mit geteiltem Zustand.

Aufgrund dieser Problematik lassen sich einige Programme generell nicht mit Emscripten übersetzen. Andere laufen langsamer als vielleicht erwartet, wenn Emscripten beispielsweise die korrekte Semantik einer Operation selbst nachbilden muss.

Anfangs konnte Emscripten nur Programme verarbeiten, die sich an Load-Store-Consistency halten. Seitdem Emscripten den Speicher aber mithilfe von typisierten Feldern darstellt und nicht mehr durch ein einfaches JavaScript-Array, besteht diese Problematik nicht mehr.


Fundamentale Probleme

Einige fundamentale Probleme kann Emscripten nicht effizient lösen:

Programme, die mit mehreren Threads arbeiten, welche einen geteilten Zustand nutzen, lassen sich mit JavaScript nicht abbilden. Durch das noch relativ neue Konzept der web worker kann JavaScript zwar mit Threads umgehen, diese können allerdings nur durch message passing miteinander kommunizieren und sich keinen Speicherbereich teilen.

Generell ist auch nicht portabler Code ein Problem, der sehr nah an der Hardware arbeitet, oder Eigenschaften seiner nativen Umgebung nutzt. So sind Programme problematisch, die eine bestimmte Byte-Reihenfolge, wie Big-Endian oder Little-Endian, im Speicher zwingend voraussetzen. Des weiteren werden native Stack Manipulationen, beispielsweise in Verbindung mit langen Sprüngen (setjmp, longjmp), von Emscripten nicht unterstützt. Programme, die eine bestimmte Speicherausrichtung voraussetzen, oder unausgerichtet auf den Speicher zugreifen, können undefiniertes Verhalten hervorbringen. In solchen Fällen kann Emscripten eine Laufzeit-Ausnahmen auslösen. Emscripten bietet aber auch einen Modus zur Codeerzeugung, der so einen Umgang mit dem Speicher unterstützt. Der erzeugte Code verliert dadurch jedoch stark an Performanz.

Theoretisch wäre es zum Teil möglich, diese problematischen Funktionalitäten in JavaScript zu emulieren, dies wäre allerdings sehr langsam.


Umgang mit semantischen Unterschieden

Da JavasScript nicht zwischen unterschiedlichen Zahlentypen unterscheidet, werden Zahlen allgemein als 64-Bit Doubles implementiert. Sind in einem zu übersetzenden Programm beim Umgang mit Zahlen Seiteneffekte wie Überläufe und Rundungsfehler von Bedeutung, muss dieses Verhalten von Emscripten emuliert werden. Dies führt allerdings zu einer langsameren Ausführungsgeschwindigkeit, da zur Umsetzung der arithmetischen Operationen letztendlich mehr Befehle notwendig sind.

Wie bereits erwähnt, verhält sich beispielsweise eine 8-Bit LLVM Addition zweier ganzer Zahlen anders, als eine normale JavaScript Addition. Wird 255 mit 1 addiert, sollte das Ergebnis 0 sein, da es zu seinem Überlauf kommt. Bildet man die Addition einfach als 255+1 in JavaScript ab, beträgt das Ergebnis aber 256. Mit etwas zusätzlichem Code lässt sich das richtige Verhalten für etwas wie a = b + c in JavaScript aber nachbilden: a = (b + c)&255. Das &255 stellt eine bitweise UND-Verknüpfung mit 255 dar und fungiert quasi als Bitmaske. Treten bei der Addition größere Zahlen als 255 auf, werden die entsprechenden Bits zur Darstellung größerer Zahlen maskiert.

Wenn etwas entsprechendes aber für jede einzelne arithmetische Operation durchgeführt werden muss, entsteht eine Menge zusätzlicher Aufwand, der eine langsamere Ausführung zur Folge hat. Besonders bei solch trivialen Dingen, wie einer einfachen Addition, die eine JavaScript-Engine normalerweise sehr schnell umsetzen könnte.

In einem normalen Programm treten solche Seiteneffekte aber meist nicht auf, oder sind nur an einigen wenigen Stellen von Bedeutung. Daher ermöglicht Emscripten eine akkurate Umsetzung genau dort, wo es nötig ist und nutzt ansonsten die einfache und performante Umsetzung. Um entsprechende Stellen zu finden, lassen sich beim Kompilieren spezielle Laufzeit-Tests in das Programm einbauen. Dann muss das Programm mit repräsentativen Eingaben ausgeführt werden, wodurch für Zeilen, bei denen Seiteneffekte auftreten, eine Warnung ausgegeben wird. Beim erneuten Kompilieren braucht die akkurate Umsetzung nur in den jeweiligen Zeilen aktiviert zu werden. Dies funktioniert aber nur, wenn man das Programm mit wirklich repräsentativen Eingaben testet, bei denen alle möglichen Seiteneffekte im Programm auch tatsächlich auftreten.


Titel | Inhalt | Einleitung | Grundlegendes | Arbeitsweise | Der Relooper | Grenzen | Performanz | Fazit | Quellen