Webanwendungen mit Performance nativer Apps

Bytecode mit WebAssembly erzeugen

| Autor / Redakteur: Filipe Martins & Anna Kobylinska / Stephan Augsten

WebAssembly soll Webanwendungen auf die Sprünge helfen.
WebAssembly soll Webanwendungen auf die Sprünge helfen. (Bild gemeinfrei: PIRO4D / Pixabay)

Wer seinen Web-Apps schon immer zur Leistung nativer Desktop-Anwendungen verhelfen wollte, kann jetzt aufatmen. Ein binäres Befehlsformat namens WebAssembly übernimmt die Schwerstarbeit.

Auf Grund von Performance-Engpässen der JavaScript-Verarbeitung im Browser hinken Web-Anwendungen gewöhnlichen Desktop-Applikationen immer noch hinterher. Die ungebrochene Dominanz von JavaScript im Web wurde für Web-Anwendungen in vielen Nutzungsszenarien zum Bremsklotz. Echtzeit-Spiele, Streaming virtueller Realität, Videoschnitt, Big-Data-Visualisierungen und Simulationen, KI-Anwendungen wie Sprach- oder Bilderkennung etc. sind nach wie vor eine Domäne von nativen Desktop-Applikationen.

An Ansätzen zum Beschleunigen von JavaScript mangelt es eigentlich nicht. Eine solche Lösung ist der AOT-Compiler (Ahead-of-Time Compiler) von Angular, mit dem sich HTML- und -TypeScript-Code in der Build-Phase in hocheffizienten JavaScript-Code konvertieren lässt. So kann der Browser sauber vorkompilierten Code herunterladen und die Anwendung schneller ausführen. Der Ansatz hat unbestrittene Vorteile, doch im Endeffekt kommt da immer noch JavaScript bei herum, mit allen Einschränkungen, die es mit sich bringt (zum Beispiel Probleme mit der Hochpräzisionsarithmetik).

Heiß begehrt: Chrome, Edge, Firefox und WebKit haben WebAssembly 1.0 bereits im Schnellverfahren implementiert
Heiß begehrt: Chrome, Edge, Firefox und WebKit haben WebAssembly 1.0 bereits im Schnellverfahren implementiert (Bild: CanIUse.com)

AOT-Kompilierungsengines haben zwar Vorteile gegenüber einem JIT-Kompiler, können jedoch an den grundlegenden Leistungsengpässen der beliebten Skriptsprache nicht wirklich etwas ändern. So ist zum Beispiel die Nutzung von Hardwarebeschleunigung in JavaScript nicht möglich. Damit bleiben Webbrowser als eine Ausführungsumgebung in Nutzungsszenarien mit hohen Performance-Anforderungen gegenüber nativen Anwendungen außen vor. Ein W3C-Standard namens WebAssembly verspricht, diese Lücken zu schließen.

Leistungsverbesserungen dank Binärcode

Bei WebAssembly (WASM) handelt es sich um ein binäres Befehlsformat für eine Stack-basierte virtuelle Maschine zur Bereitstellung von kompilierten, hochperformanten Client- und Serveranwendungen im Web. Das Beste daran: Wer in den Genuss von verbesserter Leistung und Betriebssystemportabilität seines Anwendungscode kommen möchte, braucht nicht erst noch eine weitere Programmiersprache zu lernen.

Kopf an Kopf: Performance von PolyBenchC-Benchmarks nach dem Kompilieren zu WebAssembly bei der Ausführung in Chrome und Firefox im Vergleich zu nativer Performance (die schwarze Linie bei 1.0) im Juli 2019 auf einem System mit einer CPU vom Typ 6-Core Intel Xeon E5-1650 v3 mit Hyperthreading und 64 GB RAM mit Ubuntu 16.04 mit dem Linux-Kernel v4.4.0.
Kopf an Kopf: Performance von PolyBenchC-Benchmarks nach dem Kompilieren zu WebAssembly bei der Ausführung in Chrome und Firefox im Vergleich zu nativer Performance (die schwarze Linie bei 1.0) im Juli 2019 auf einem System mit einer CPU vom Typ 6-Core Intel Xeon E5-1650 v3 mit Hyperthreading und 64 GB RAM mit Ubuntu 16.04 mit dem Linux-Kernel v4.4.0. (Bild: University of Massachusetts Amherst, USENIX Association)

WebAssembly ist eine Low-level-, statisch typisierte Sprache, die keine Garbage-Collection benötigt und JavaScript-Interoperabilität bietet. WebAssembly ist aber nicht als Programmiersprache ausgelegt, sondern als ein portables Kompilierungsziel für Hochsprachen wie C/C++ oder Rust zur Ausführung im Webbrowser.

Ist das schon schnell genug? Performance der SPEC-CPU-Benchmarks kompiliert zu WebAssembly und ausgeführt in Chrome und Firefox im Vergleich zu nativer Performance (die schwarze Linie bei 1.0) im Juli 2019 auf einem System mit einer CPU vom Typ 6-Core Intel Xeon E5-1650 v3 mit Hyperthreading und 64 GB RAM mit Ubuntu 16.04 mit dem Linux-Kernel v4.4.0.
Ist das schon schnell genug? Performance der SPEC-CPU-Benchmarks kompiliert zu WebAssembly und ausgeführt in Chrome und Firefox im Vergleich zu nativer Performance (die schwarze Linie bei 1.0) im Juli 2019 auf einem System mit einer CPU vom Typ 6-Core Intel Xeon E5-1650 v3 mit Hyperthreading und 64 GB RAM mit Ubuntu 16.04 mit dem Linux-Kernel v4.4.0. (Bild: University of Massachusetts Amherst, USENIX Association)

Der resultierende Bytecode wird auf dem Client in systemeigenen Maschinencode übersetzt und zum Beispiel in einem Webbrowser ausgeführt. WebAssembly beschreibt hierzu eine speichersichere Sandbox-Ausführungsumgebung, die sich sogar in vorhandenen virtuellen JavaScript-Maschinen implementieren lässt.

Entwickler, die in einer Sprache höherer Ebene zuhause sind, können zum Teil sogar ihren bestehenden Code für WebAssembly ausgeben. Wer beispielsweise eine mathematische Funktion benötigt, die aber nur in C++ vorliegt, kann sie als ein WebAssembly-Modul kompilieren und aus der Web-Anwendung heraus direkt im Browser ausführen. Die übrigen Teile der Web-App können unverändert in JavaScript codiert sein.

Die Implikationen für das Web der Zukunft sind enorm. Mit WASM ist es möglich, native Desktop-Anwendungen ohne einen Performanceverlust für die betriebssystemagnostische Ausführung im Browser zu portieren.

Für arithmetische Berechnungen mit einer hohen Genauigkeit mussten Web-Entwickler bisher serverseitigen Code schreiben und diese durch AJAX-Interaktionen des Benutzers mit der Web-Anwendung auslösen. Die hohe Latenz machte aber viele Nutzungsszenarien wie Echtzeitspiele oder Data-Analytics komplett unrealistisch.

Kompilierte Node.js-Module sind nicht wirklich eine Alternative, weil sie für jede einzelne Architektur separat kompiliert werden müssen und das ist viel zu umständlich. Bei WebAssembly gibt es das Problem nicht.

Ein (universelles) Kompilierungsziel, eine universelle Runtime

Die Vorgehensweise zur Ausgabe für WebAssembly hängt im Einzelfall davon ab, in welcher Programmiersprache der betreffende Code vorliegt. Zur Auswahl stehen die folgenden Workflows:

  • WebAssembly als direktes Kompilierungsziel: Bei Sprachen, die WebAssembly unterstützen, kann die Ausgabe für dieses Kompilierungsziel direkt erfolgen. Entwickler können also etwa eine Rust-Applikation verfassen und als Ausgabe-Target WebAssembly wählen.
  • WebAssembly aus clang+LLVM-Bitcode: In diese Kategorie fallen unter anderem C/C++, Kotlin/Native, Go, C# und viele andere. Tools wie Emscripten können den clang+LLVM-Bitcode aus beliebigen unterstützten Sprachen als ein WebAssembly-Binary (.wasm) zur Bereitstellung in einer existierenden JavaScript-Anwendung verpacken (in Emscripten ist WebAssembly im Übrigen die Standardeinstellung).
  • WebAssembly aus AssemblyScript: AssemblyScript ermöglicht das Kompilieren einer strikt typisierten Untermenge von TypeScript (einer typisierten Obermenge von JavaScript) mithilfe von Binaryen, einem Ahead-of-Time-Compiler, zu WebAssembly.
  • WebAssembly auf Assembler-Ebene: Wer es unbedingt darauf anlegt, könnte WebAssembly auf der Assembler-Ebene coden.
  • WebAssembly aus Toolchains von Drittanbietern: Für Sprachen wie Java und Lua oder auch .Net gibt es ebenfalls Tools von wohlwollenden Drittanbietern.
  • Interpreter anderer Sprachen nach JavaScript kompilieren und den Code dieser Sprachen im Browser ausführen: Wer die in C/C++ geschriebene Runtime einer Sprache wie Python, Ruby oder Lua nach JavaScript kompiliert, kann Code in diesen anderen Sprachen auf diese Weise im Browser indirekt ans Laufen bekommen (konkret ist dies für Python und Lua bereits geschehen).

Der Sinn und Zweck von WebAssembly besteht darin, den Anwendungscode schneller laden, analysieren und ausführen zu können, als dies mit JavaScript möglich ist. Zum Ausführen von WebAssembly-Binärcode im Webbrowser muss dieser erst einmal das WASM-Modul herunterladen und einrichten.

Eine gewisse Verzögerung ist in der Initialisierungsphase unvermeidbar (diese tritt aber im Übrigen auch bei sm.js auf). Die Bereitstellung von WebAssembly erfolgt im Übrigen mit Hilfe von JavaScript. Hat der Browser das WASM-Modul erst einmal geladen, geht es dann aber richtig zur Sache.

Eine (nahezu universelle) Toolchain: Emscripten & Co

Emscripten ist ein quelloffenes Toolchain zum Kompilieren von C/C++ und beliebigen anderen Sprachen über LLVM-Bitcode nach asm.js oder WebAssembly. Die Toolchain stellt einen „zentralen Umschlagplatz“ für eine Vielzahl verschiedener Programmiersprachen dar. Zum Erzeugen von LLVM-Bitcode kann Emscripten auf llvm-gcc (DragonEgg) oder clang zurückgreifen.

Emscripten verwendet zurzeit einen von zwei unterstützten Backends:

  • fastcomp (auf der Basis von asm.js, einem Subset von JavaScript, und asm2wasm);
  • das Upstream-LLVM-wasm-Backend

Die asm.js-basierte Lösung ist eine temporäre Krücke, suboptimal und bisher leider noch die Default-Einstellung. In naher Zukunft soll das Toolchain auf das LLVM-Backend „umsatteln“. Dieses bringt eine Menge nützlicher Features mit sich, darunter:

  • deutlich schnelleres Linking
  • schnellerer und kürzerer Code
  • Unterstützung für LLVM IR
  • erweiterter Support für WebAssembly
  • schnellerer Einbau von Upstream-Updates

Die Unterstützung von WebAssembly in Emscripten macht sich Binaryen im Emscripten SDK (kurz: emsdk) zunutze. Wer bisher noch kein emsdk eingerichtet hat, muss es selbst kompilieren und in der .emscripten-Datei einrichten.

Die Core-Codebasis von Emscripten befindet sich in der Haupt-Repo von emscripten und muss hierzu nicht erst noch kompiliert werden. Für die meisten Skripte, die alle Tools zusammenfügen, wird übrigens Python verwendet.

Wem die Performance am Herzen liegt, der ist gut beraten, das LLVM-wasm-Backend selbst zu kompilieren. LLVM stellt unter anderem clang, wasm-ld und Binaryen bereit. LLVM ist dann in der Lage, eine Menge intelligenter Optimierungen durchzuführen. Das Resultat ist schnellerer Code.

Um LLVM zu kompilieren eignet sich am besten die monorepo (https://github.com/llvm/llvm-project) unter Einbindung von ...

clang und

wasm-ld

..., beispielsweise über eine Option wie:

-DLLVM_ENABLE_PROJECTS='lld;clang'

In den „master“-Branches und/oder den Packaging-Anweisungen finden sich jeweils die allerneuesten Informationen im Sinne des Kleingedruckten. Nach dem Kompilieren gilt es, die .emscripten-Datei anzupassen, um die Pfade für jeden dieser Tools zu verzeichnen. Sollte die Datei noch nicht vorhanden sein, hilft es, emcc auszuführen.

Anders als seine Vorgänger zu Ende gedacht

WebAssembly ist nicht der erste Versuch, Binärcode im Browser auszuführen. Mit dem Gedanken hatten schon viele Anbieter gespielt, nicht zuletzt Microsoft.

Mit Microsofts ActiveX konnten Web-Entwickler signierte x86-Bibliotheken in Webseiten einbetten. Leider bekamen diese Binaries uneingeschränkten Zugriff auf die Windows-API und so hat sich die Technologie nie durchgesetzt. WebAssembly-Module laufen im Gegensatz dazu in einer Sandbox. Diesem Laufzeitmodus liegt bei WebAssembly das Ausführungsmodell von JavaScript zu Grunde. WebAssembly erzwingt unter anderem same-origin- und andere Berechtigungsrichtlinien des Browsers.

Die Entwickler von Native Client (NaCl) haben Microsofts Fehler nicht wiederholt. NaCl kann einer Web-Anwendung ein Modul in plattformspezifischem Maschinencode hinzufügen, lässt dieses aber in einer Sandbox eingeschlossen laufen. Leider vertraut NaCl auf die statische Validierung von Maschinencode und beschränkt die Freiheitsgrade der Code-Generatoren auf bestimmte Datenmuster.

So kommt der Browser im Endeffekt leider nur in den Genuss von einer Untermenge der Befehlssätze von x86, ARM und MIPS kommen kann. Die fehlende Portabilität des resultierenden Code führte zur Entstehung von Portable NaCl (PNaCl). Im Endeffekt konnte sich keine dieser Technologien durchsetzen.

WebAssembly hat die Nase vorn: Relative Performance von asm.js zu WebAssembly für Chrome und Firefox zeigt einen Zeitvorsprung des Standards um den Faktor von 1,54 in Chrome und 1,39 in Firefox gegenüber der quelloffenen Bibliothek.
WebAssembly hat die Nase vorn: Relative Performance von asm.js zu WebAssembly für Chrome und Firefox zeigt einen Zeitvorsprung des Standards um den Faktor von 1,54 in Chrome und 1,39 in Firefox gegenüber der quelloffenen Bibliothek. (Bild: University of Massachusetts Amherst, USENIX Association)

Asm.js, eine in den eigenen Worten der Entwickler „außerordentlich optimierbare, niedrigstufige Teilmenge von JavaScript“ (in die sich u.a. C übersetzen lässt), lässt sich ebenfalls in ein natives Binärformat der Zielplattform kompilieren. Da es sich aber bei dem Ausgangscode um (eine kleine Untermenge von) JavaScript handelt, erbt Asm.js sämtliche arithmetische Unzulänglichkeiten von der beliebten Skriptsprache.

Web-Assembly-Binärdateien sind kompakter als JavaScript, lassen sich einfacher validieren und trumpfen mit Garantien von Type-Safety und Isolation. Das Hauptnutzungsszenario von WebAssembly ist derzeit die Ausführung von vorkompiliertem Code im Webbrowser. Denkbar wäre aber auch durchaus die Ausführung in mobilen Apps, in Desktop-Anwendungen oder als serverseitige Binaries.

Fastly.com, ein Anbieter von Edge-Dienstleistungen für Web-Entwickler, bietet mit Lucet einen quelloffenen nativen WebAssembly-Compiler samt Runtime für Edge-Infrastrukturen (nicht zu verwechseln mit Microsofts Browser Edge). WebAssembly-Code lässt sich so serverseitig am Network-Edge ausführen. Lucet initialisiert Instanzen der Ausführungsumgebung für WebAssembly-Module in weniger als 50 Mikrosekunden, also einhundert Mal schneller als Google Chrome, freut sich das Team von Fastly.

Fazit

Mit der nativen Performance einer Desktop-App können JavaScript-Anwendungen nicht mithalten. Bisher gab ja auch praktisch keine Möglichkeiten, um die Leistungsengpässe von JavaScript im Browser adäquat zu umgehen. WebAssembly verspricht Abhilfe

Kommentare werden geladen....

Kommentar zu diesem Artikel

Der Kommentar wird durch einen Redakteur geprüft und in Kürze freigeschaltet.

Anonym mitdiskutieren oder einloggen Anmelden

Avatar
Zur Wahrung unserer Interessen speichern wir zusätzlich zu den o.g. Informationen die IP-Adresse. Dies dient ausschließlich dem Zweck, dass Sie als Urheber des Kommentars identifiziert werden können. Rechtliche Grundlage ist die Wahrung berechtigter Interessen gem. Art 6 Abs 1 lit. f) DSGVO.
  1. Avatar
    Avatar
    Bearbeitet von am
    Bearbeitet von am
    1. Avatar
      Avatar
      Bearbeitet von am
      Bearbeitet von am

Kommentare werden geladen....

Kommentar melden

Melden Sie diesen Kommentar, wenn dieser nicht den Richtlinien entspricht.

Kommentar Freigeben

Der untenstehende Text wird an den Kommentator gesendet, falls dieser eine Email-hinterlegt hat.

Freigabe entfernen

Der untenstehende Text wird an den Kommentator gesendet, falls dieser eine Email-hinterlegt hat.

copyright

Dieser Beitrag ist urheberrechtlich geschützt. Sie wollen ihn für Ihre Zwecke verwenden? Kontaktieren Sie uns über: support.vogel.de/ (ID: 46192492 / Web Apps)