Wasm oder nicht Wasm?

Wir bei Linkurious arbeiten an Linkurious Enterprise. Es handelt sich um eine webbasierte Plattform, die die Leistungsfähigkeit von Grafiken und deren Visualisierungstools nutzt, um Unternehmen und Behörden auf der ganzen Welt bei der Bekämpfung der Finanzkriminalität zu unterstützen.



Eines der Hauptmerkmale von Linkurious Enterprise ist eine einfach zu erlernende und benutzerfreundliche Grafikvisualisierungsoberfläche für Laien. Im Jahr 2015 haben wir frustriert von den Funktionen vorhandener JavaScript-Bibliotheken für die Grafikvisualisierung begonnen, unsere eigene Bibliothek zu entwickeln - Ogma.











Ogma ist eine JS-Bibliothek für Rendering und Rechenleistung, mit der Netzwerkstrukturen gerendert werden sollen. Möglicherweise haben Sie gesehen, wie Netzwerkstrukturen mit anderen JavaScript-Tools wie D3.js oder Sigma.js gerendert werden. Uns fehlten die Fähigkeiten dieser Tools. Für uns war es wichtig, dass die von uns verwendete Lösung einige spezifische Funktionen aufweist, damit sie bestimmte Leistungsanforderungen erfüllt. Wir haben weder das eine noch das andere in Bibliotheken von Drittanbietern gefunden. Aus diesem Grund haben wir uns entschlossen, unsere eigene Bibliothek von Grund auf neu zu entwickeln.



Aufgabe





Visualisierung der Netzwerkstruktur



Die Ogma-Bibliothek wurde für die Verwendung der fortschrittlichsten Algorithmen entwickelt. Alle Komponenten, von der fortschrittlichen WebGL-basierten Rendering-Engine bis zu den in den Tiefen verwendeten Web-Workern, zielen darauf ab, die beste verfügbare Netzwerk-Rendering-Leistung bereitzustellen. Unser Ziel war es, die Bibliothek bei der Ausführung langfristiger Aufgaben sehr interaktiv zu gestalten, damit die darin implementierten führenden Algorithmen zur Grafikvisualisierung schnell und stabil funktionieren.



Die WebAssembly (Wasm) -Technologie hat Webentwicklern von Anfang an ein Leistungsniveau versprochen, das im Vergleich zu dem, was ihnen zuvor zur Verfügung stand, günstig ist. Gleichzeitig mussten die Entwickler selbst keine übermäßigen Anstrengungen unternehmen, um die neue Technologie zu nutzen. Sie mussten lediglich Code in einer ihnen bekannten Hochleistungssprache schreiben, die nach der Kompilierung in einem Browser ausgeführt werden konnte.



Nachdem sich die Wasm-Technologie etwas weiterentwickelt hatte, haben wir uns entschlossen, sie auszuprobieren und einige Leistungstests durchzuführen, bevor wir sie implementieren. Bevor wir ohne Rückblick in den schnellen Wasm-Zug stiegen, beschlossen wir im Allgemeinen, auf Nummer sicher zu gehen.



Was uns an Wasm interessiert hat, war, dass diese Technologie unsere Probleme gut lösen kann, indem Speicher- und Prozessorressourcen effizienter als JavaScript verwendet werden.



Unsere Forschung



Unsere Forschung begann mit der Suche nach einem Algorithmus, der darauf abzielt, mit Graphen zu arbeiten, die mit ähnlichen Datenstrukturen leicht in verschiedene Sprachen portiert werden können.



Wir haben den n-Körper-Algorithmus gewählt. Es wird häufig als Basis für den Vergleich von Algorithmen zur Visualisierung von Kraftgraphen verwendet. Die nach diesem Algorithmus durchgeführten Berechnungen sind der ressourcenintensivste Teil der Visualisierungssysteme. Wenn dieser Teil der Arbeit solcher Systeme effizienter als zuvor durchgeführt werden könnte, würde sich dies sehr positiv auf alle in Ogma implementierten Algorithmen zur Visualisierung von Kraftgraphen auswirken.



Benchmark



Es gibt Lügen, grobe Lügen und Benchmarks.



Max De Marzi Die



Entwicklung eines „ehrlichen“ Benchmarks ist oft eine unmögliche Aufgabe. Tatsache ist, dass es in einer künstlich geschaffenen Umgebung schwierig ist, für die reale Welt typische Szenarien zu reproduzieren. Die Schaffung einer angemessenen Umgebung für komplexe Systeme ist immer äußerst schwierig. In einer Laborumgebung ist es zwar einfach, externe Faktoren zu steuern, aber in Wirklichkeit gibt es viele unerwartete Einflüsse darauf, wie die "Leistung" einer Lösung aussieht.



In unserem Fall zielte der Benchmark darauf ab, ein einzelnes, genau definiertes Problem zu lösen, um die Leistung der Implementierung des n-Körper-Algorithmus zu untersuchen.



Es ist ein klarer und gut dokumentierter Algorithmus, der von seriösen Organisationen zur Messung der Leistung verwendet wird.Programmiersprachen.



Wie bei jedem fairen Test haben wir einige Regeln für die verschiedenen Sprachen, die wir lernen, vordefiniert:



  • Unterschiedliche Implementierungen des Algorithmus sollten ähnliche Codestrukturen verwenden.
  • Es ist verboten, mehrere Prozesse oder mehrere Threads zu verwenden.
  • Die Verwendung von SIMD ist verboten .
  • Es werden nur stabile Versionen von Compilern getestet. Die Verwendung von Releases wie Nightly, Beta, Alpha, Pre-Alpha ist verboten.
  • Für jede Sprache werden nur die neuesten Compilerversionen verwendet.


Nachdem wir uns für die Regeln entschieden hatten, konnten wir bereits mit der Implementierung des Algorithmus fortfahren. Zuvor mussten wir jedoch auch die Sprachen auswählen, deren Leistung wir untersuchen werden.



JS Konkurrenten



Wasm ist ein Binärformat für Anweisungen, die in einem Browser ausgeführt werden. In verschiedenen Programmiersprachen geschriebener Code wird in dieses Format kompiliert. Sie sagen über Wasm-Code als von Menschen lesbaren Code, aber das Schreiben von Programmen direkt auf Wasm ist keine Entscheidung, die eine vernünftige Person treffen kann. Als Ergebnis haben wir eine Benchmark-Umfrage durchgeführt und die folgenden Kandidaten ausgewählt:





Der n-Körper-Algorithmus wurde in diesen drei Sprachen implementiert. Die Leistung verschiedener Implementierungen wurde mit der Leistung der Basis-JavaScript-Implementierung verglichen.



In jeder Implementierung des Algorithmus haben wir 1000 Punkte verwendet und den Algorithmus mit einer anderen Anzahl von Iterationen ausgeführt. Wir haben die Zeit gemessen, die benötigt wurde, um jede Programmsitzung abzuschließen.



Die Tests wurden mit folgender Software und Hardware durchgeführt:



  • NodeJS 12.9.1
  • Chrome 79.0.3945.130 (offizieller Build) (64-Bit)
  • clang 10.0.0 - für die Version des in der C-Sprache implementierten Algorithmus
  • emcc 1.39.6 - Frontend zum Aufrufen des Emscripten-Compilers über die Befehlszeile, eine Alternative zu gcc / clang und auch ein Linker
  • Ladung 1.40.0
  • Wasm-Pack 0.8.1
  • AssemblyScript 0.9.0
  • MacOS 10.15.2
  • Macbook Pro 2017 Netzhaut
  • Intel Dual Core i5 2,3 GHz, 8 GB DDR3, 256 GB SSD


Wie Sie sehen können, haben wir für die Tests nicht den schnellsten Computer ausgewählt, sondern Wasm, dh den Code, der im Kontext des Browsers ausgeführt wird. Außerdem hat der Browser normalerweise nicht Zugriff auf alle im System verfügbaren Prozessorkerne und auf den gesamten Arbeitsspeicher.



Um es interessanter zu machen, haben wir mehrere Versionen jeder Implementierung des Algorithmus erstellt. An einem Punkt im n-Körpersystem hatten sie eine numerische 64-Bit-Darstellung von Koordinaten, an dem anderen eine 32-Bit-Koordinatendarstellung.



Es ist auch erwähnenswert, dass wir eine "doppelte" Implementierung des Algorithmus in Rust verwendet haben. Zunächst wurde ohne Verwendung von Wasm-Tools eine "native", "unsichere" Rust-Implementierung geschrieben. Später wurde mit wasm-pack eine zusätzliche "sichere" Rust-Implementierung erstellt. Es wurde erwartet, dass diese Implementierung des Algorithmus einfacher in JS zu integrieren ist und dass der Speicher in Wasm besser verwaltet werden kann.



Testen



Wir haben unsere Experimente in zwei Hauptumgebungen durchgeführt. Dies ist Node.js und Browser (Chrome).



In beiden Fällen wurden Benchmarks mit einem warmen Skript durchgeführt. Die Speicherbereinigung wurde nämlich nicht gestartet, bevor die Tests ausgeführt wurden. Als wir die Garbage Collection nach jedem Test ausführten, schien dies keinen großen Einfluss auf die Ergebnisse zu haben.



Basierend auf dem in AssemblyScript geschriebenen Quellcode wurde Folgendes generiert:



  • Grundlegende JS-Implementierung des Algorithmus.
  • Wasm-Modul.
  • Asm.js Modul.


Es ist interessant festzustellen, dass das Modul asm.js nicht vollständig mit asm.js kompatibel war. Wir haben versucht, eine Anweisung "use asm" am oberen Rand des Moduls hinzuzufügen, aber der Browser hat diese Optimierung nicht akzeptiert. Später stellten wir fest, dass der von mir verwendete binäre Compiler nicht wirklich versuchte, den Code vollständig mit asm.js kompatibel zu machen. Stattdessen konzentrierte es sich darauf, eine Art effiziente JS-Version von Wasm zu entwickeln.



Wir haben zuerst Tests in Node.js durchgeführt.





Ausführen des Codes in der Node.js-Umgebung



Anschließend haben wir die Leistung desselben Codes im Browser gemessen.





Ausführen des Codes in einem Browser



Wir haben sofort festgestellt, dass die asm.js-Version des Codes langsamer arbeitet als die anderen Optionen. Diese Grafiken ermöglichen jedoch keinen ausreichend klaren Vergleich der Ergebnisse verschiedener Codevarianten mit der grundlegenden JS-Implementierung des Algorithmus. Um alles besser zu verstehen, haben wir einige weitere Diagramme erstellt.





Unterschiede zwischen anderen Implementierungen des Algorithmus und der JS-Implementierung (Benchmark-Version mit 64-Bit-Punktkoordinaten)





Unterschiede zwischen anderen Implementierungen des Algorithmus und der JS-Implementierung (Benchmark-Version mit 32-Bit-Punktkoordinaten)



Benchmark-Versionen mit 64-Bit- und 32-Bit-Punktkoordinaten unterscheiden sich deutlich. Dies könnte uns zu der Annahme führen, dass Zahlen in JavaScript beides sein können. Tatsache ist, dass Zahlen in JS bei der Implementierung des Algorithmus als Vergleichsbasis immer 64-Bit sind, aber Compiler, die Code aus anderen Sprachen in Wasm konvertieren, arbeiten mit solchen Zahlen auf unterschiedliche Weise.



Dies hat insbesondere einen großen Einfluss auf die asm.js-Version des Tests. Die Version mit 32-Bit-Punktkoordinaten ist in der Leistung sowohl der grundlegenden JS-Implementierung als auch der asm.js-Version, die 64-Bit-Zahlen verwendet, sehr viel schlechter.



In den vorherigen Diagrammen ist es schwierig zu verstehen, wie die Leistung der anderen Codevarianten im Vergleich zur JS-Variante ist. Dies liegt daran, dass sich die Metriken von AssemblyScript zu stark von den anderen unterscheiden. Um dies zu verstehen, haben wir ein weiteres Diagramm erstellt, in dem die Ergebnisse von asm.js entfernt wurden.





Unterschiede zwischen anderen Implementierungen des Algorithmus gegenüber der JS-Implementierung (Benchmark-Variante mit 64-Bit-Punktkoordinaten ohne asm.js)





Unterschiede zwischen anderen Implementierungen des Algorithmus und der JS-Implementierung (Benchmark-Variante mit 32-Bit-Koordinaten von Punkten ohne asm.js)



Unterschiedliche Darstellungen von Zahlen scheinen andere Versionen des Tests beeinflusst zu haben. Aber sie haben auf unterschiedliche Weise beeinflusst. Beispielsweise wurde die C-Variante, die 32-Bit-Zahlen (Floats) verwendete, langsamer als die C-Variante, die 64-Bit-Zahlen (Double) verwendete. Beide Rust-Versionen des Tests mit 32-Bit-Nummern (f32) wurden schneller als ihre Versionen mit 64-Bit-Nummern (f64).



Schlechte Implementierung des Algorithmus?



Die Analyse der obigen Daten kann den folgenden Gedanken nahe legen. Ist es möglich, dass Wasm-Implementierungen nur die Leistungsmerkmale der nativen Algorithmusimplementierungen widerspiegeln, da alle getesteten Wasm-Assemblys in ihrer Leistung der JavaScript-Implementierung sehr nahe kommen?





Vergleichen nativer Implementierungen eines Algorithmus mit einer JavaScript-Implementierung



Native Versionen einer Algorithmusimplementierung sind immer schneller als eine JavaScript-Implementierung.



Wir haben auch festgestellt, dass Wasm-Assemblys langsamer sind als die nativen Versionen des Codes, der zum Erstellen solcher Assemblys verwendet wird. Der Leistungsunterschied beträgt 20-50%. Wir haben dies an einer verkürzten Version des Benchmarks mit 1000 Iterationen herausgefunden.





C-Implementierung und entsprechende Wasm-Montage





Rostimplementierung und entsprechender Wasm Build





Rust-Implementierung, die mit wasm-pack erstellt wurde, und die entsprechende Wasm-Assembly



Bei der Messung der Ausführungszeit des nativen Codes wurde auch die zum Laden des Programms erforderliche Zeit berücksichtigt, und bei Wasm-Varianten wurde diese Zeit nicht berücksichtigt.



Ergebnis



Im Durchschnitt betrug der Leistungsgewinn der beiden Rust-Implementierungen des Algorithmus im Vergleich zu seiner grundlegenden JS-Implementierung 20%. Dies ist wahrscheinlich gut für das Image von Rust, aber es ist zu wenig Leistungsgewinn im Vergleich zu dem Aufwand, der erforderlich war, um es zu erhalten.



Welche Schlussfolgerungen können wir aus diesen Tests ziehen? Und hier einige: Durchdachtes Schreiben von JS-Code ermöglicht eine relativ hohe Leistung und erfordert keinen Wechsel zu anderen Programmiersprachen.



Das Erlernen neuer Programmiersprachen ist immer gut. Aber es muss gute Gründe geben, neue Sprachen zu lernen. Leistung ist oft der „falsche“ Grund, da die Entwurfsarchitektur auf hoher Ebene für die Leistung wichtiger ist als Compiler oder Mikrooptimierungen.



Als Experiment haben wir JavaScript in TypeScript geändert, um eine Implementierung des Force-Graph-Visualisierungsalgorithmus zu schreiben. Infolgedessen haben wir die Qualität der Codebasis verbessert, nicht jedoch die Leistung. Wir haben die Leistung speziell gemessen und es stellte sich heraus, dass sie nach dem Übergang leicht um 5% zunahm. Wahrscheinlich ist der Grund das Code-Refactoring.



Wenn Sie an der Entwicklung und Leistung von JavaScript interessiert sind, könnte es Sie interessieren, diesen Vortrag zu sehen, der ähnliche Ergebnisse wie die von uns erzielten klang.



Wie gehen Sie mit der Entwicklung der "schweren" Teile von Webprojekten um?






All Articles