Um die Leistung von Webanwendungen zu verbessern, verwenden Sie WebAssembly in Verbindung mit AssemblyScript, um leistungskritische JavaScript-Komponenten einer Webanwendung neu zu schreiben. "Und wird es wirklich helfen?" Sie fragen.
Leider gibt es keine klare Antwort auf diese Frage. Es hängt alles davon ab, wie Sie sie verwenden. Es gibt viele Möglichkeiten: In einigen Fällen ist die Antwort negativ, in anderen positiv. In einer Situation ist es besser, JavaScript gegenüber AssemblyScript zu wählen, und in einer anderen ist es umgekehrt. Dies wird durch viele verschiedene Bedingungen beeinflusst.
In diesem Artikel werden wir diese Bedingungen analysieren, eine Reihe von Lösungen vorschlagen und sie an mehreren Testcodebeispielen testen.
Wer bin ich und warum mache ich dieses Thema?
(Sie können diesen Abschnitt überspringen, er ist nicht unbedingt erforderlich, um weiteres Material zu verstehen.)
Ich mag die AssemblyScript- Sprache sehr . Irgendwann habe ich sogar angefangen, Entwicklern finanziell zu helfen. Sie haben ein kleines Team, in dem jeder ernsthaft an diesem Projekt interessiert ist. AssemblyScript ist eine sehr junge TypeScript-ähnliche Sprache, die in WebAssembly (Wasm) kompiliert werden kann. Dies ist genau einer seiner Vorteile. Zuvor musste ein Webentwickler, um Wasm verwenden zu können, Fremdsprachen wie C, C ++, C #, Go oder Rust lernen, da solche Hochsprachen mit statischer Typisierung aus dem in WebAssembly kompiliert werden konnten Anfang.
Obwohl AssemblyScript (ASC) TypeScript (TS) ähnelt, ist es dieser Sprache nicht zugeordnet und wird nicht mit JS kompiliert. Ähnlichkeit in Syntax und Semantik ist erforderlich, um den Prozess des "Portierens" von TS nach ASC zu erleichtern. Diese Portierung läuft im Wesentlichen darauf hinaus, Typanmerkungen hinzuzufügen.
Ich war immer daran interessiert, JS-Code zu verwenden, ihn nach ASC zu portieren, ihn nach Wasm zu kompilieren und die Leistung zu vergleichen. Als mein Kollege Ingvar mir ein JavaScript-Snippet schickte , um Bilder zu verwischen , entschied ich mich, es zu verwenden. Ich habe ein kleines Experiment durchgeführt, um herauszufinden, ob es sich lohnt, dieses Thema genauer zu untersuchen. Es hat sich gelohnt. Infolgedessen erschien dieser Artikel.
Um besser wissen AssemblyScript, können Sie überprüfen die offizielle out Website , nehmen Sie den Discord Kanal, oder beobachten Sie das Einführungsvideo auf meinem Youtube - Kanal. Und wir gehen weiter.
Vorteile von WebAssembly
Wie ich oben geschrieben habe, bestand die Hauptaufgabe von Wasm lange Zeit darin, Code zu kompilieren, der in Hochsprachen für allgemeine Zwecke geschrieben wurde. Beispielsweise verwenden wir bei Squoosh (einem Online-Bildverarbeitungstool) Bibliotheken aus dem C / C ++ - und Rust-Ökosystem. Diese Bibliotheken wurden ursprünglich nicht für die Verwendung in Webanwendungen entwickelt, aber WebAssembly macht es möglich.
Darüber hinaus ist nach allgemeiner Meinung auch das Kompilieren des Quellcodes in Wasm erforderlich, da Sie durch die Verwendung von Wasm-Binärdateien die Arbeit einer Webanwendung beschleunigen können. Ich stimme zumindest zu, dass WebAssembly- und JavaScript-Binärdateien unter idealen (Labor-) Bedingungen dies könnenStellen Sie ungefähr gleiche Werte für die Spitzenleistung bereit. Dies ist bei Kampfwebprojekten kaum möglich.
Meiner Meinung nach ist es sinnvoller, sich WebAssembly als ein Optimierungswerkzeug für durchschnittliche Arbeitsleistungswerte vorzustellen. Obwohl Wasm in letzter Zeit die Möglichkeit hat, SIMD-Anweisungen und gemeinsam genutzte Speicherströme zu verwenden. Dies sollte die Wettbewerbsfähigkeit erhöhen. Aber wie ich oben geschrieben habe, hängt alles von der spezifischen Situation und den Anfangsbedingungen ab.
Im Folgenden werden einige solcher Bedingungen betrachtet:
Mangel an Aufwärmen
Die V8 JS-Engine verarbeitet den Quellcode und präsentiert ihn als abstrakten Syntaxbaum (AST). Basierend auf dem konstruierten AST generiert der optimierte Zündinterpreter einen Bytecode. Der resultierende Bytecode wird vom Sparkplug-Compiler übernommen und erzeugt am Ausgang den noch nicht optimierten Maschinencode mit großem Platzbedarf. Während der Ausführung des Codes sammelt V8 Informationen über die Formen (Typen) der verwendeten Objekte und führt dann den optimierenden Compiler TurboFan aus. Auf der Grundlage der gesammelten Informationen zu den Objekten werden Maschinenanweisungen auf niedriger Ebene generiert, die für die Zielarchitektur optimiert sind.
Sie können verstehen, wie JS-Engines funktionieren, indem Sie die Übersetzung dieses Artikels studieren .
JS-Engine-Pipeline. Allgemeines Schema
Auf der anderen Seite verwendet WebAssembly statische Typisierung, sodass Sie sofort Maschinencode daraus generieren können. Die V8-Engine verfügt über einen Streaming-Wasm-Compiler namens Liftoff. Wie bei Ignition können Sie damit nicht optimierten Code schnell vorbereiten und ausführen. Danach wacht derselbe TurboFan auf und optimiert den Maschinencode. Die Ausführung erfolgt schneller als nach dem Kompilieren von Liftoff, die Generierung dauert jedoch länger.
Der grundlegende Unterschied zwischen der JavaScript-Pipeline und der WebAssembly-Pipeline: Die V8-Engine muss keine Informationen zu Objekten und Typen sammeln, da Wasm statisch typisiert ist und alles im Voraus bekannt ist. Das spart Zeit.
Fehlende Deoptimierung
Der von TurboFan für JavaScript generierte Maschinencode kann nur verwendet werden, solange die Typannahmen beibehalten werden. Angenommen, TurboFan hat beispielsweise für eine Funktion f mit einem numerischen Parameter Maschinencode generiert. Bei einem Aufruf dieser Funktion mit einem Objekt anstelle einer Nummer verwendet die Engine erneut Zündung oder Zündkerze. Dies wird als Deoptimierung bezeichnet.
Bei WebAssembly können sich die Typen während der Programmausführung nicht ändern. Daher besteht keine Notwendigkeit für eine solche Deoptimierung. Und die Typen selbst, die Wasm unterstützt, werden organisch in Maschinencode übersetzt.
Minimierung von Binärdateien für große Projekte
Wasm wurde ursprünglich entwickelt , mit der kompakten Binärformat im Auge behalten. Daher werden solche Binärdateien schnell geladen. In vielen Fällen fallen sie jedoch immer noch mehr aus, als wir möchten (zumindest in Bezug auf die im Netzwerk akzeptierten Volumina). Mit gzip oder brotli werden diese Dateien jedoch gut komprimiert.
Im Laufe der Jahre hat JavaScript viele Dinge sofort gelernt: Arrays, Objekte, Wörterbücher, Iteratoren, Zeichenfolgenverarbeitung, prototypische Vererbung und so weiter. All dies ist in seinen Motor eingebaut. Und die C ++ - Sprache kann sich beispielsweise eines viel größeren Umfangs rühmen. Und jedes Mal, wenn Sie beim Kompilieren in WebAssembly eine dieser Sprachabstraktionen verwenden, muss der entsprechende Code unter der Haube in Ihrer Binärdatei enthalten sein. Dies ist einer der Gründe für die Verbreitung von WebAssembly-Binärdateien.
Wasm weiß eigentlich nichts über C ++ (oder eine andere Sprache). Daher bietet die Wasm-Laufzeit keine Standard-C ++ - Bibliothek und der Compiler muss sie jeder Binärdatei hinzufügen. Ein solcher Code muss jedoch nur einmal verbunden werden. Daher hat dies bei größeren Projekten keinen großen Einfluss auf die resultierende Größe der Wasm-Binärdatei, die am Ende häufig kleiner als andere Binärdateien ist.
Es ist klar, dass es nicht in allen Fällen möglich ist, eine fundierte Entscheidung zu treffen, indem nur die Größen der Binärdateien verglichen werden. Wenn beispielsweise der AssemblyScript-Quellcode in Wasm kompiliert wird, stellt sich heraus, dass die Binärdatei wirklich sehr kompakt ist. Aber wie schnell wird es laufen? Ich habe mir die Aufgabe gestellt, verschiedene Versionen von JS- und ASC-Binärdateien anhand von zwei Kriterien gleichzeitig zu vergleichen - Geschwindigkeit und Größe.
Portierung auf AssemblyScript
Wie ich bereits geschrieben habe, sind TypeScript und ASC in Syntax und Semantik sehr ähnlich. Es ist leicht anzunehmen, dass es Ähnlichkeiten mit JS gibt. Bei der Portierung geht es also hauptsächlich darum, Typanmerkungen hinzuzufügen (oder Typen zu ersetzen). So starten Sie die Portierung von Glur , JS-Bibliothek für Bildunschärfe.
Datentypzuordnung
Integrierte AssemblyScript-Typen werden ähnlich wie WebAssembly VM-Typen (Wasm Virtual Machine) implementiert. Wenn in TypeScript beispielsweise der Zahlentyp als 64-Bit-Gleitkommazahl (gemäß IEEE754-Standard) implementiert ist, gibt es in ASC eine Reihe von numerischen Typen: u8, u16, u32, i8, i16, i32 , f32 und f64. Darüber hinaus finden Sie in der AssemblyScript- Standardbibliothek gängige zusammengesetzte Datentypen (Zeichenfolge, Array, ArrayBuffer, Uint8Array usw.), die mit bestimmten Vorbehalten in TypeScript und JavaScript vorhanden sind. Ich werde hier nicht auf die Typzuordnungstabellen AssemblyScript, TypeScript und Wasm VM eingehen. Dies ist ein Thema für einen anderen Artikel. Das einzige, was ich beachten möchte, ist, dass ASC den StaticArray-Typ implementiert, für den ich in JS und WebAssembly VM keine Analoga gefunden habe.
Schließlich wenden wir uns unserem Beispielcode aus der Glur-Bibliothek zu.
JavaScript:
function gaussCoef(sigma) {
if (sigma < 0.5)
sigma = 0.5;
var a = Math.exp(0.726 * 0.726) / sigma;
/* ... more math ... */
return new Float32Array([
a0, a1, a2, a3,
b1, b2,
left_corner, right_corner
]);
}
AssemblyScript:
function gaussCoef(sigma: f32): Float32Array {
if (sigma < 0.5)
sigma = 0.5;
let a: f32 = Mathf.exp(0.726 * 0.726) / sigma;
/* ... more math ... */
const r = new Float32Array(8);
const v = [
a0, a1, a2, a3,
b1, b2,
left_corner, right_corner
];
for (let i = 0; i < v.length; i++) {
r[i] = v[i];
}
return r;
}
Das AssemblyScript-Codefragment enthält am Ende eine zusätzliche Schleife, da das Array nicht über den Konstruktor initialisiert werden kann. ASC implementiert keine Funktionsüberladung, daher haben wir in diesem Fall nur einen Konstruktor Float32Array (lengthOfArray: i32). AssemblyScript hat Rückrufe, aber keine Abschlüsse. Daher gibt es keine Möglichkeit, .forEach () zum Auffüllen eines Arrays mit Werten zu verwenden. Also musste ich eine reguläre for-Schleife verwenden, um jeweils ein Element zu kopieren.
Möglicherweise haben Sie bemerkt, dass im Code-Snippet auf AssemblyScript Math, Mathf. , 64- , — 32-. Math . - , , f32. . .
:
Ich habe lange gebraucht, um zu verstehen: Die Auswahl der Typen ist sehr wichtig. Das Verwischen des Bildes beinhaltet Faltungsoperationen, und das ist eine ganze Reihe von for-Schleifen, die alle Pixel durchlaufen. Es war naiv zu glauben, dass die Schleifenzähler auch positiv sind, wenn alle Pixelindizes positiv sind. Ich hätte nicht den Typ u32 (32-Bit-Ganzzahl ohne Vorzeichen) für sie wählen sollen. Wenn eine dieser Schleifen in die entgegengesetzte Richtung läuft, wird sie unendlich (das Programm wird eine Schleife ausführen):
let j: u32;
// ... many many lines of code ...
for (j = width — 1; j >= 0; j--) {
// ...
}
Ich habe keine anderen Schwierigkeiten bei der Portierung gefunden.
D8-Shell-Benchmarks
Okay, die zweisprachigen Codefragmente sind fertig. Jetzt können Sie ASC zu Wasm kompilieren und die ersten Benchmarks ausführen.
Ein paar Worte zu d8: Dies ist eine Befehlsshell für die V8-Engine (sie selbst hat keine eigene Schnittstelle), mit der Sie alle erforderlichen Aktionen sowohl mit Wasm als auch mit JS ausführen können. Im Prinzip kann d8 mit Node verglichen werden, der plötzlich die Standardbibliothek abhackte und nur noch reines ECMAScript übrig blieb. Wenn Sie keine kompilierte Version von V8 im Gebietsschema haben (die Kompilierung wird hier beschrieben ), können Sie d8 nicht verwenden. Verwenden Sie zum Installieren von d8 das Tool jsvu .
Da der Titel dieses Abschnitts jedoch das Wort "Benchmarks" enthält, finde ich es wichtig, hier einen Haftungsausschluss anzugeben: Die Zahlen und Ergebnisse, die ich erhalten habe, beziehen sich auf Code, den ich in den Sprachen meiner Wahl geschrieben habe, die auf meinem Computer ausgeführt werden Computer (2020 MacBook Air M1) mit den von mir erstellten Testskripten. Die Ergebnisse sind bestenfalls grobe Richtlinien. Daher wäre es voreilig, allgemeine quantitative Schätzungen der Leistung von AssemblyScript mit WebAssembly oder JavaScript mit V8 basierend auf diesen zu geben.
Möglicherweise haben Sie eine andere Frage: Warum habe ich d8 ausgewählt und keine Skripte im Browser oder im Knoten ausgeführt? Ich glaube, dass sowohl der Browser als auch der Knoten für unsere Experimente nicht steril genug sind. Neben der notwendigen Sterilität ermöglicht d8 die Steuerung der Pipeline des V8-Motors. Ich kann jedes Optimierungsszenario erfassen und zum Beispiel nur Zündung, nur Zündkerze oder Abheben verwenden, damit sich die Leistungsmerkmale während des Tests nicht ändern.
Experimentelle Technik
Wie ich oben geschrieben habe, haben wir die Möglichkeit, die JavaScript-Engine vor dem Ausführen des Leistungstests "aufzuwärmen". Während dieses Aufwärmvorgangs nimmt der V8 die notwendige Optimierung vor. Also habe ich das Unschärfeprogramm 5 Mal ausgeführt, bevor ich mit den Messungen begonnen habe, dann 50 Läufe ausgeführt und die 5 schnellsten und langsamsten Läufe ignoriert, um potenzielle Ausreißer und zu viele Ausreißer zu entfernen.
Sehen Sie, was passiert ist:
Einerseits war ich froh, dass Liftoff im Vergleich zu Ignition und Sparkplug einen schnelleren Code produzierte. Aber die Tatsache, dass AssemblyScript, das mithilfe der Optimierung in Wasm kompiliert wurde, sich als um ein Vielfaches langsamer herausstellte als das JavaScript-Bundle - TurboFan -, hat mich verwirrt.
Obwohl ich später dennoch zugab, dass die Kräfte anfangs nicht gleich waren: Ein riesiges Team von Ingenieuren arbeitet seit vielen Jahren an JS und seinem V8-Motor und implementiert Optimierungen und andere intelligente Dinge. AssemblyScript ist ein relativ junges Projekt mit einem kleinen Team. Der ASC-Compiler selbst ist Single-Pass und legt alle Optimierungsbemühungen auf die Binaryen- Bibliothek ... Dies bedeutet, dass die Optimierung auf der Bytecode-Ebene von Wasm VM erfolgt, nachdem die meisten Semantiken auf hoher Ebene bereits kompiliert wurden. Der V8 hat hier einen klaren Vorteil. Der Unschärfecode ist jedoch sehr einfach - es handelt sich um die üblichen arithmetischen Operationen mit Werten aus dem Speicher. Es schien, dass ASC und Wasm mit dieser Aufgabe besser hätten abschneiden sollen. Was ist hier los?
Lassen Sie uns tiefer graben
Ich habe schnell die klugen Jungs im V8-Team und die ebenso klugen Jungs im AssemblyScript-Team konsultiert (danke an Daniel und Max!). Es stellte sich heraus, dass beim Kompilieren von ASC die "Bounds Check" (Grenzwerte) nicht gestartet wird.
V8 kann jederzeit den JS-Quellcode anzeigen und seine Semantik verstehen. Diese Informationen werden für wiederholte oder zusätzliche Optimierungen verwendet. Sie haben beispielsweise einen ArrayBuffer, der eine Reihe von Binärdaten enthält. In diesem Fall erwartet V8, dass es am sinnvollsten ist, nicht nur chaotisch durch Speicherzellen zu laufen, sondern einen Iterator durch eine for ... of-Schleife zu verwenden.
for (<i>variable</i> of <i>iterableObject</i>) {
<i>statement</i>
}
Die Semantik dieses Operators stellt sicher, dass wir die Grenzen des Arrays niemals überschreiten. Folglich übernimmt der TurboFan-Compiler keine Überprüfung der Grenzen. Vor dem Kompilieren von ASC zu Wasm wird die AssemblyScript-Semantik für eine solche Optimierung jedoch nicht verwendet: Alle Optimierungen werden auf der Ebene der virtuellen WebAssembly-Maschine durchgeführt.
Glücklicherweise hat ASC immer noch einen Trumpf im Ärmel - die ungeprüfte () Annotation. Es gibt an, welche Werte auf die Möglichkeit eines Überschreitens der Grenzen überprüft werden sollen.
- prev_prev_out_r = prev_src_r * Koeffizient [6];
- line [line_index] = prev_out_r;
Die vorherigen 2 Zeilen müssen wie folgt umgeschrieben werden:
+ prev_prev_out_r = prev_src_r * nicht markiert (Koeffizient [6]);
+ deaktiviert (Zeile [Zeilenindex] = prev_out_r);
Ja, da ist noch mehr. In AssemblyScript werden typisierte Arrays (Uint8Array, Float32Array usw.) im Image und in der Abbildung von ArrayBuffer implementiert. Aufgrund des Fehlens einer allgemeinen Optimierung für den Zugriff auf das Array-Element mit dem Index i müssen Sie jedoch jedes Mal zweimal auf den Speicher zugreifen: das erste Mal, um den Zeiger auf das erste Array-Element zu laden, und das zweite Mal, um das Element zu laden Offset i * sizeOfType. Das heißt, Sie müssen das Array als Puffer (über einen Zeiger) bezeichnen. Im Fall von JS geschieht dies meistens nicht, da V8 es schafft, den Array-Zugriff auf hoher Ebene mithilfe eines einzelnen Speicherzugriffs zu optimieren.
Um die Leistung zu verbessern, implementiert AssemblyScript statische Arrays (StaticArray). Sie ähneln Array, haben jedoch eine feste Länge. Folglich muss kein Zeiger auf das erste Element des Arrays gespeichert werden. Verwenden Sie diese Arrays nach Möglichkeit, um Ihre Programme zu beschleunigen.
Also nahm ich das AssemblyScript - TurboFan-Bündel (es funktionierte schneller) und nannte es naiv. Dann habe ich die beiden Optimierungen hinzugefügt, über die ich in diesem Abschnitt gesprochen habe, und ich habe eine Variante namens optimiert erhalten.
Viel besser! Wir haben bedeutende Fortschritte gemacht. AssemblyScript ist jedoch immer noch langsamer als JavaScript. Ist das alles was wir tun können? [Spoiler: nein]
Oh, diese Stille
Die Leute vom AssemblyScript-Team sagten mir auch, dass das Flag --optimize gleich -O3s ist. Es optimiert die Arbeitsgeschwindigkeit gut, bringt sie aber nicht auf das Maximum, da es gleichzeitig das Wachstum der Binärdatei verhindert. Das Flag -O3 optimiert nur die Geschwindigkeit und tut dies bis zum Ende. Die standardmäßige Verwendung von -O3s scheint korrekt zu sein, da es in der Webentwicklung üblich ist, die Größe von Binärdateien zu reduzieren. Aber lohnt es sich? Zumindest in diesem speziellen Beispiel lautet die Antwort nein: -O3s spart nur knapp 30 Bytes, übersieht jedoch einen signifikanten Leistungsabfall:
Ein einziges Optimierungsflag dreht nur das Spiel um: Schließlich hat AssemblyScript JavaScript überholt (in diesem speziellen Testfall!).
Ich werde das O3-Flag nicht mehr in der Tabelle angeben, aber seien Sie versichert: Von nun an bis zum Ende des Artikels wird es für uns unsichtbar sein.
Blasensortierung
Um sicherzustellen, dass das verschwommene Beispiel nicht nur ein Unfall ist, habe ich mich für ein anderes entschieden. Ich habe die Sortierimplementierung in StackOverflow übernommen und denselben Prozess durchlaufen:
- portierte den Code durch Hinzufügen von Typen;
- startete den Test;
- optimiert;
- führte den Test erneut durch.
(Ich habe das Erstellen und Auffüllen eines Arrays für die Blasensortierung nicht getestet.)
Wir haben es wieder getan! Diesmal mit einem noch größeren Geschwindigkeitsgewinn: Das optimierte AssemblyScript ist fast doppelt so schnell wie JavaScript. Aber das ist nicht alles. Weitere Höhen und Tiefen erwarten mich wieder. Bitte nicht wechseln!
Speicherverwaltung
Einige von Ihnen haben vielleicht bemerkt, dass diese beiden Beispiele nicht wirklich zeigen, wie man mit Speicher arbeitet. In JavaScript übernimmt V8 die gesamte Speicherverwaltung (und Speicherbereinigung) für Sie. In WebAssembly hingegen haben Sie einen Teil des linearen Speichers und müssen entscheiden, wie er verwendet werden soll (oder wasm sich entscheiden muss). Wie stark wird sich unsere Tabelle ändern, wenn wir Heap intensiv nutzen?
Ich entschied mich für ein neues Beispiel mit einer binären Heap- Implementierung ... Während des Testens fülle ich einen Haufen mit 1 Million Zufallszahlen (mit freundlicher Genehmigung von Math.random ()) und pop () gibt sie zurück und prüfe, ob die Zahlen in aufsteigender Reihenfolge sind. Das allgemeine Arbeitsschema bleibt das gleiche: Portieren Sie den JS-Code auf ASC, kompilieren Sie mit der naiven Konfiguration, führen Sie die Tests aus, optimieren Sie die Tests und führen Sie sie erneut aus:
80x langsamer als JavaScript mit TurboFan ?! Und 6x langsamer als Zündung! Was schief gelaufen ist?
Einrichten der Laufzeitumgebung
Alle Daten, die wir in AssemblyScript generieren, müssen im Speicher gespeichert werden. Aber wir müssen sicherstellen, dass wir nichts überschreiben, was bereits vorhanden ist. Da AssemblyScript dazu neigt, das JavaScript-Verhalten nachzuahmen, verfügt es auch über einen Garbage Collector. Beim Kompilieren wird dieser Collector dem WebAssembly-Modul hinzugefügt. ASC möchte nicht, dass Sie sich Gedanken darüber machen, wann Sie Speicher zuweisen und wann Sie Speicher freigeben müssen.
In diesem Modus (inkrementell genannt) funktioniert es standardmäßig. Gleichzeitig werden dem Wasm-Modul nur etwa 2 KB im gzip-Archiv hinzugefügt. AssemblyScript bietet auch alternative Modi, Minimal und Stub. Modi können mit dem Flag --runtime ausgewählt werden. Minimal verwendet denselben Speicherzuweiser, jedoch einen leichteren Garbage Collector, der nicht automatisch startet, sondern manuell aufgerufen werden muss. Dies ist nützlich, wenn Sie Hochleistungsanwendungen (wie Spiele) entwickeln, bei denen Sie steuern möchten, wann der Garbage Collector Ihr Programm anhält. Im Stub-Modus wird dem Wasm-Modul nur sehr wenig Code hinzugefügt (~ 400B im gzip-Format). Es funktioniert schnell, da ein Backup-Allokator verwendet wird (weitere Details zu Allokatoren finden Sie hier ).
Redundante Allokatoren sind sehr schnell, können jedoch keinen Speicher freigeben. Es mag albern klingen, kann aber für "einmalige" Instanzen von Modulen nützlich sein, bei denen Sie nach Abschluss der Aufgabe die gesamte WebAssembly-Instanz löschen und eine neue erstellen, anstatt Speicher freizugeben.
Mal sehen, wie unser Modul in verschiedenen Modi funktioniert:
Sowohl Minimal als auch Stub haben uns dem Niveau der JavaScript-Leistung deutlich näher gebracht. Ich wundere mich warum? Wie oben erwähnt, verwenden Minimal und Inkremental denselben Allokator. Beide haben auch einen Garbage Collector, aber minimal führt ihn nur aus, wenn er explizit aufgerufen wird (was ich nicht tue). Dies bedeutet, dass der Punkt darin besteht, dass inkrementell die Speicherbereinigung automatisch gestartet wird und dies häufig unnötig geschieht. Nun, warum ist dies notwendig, wenn nur ein Array verfolgt werden muss?
Speicherzuordnungsproblem
Nachdem ich das Wasm-Modul mehrmals im Debug-Modus (--debug) ausgeführt hatte, stellte ich fest, dass sich die Arbeitsgeschwindigkeit aufgrund der Bibliothek libsystem_platform.dylib verlangsamt. Es enthält Grundelemente auf Betriebssystemebene für Threading und Speicherverwaltung. Aufrufe dieser Bibliothek erfolgen über __new () und __renew (), die wiederum von Array # push aufgerufen werden: Ich sehe: Hier liegt ein Speicherverwaltungsproblem vor. Aber JavaScript schafft es irgendwie, ein ständig wachsendes Array schnell zu verarbeiten. Warum kann AssemblyScript dies nicht tun? Glücklicherweise ist die AssemblyScript-Standardbibliotheksquelle öffentlich verfügbar. Schauen wir uns also diese unheimliche push () -Funktion der Array-Klasse an:
[Bottom up (heavy) profile]:
ticks parent name
18670 96.1% /usr/lib/system/libsystem_platform.dylib
13530 72.5% Function: *~lib/rt/itcms/__renew
13530 100.0% Function: *~lib/array/ensureSize
13530 100.0% Function: *~lib/array/Array#push
13530 100.0% Function: *binaryheap_optimized/BinaryHeap#push
13530 100.0% Function: *binaryheap_optimized/push
5119 27.4% Function: *~lib/rt/itcms/__new
5119 100.0% Function: *~lib/rt/itcms/__renew
5119 100.0% Function: *~lib/array/ensureSize
5119 100.0% Function: *~lib/array/Array#push
5119 100.0% Function: *binaryheap_optimized/BinaryHeap#push
export class Array<T> {
// ...
push(value: T): i32 {
var length = this.length_;
var newLength = length + 1;
ensureSize(changetype<usize>(this), newLength, alignof<T>());
// ...
return newLength;
}
// ...
}
Bisher ist alles korrekt: Die neue Länge des Arrays entspricht der aktuellen Länge, die um 1 erhöht wird. Als Nächstes wird die Funktion sureSize () aufgerufen, um sicherzustellen, dass im Puffer (Kapazität) genügend Kapazität für vorhanden ist das neue Element.
function ensureSize(array: usize, minSize: usize, alignLog2: u32): void {
// ...
if (minSize > <usize>oldCapacity >>> alignLog2) {
// ...
let newCapacity = minSize << alignLog2;
let newData = __renew(oldData, newCapacity);
// ...
}
}
Die Funktion sureSize () überprüft wiederum: Ist die Kapazität geringer als die neue minSize? Wenn ja, weisen Sie mit der Funktion _renew () einen neuen minSize-Puffer zu. Dies beinhaltet das Kopieren aller Daten aus dem alten Puffer in den neuen Puffer. Aus diesem Grund führt unser Test mit dem Füllen des Arrays mit 1 Million Werten (ein Element nach dem anderen) zur Neuzuweisung einer großen Speichermenge und verursacht viel Müll.
In anderen Bibliotheken (wie std :: vec in Rust oder SlicesIn Go) hat der neue Puffer die doppelte Kapazität des alten Puffers, wodurch die Arbeit mit dem Speicher kostengünstiger und langsamer wird. Ich arbeite an diesem Problem in ASC. Bisher besteht die einzige Lösung darin, ein eigenes CustomArray mit eigener Speicheroptimierung zu erstellen.
Jetzt ist inkrementell so schnell wie minimal und stub. In diesem Testfall bleibt JavaScript jedoch führend. Ich könnte wahrscheinlich weitere Optimierungen auf Sprachebene vornehmen, aber dies ist kein Artikel darüber, wie AssemblyScript selbst optimiert werden kann. Ich bin schon tief genug gesunken.
Es gibt viele einfache Optimierungen, die der AssemblyScript-Compiler für mich vornehmen könnte. Zu diesem Zweck arbeitet das ASC-Team an einem hochrangigen IR-Optimierer (Intermediate Representation) namens AIR. Kann dies den Job beschleunigen und es mir ersparen, den Array-Zugriff jedes Mal manuell optimieren zu müssen? Wahrscheinlich. Wird es schneller als JavaScript sein? Schwer zu sagen. Auf jeden Fall war es für mich interessant, um ASC zu konkurrieren, die Fähigkeiten von JS zu bewerten und zu sehen, was eine "ausgereiftere" Sprache mit "sehr intelligenten" Kompilierungswerkzeugen erreichen kann.
Rust & C ++
Ich habe den Code in Rust so idiomatisch wie möglich umgeschrieben und in WebAssembly kompiliert. Es stellte sich heraus, dass es schneller als AssemblyScript (naiv) war, aber langsamer als unser optimiertes AssemblyScript mit CustomArray. Als nächstes habe ich das aus Rust kompilierte Modul ähnlich wie AssemblyScript optimiert. Mit dieser Optimierung ist das Rust-basierte Wasm-Modul schneller als unser optimiertes AssemblyScript, aber immer noch langsamer als JavaScript.
Ich habe den gleichen Ansatz für C ++ gewählt und Emscripten zum Kompilieren in WebAssembly verwendet . Zu meiner Überraschung stellte sich heraus, dass selbst die erste Option ohne Optimierung nicht schlechter als JavaScript war.
Hier gibt es keine Bild-URL. Ich habe selbst einen Screenshot gemacht.
Versionen, die als idiomatisch markiert sind, wurden ohnehin vom JS-Quellcode beeinflusst. Ich habe versucht, mein Wissen über die Redewendungen von Rust, C ++, zu nutzen, aber die Installation war fest in meinem Kopf, dass ich portierte. Ich bin sicher, dass jemand mit mehr Erfahrung in diesen Sprachen die Aufgabe von Grund auf neu implementieren könnte und der Code anders aussehen würde.
Ich bin mir ziemlich sicher, dass Rust- und C ++ - Module noch schneller laufen könnten. Aber ich hatte nicht genug tiefe Kenntnisse dieser Sprachen, um mehr aus ihnen herauszuholen.
Wieder über die Größe der Binärdateien
Werfen wir einen Blick auf die Größe der Binärdateien nach der gzip-Komprimierung. Im Vergleich zu Rust und C ++ sind AssemblyScript-Binärdateien in der Tat viel leichter.
Und doch ... Empfehlungen
Ich habe am Anfang des Artikels darüber geschrieben und werde es jetzt wiederholen: Die Ergebnisse sind bestenfalls ungefähre Richtlinien. Daher wäre es voreilig, auf ihrer Grundlage allgemeine quantitative Schätzungen der Produktivität vorzunehmen. Zum Beispiel kann man nicht sagen, dass Rust in allen Fällen 1,2-mal langsamer als JavaScript ist. Diese Zahlen hängen stark vom Code ab, den ich geschrieben habe, von den Optimierungen, die ich angewendet habe, und von der Maschine, die ich verwendet habe.
Dennoch denke ich, dass es einige allgemeine Richtlinien gibt, die wir lernen können, um Ihnen zu helfen, das Thema besser zu verstehen und bessere Entscheidungen zu treffen:
- Liftoff AssemblyScript Wasm-, , , Ignition SparkPlug JavaScript. JS-, WebAssembly — .
- V8 JavaScript-. WebAssembly , JavaScript, , .
- , , .
- AssemblyScript-Module sind im Allgemeinen viel leichter als Wasm-Module, die aus anderen Sprachen kompiliert wurden. In diesem Artikel war die AssemblyScript-Binärdatei nicht kleiner als die JavaScript-Binärdatei, aber das Gegenteil gilt für größere Module, wie vom ASC-Entwicklungsteam angegeben.
Wenn Sie mir nicht glauben (und es auch nicht müssen) und den Code der Testfälle selbst herausfinden möchten, sind sie hier .
Unsere Server können für die WebAssembly-Entwicklung verwendet werden.
Registrieren Sie sich über den obigen Link oder indem Sie auf das Banner klicken und erhalten Sie 10% Rabatt für den ersten Monat der Anmietung eines Servers einer beliebigen Konfiguration!