Wasmer: Die schnellste Go-Bibliothek zum Ausführen von WebAssembly-Code

WebAssembly (wasm) ist ein portables binäres Anweisungsformat. Der gleiche Code-Wasm-Code kann in jeder Umgebung ausgeführt werden. Um diese Aussage zu unterstützen, muss jede Sprache, Plattform und jedes System in der Lage sein, solchen Code auszuführen, um ihn so schnell und sicher wie möglich zu machen.


Wasmer ist eine in Rust geschriebene Wasm-Laufzeit. Natürlich kann der Wasmer in jeder Rust-Anwendung verwendet werden. Der Autor des Materials, dessen Übersetzung wir heute veröffentlichen, sagt, dass er und andere Teilnehmer des Wasmer-Projekts diese Wasm-Code-Laufzeit erfolgreich in anderen Sprachen implementiert haben:


Hier werden wir über ein neues Projekt sprechen - go-ext-wasm , eine Bibliothek für Go, die zur Ausführung von binärem Wasm-Code entwickelt wurde. Wie sich herausstellte, ist das go-ext-wasm-Projekt viel schneller als andere ähnliche Lösungen. Aber lasst uns nicht weiterkommen. Beginnen wir mit einer Geschichte darüber, wie man mit ihm arbeitet.

Wasm-Funktionen von Go aus aufrufen


Installieren Sie den Wasmer zunächst in einer Go-Umgebung (mit CGO-Unterstützung).

export CGO_ENABLED=1; export CC=gcc; go install github.com/wasmerio/go-ext-wasm/wasmer 

Das go-ext-wasm-Projekt ist eine reguläre Go-Bibliothek. Bei der Arbeit mit dieser Bibliothek wird das import "github.com/wasmerio/go-ext-wasm/wasmer" .

Jetzt lass uns üben. Wir werden ein einfaches Programm schreiben, das in wasm kompiliert wird. Wir werden dafür zum Beispiel Rust verwenden:

 #[no_mangle] pub extern fn sum(x: i32, y: i32) -> i32 {   x + y } 

Wir rufen die Datei mit dem Programm simple.rs . Als Ergebnis des Kompilierens dieses Programms erhalten wir die Datei simple.wasm .

Das folgende in Go geschriebene Programm führt die sum aus der WASM-Datei aus und übergibt ihr die Zahlen 5 und 37 als Argumente:

 package main import (   "fmt"   wasm "github.com/wasmerio/go-ext-wasm/wasmer" ) func main() {   //   WebAssembly.   bytes, _ := wasm.ReadBytes("simple.wasm")   //    WebAssembly.   instance, _ := wasm.NewInstance(bytes)   defer instance.Close()   //    `sum`   WebAssembly.   sum := instance.Exports["sum"]   //        Go.   //   ,      ,  .   result, _ := sum(5, 37)   fmt.Println(result) // 42! } 

Hier ruft ein in Go geschriebenes Programm eine Funktion aus einer WASM-Datei auf, die durch Kompilieren von in Rust geschriebenem Code erhalten wurde.

Das Experiment war also ein Erfolg. Wir haben den WebAssembly-Code in Go erfolgreich ausgeführt. Es ist zu beachten, dass die Datentypkonvertierung automatisiert ist. Diese Go-Werte, die an den Wasm-Code übergeben werden, werden in WebAssembly-Typen umgewandelt. Was die wasm-Funktion zurückgibt, wird in Go-Typen umgewandelt. Daher sieht das Arbeiten mit Funktionen aus WASM-Dateien in Go genauso aus wie das Arbeiten mit normalen Go-Funktionen.

Rufen Sie Go-Funktionen aus dem WebAssembly-Code auf


Wie wir im vorherigen Beispiel gesehen haben, können WebAssembly-Module Funktionen exportieren, die von außen aufgerufen werden können. Dies ist der Mechanismus, mit dem Wasm-Code in verschiedenen Umgebungen ausgeführt werden kann.

Gleichzeitig können WebAssembly-Module selbst mit importierten Funktionen arbeiten. Betrachten Sie das folgende in Rust geschriebene Programm.

 extern {   fn sum(x: i32, y: i32) -> i32; } #[no_mangle] pub extern fn add1(x: i32, y: i32) -> i32 {   unsafe { sum(x, y) } + 1 } 

import.rs Sie die Datei mit import.rs . Wenn Sie es in WebAssembly kompilieren, erhalten Sie Code, den Sie hier finden .

Die exportierte Funktion add1 ruft die sum . Es gibt keine Implementierung dieser Funktion, nur ihre Signatur ist in der Datei definiert. Dies ist die sogenannte externe Funktion. Für WebAssembly ist dies eine importierte Funktion. Die Implementierung muss importiert werden.

Wir implementieren die sum mit Go. Dafür müssen wir cgo verwenden . Hier ist der resultierende Code. Einige Kommentare, die Beschreibungen der Hauptcodefragmente sind, sind nummeriert. Im Folgenden werden wir ausführlicher darüber sprechen.

 package main // // 1.    `sum` (   cgo). // // #include <stdlib.h> // // extern int32_t sum(void *context, int32_t x, int32_t y); import "C" import (   "fmt"   wasm "github.com/wasmerio/go-ext-wasm/wasmer"   "unsafe" ) // 2.    `sum`    ( cgo). //export sum func sum(context unsafe.Pointer, x int32, y int32) int32 {   return x + y } func main() {   //   WebAssembly.   bytes, _ := wasm.ReadBytes("import.wasm")   // 3.     WebAssembly.   imports, _ := wasm.NewImports().Append("sum", sum, C.sum)   // 4.     WebAssembly  .   instance, _ := wasm.NewInstanceWithImports(bytes, imports)   //    WebAssembly.   defer instance.Close()   //    `add1`   WebAssembly.   add1 := instance.Exports["add1"]   //   .   result, _ := add1(1, 2)   fmt.Println(result)   // add1(1, 2)   // = sum(1 + 2) + 1   // = 1 + 2 + 1   // = 4   // QED } 

Lassen Sie uns diesen Code analysieren:

  1. Die Signatur der sum ist in C definiert (siehe Kommentar zum import "C" ).
  2. Die Implementierung der sum ist in Go definiert (beachten Sie die Zeile //export - dieser Mechanismus, mit dem cgo die Verbindung von in Go geschriebenem Code mit in C geschriebenem Code herstellt).
  3. NewImports ist eine API zum Erstellen von WebAssembly-Importen. In diesem Code ist "sum" der Name der von WebAssembly importierten Funktion, sum ist der Zeiger auf die Go-Funktion und C.sum ist der Zeiger auf die cgo-Funktion.
  4. Und schließlich ist NewInstanceWithImports ein Konstruktor, mit dem ein WebAssembly-Modul mit Importen initialisiert werden kann.

Daten aus dem Speicher lesen


Die WebAssembly-Instanz verfügt über einen linearen Speicher. Lassen Sie uns darüber sprechen, wie Daten daraus gelesen werden. Beginnen wir wie gewohnt mit dem Rust-Code, den wir memory.rs nennen.

 #[no_mangle] pub extern fn return_hello() -> *const u8 {   b"Hello, World!\0".as_ptr() } 

Das Ergebnis der Kompilierung dieses Codes befindet sich in der Datei memory.wasm , die unten verwendet wird.

Die Funktion return_hello gibt einen Zeiger auf eine Zeichenfolge zurück. Die Zeile endet wie in C mit einem Nullzeichen.

Gehen Sie jetzt zur Go-Seite:

 bytes, _ := wasm.ReadBytes("memory.wasm") instance, _ := wasm.NewInstance(bytes) defer instance.Close() //    `return_hello`. //      . result, _ := instance.Exports["return_hello"]() //      . pointer := result.ToI32() //    . memory := instance.Memory.Data() fmt.Println(string(memory[pointer : pointer+13])) // Hello, World! 

Die Funktion return_hello gibt einen Zeiger als i32 Wert zurück. Wir erhalten diesen Wert durch Aufrufen von ToI32 . Dann erhalten wir die Daten aus dem Speicher mit instance.Memory.Data() .

Diese Funktion gibt den Speicherbereich der WebAssembly-Instanz zurück. Sie können es wie jedes Go-Slice verwenden.

Glücklicherweise kennen wir die Länge der Zeile, die wir lesen möchten. Um die erforderlichen Informationen zu lesen, reicht es aus, das memory[pointer : pointer+13] verwenden. Dann werden die gelesenen Daten in eine Zeichenfolge konvertiert.

Hier ist ein Beispiel, das erweiterte Speichermechanismen bei Verwendung des WebAssembly-Codes von Go zeigt.

Benchmarks


Das go-ext-wasm-Projekt verfügt, wie wir gerade gesehen haben, über eine praktische API. Jetzt ist es Zeit, über seine Leistung zu sprechen.

Im Gegensatz zu PHP oder Ruby bietet die Go-Welt bereits Lösungen für die Arbeit mit Wasm-Code. Insbesondere sprechen wir über folgende Projekte:

  • Leben aus Perlin Network - WebAssembly-Interpreter.
  • Go Interpreter's Wagon ist ein WebAssembly-Interpreter und ein Toolkit.

Das Material des php-ext-wasm-Projekts verwendete den n-Körper- Algorithmus, um die Leistung zu untersuchen. Es gibt viele andere Algorithmen, mit denen die Leistung von Codeausführungsumgebungen untersucht werden kann. Dies ist beispielsweise der Fibonacci- Algorithmus (rekursive Version) und der Pollard-ρ-Algorithmus, die in Life verwendet werden. Dies ist der Snappy-Komprimierungsalgorithmus. Letzteres funktioniert erfolgreich mit Go-Ext-Wasm, aber nicht mit Life oder Wagon. Infolgedessen wurde er aus dem Testset entfernt. Testcode finden Sie hier .

Während der Tests wurden die neuesten Versionen der Forschungsprojekte verwendet. Dies sind nämlich Life 20190521143330-57f3819c2df0 und Wagon 0.4.0.

Die in der Tabelle angegebenen Zahlen geben die Durchschnittswerte wieder, die nach 10 Teststarts erhalten wurden. In der Studie wurde das 2016 MacBook Pro 15 "mit einem Intel Core i7 2,9-GHz-Prozessor und 16 GB Speicher verwendet.

Die Testergebnisse werden entlang der X-Achse gemäß den Testtypen gruppiert. Die Y-Achse zeigt die Zeit in Millisekunden an, die erforderlich ist, um den Test abzuschließen. Je kleiner der Indikator, desto besser.


Leistungsvergleich von Wasmer, Wagon und Life mit Implementierungen verschiedener Algorithmen

Life- und Wagon-Plattformen liefern im Durchschnitt ungefähr die gleichen Ergebnisse. Wasmer ist im Durchschnitt 72-mal schneller.

Es ist wichtig zu beachten, dass Wasmer drei Backends unterstützt: Singlepass , Cranelift und LLVM . Das Standard-Backend in der Go-Bibliothek ist Cranelift ( hier erfahren Sie mehr darüber). Die Verwendung von LLVM bietet eine Leistung, die nahezu nativ ist. Es wurde jedoch beschlossen, mit Cranelift zu beginnen, da dieses Backend das beste Verhältnis zwischen Kompilierungszeit und Programmausführungszeit bietet.

Hier können Sie über verschiedene Backends lesen, deren Vor- und Nachteile und in welchen Situationen es besser ist, sie zu verwenden.

Zusammenfassung


Das Open-Source-Projekt go-ext-wasm ist eine neue Go-Bibliothek, mit der binärer Wasm-Code ausgeführt werden kann. Es enthält eine Wasmer-Laufzeit . Die erste Version enthält APIs, deren Bedarf am häufigsten auftritt.
Leistungstests zeigten, dass Wasmer im Durchschnitt 72-mal schneller ist als Life and Wagon.

Liebe Leser! Planen Sie, die Möglichkeit zu nutzen, Wasm-Code in Go mit go-ext-wasm auszuführen?

Source: https://habr.com/ru/post/de454518/


All Articles