Wasmer: la bibliothèque Go la plus rapide pour exécuter le code WebAssembly

WebAssembly (wasm) est un format d'instruction binaire portable. Le même code wasm peut être exécuté dans n'importe quel environnement. Afin de prendre en charge cette déclaration, chaque langue, plate-forme et système doit être capable d'exécuter un tel code, le rendant aussi rapide et sûr que possible.


Wasmer est un runtime de wasm écrit en Rust . De toute évidence, le wasmer peut être utilisé dans n'importe quelle application Rust. L'auteur du document, dont nous publions la traduction aujourd'hui, dit que lui et d'autres participants au projet Wasmer ont mis en œuvre avec succès ce runtime de code wasm dans d'autres langues:


Ici, nous parlerons d'un nouveau projet - go-ext-wasm , qui est une bibliothèque pour Go, conçue pour exécuter du code wasm binaire. Il s'est avéré que le projet go-ext-wasm est beaucoup plus rapide que d'autres solutions similaires. Mais n'allons pas de l'avant. Commençons par une histoire sur la façon de travailler avec lui.

Appel des fonctions de wasm à partir de Go


Pour commencer, installez wasmer dans un environnement Go (avec support cgo).

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

Le projet go-ext-wasm est une bibliothèque Go classique. Lorsque vous travaillez avec cette bibliothèque, la construction d' import "github.com/wasmerio/go-ext-wasm/wasmer" .

Passons maintenant à la pratique. Nous allons écrire un programme simple qui se compile en wasm. Nous utiliserons pour cela, par exemple, Rust:

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

Nous appelons le fichier avec le programme simple.rs , à la suite de la compilation de ce programme, nous obtenons le fichier simple.wasm .

Le programme suivant, écrit en Go, exécute la fonction sum du fichier wasm, en lui passant les nombres 5 et 37 comme arguments:

 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! } 

Ici, un programme écrit en Go appelle une fonction à partir d'un fichier wasm qui a été obtenu en compilant du code écrit en Rust.

L'expérience a donc été un succès, nous avons réussi à exécuter le code WebAssembly dans Go. Il convient de noter que la conversion des types de données est automatisée. Les valeurs Go transmises au code wasm sont converties en types WebAssembly. Ce que la fonction wasm renvoie est converti en types Go. Par conséquent, travailler avec des fonctions à partir de fichiers wasm dans Go ressemble à travailler avec des fonctions Go normales.

Fonctions d'appel à partir du code WebAssembly


Comme nous l'avons vu dans l'exemple précédent, les modules WebAssembly sont capables d'exporter des fonctions qui peuvent être appelées de l'extérieur. C'est le mécanisme qui permet au code wasm d'être exécuté dans divers environnements.

Dans le même temps, les modules WebAssembly eux-mêmes peuvent fonctionner avec des fonctions importées. Considérez le programme suivant écrit en Rust.

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

Nommez le fichier avec import.rs . Le compiler dans WebAssembly se traduira par du code qui peut être trouvé ici .

La fonction add1 exportée appelle la fonction sum . Il n'y a pas d'implémentation de cette fonction, seule sa signature est définie dans le fichier. Il s'agit de la fonction dite externe. Pour WebAssembly, il s'agit d'une fonction importée. Son implémentation doit être importée.

Nous implémentons la fonction sum utilisant Go. Pour cela, nous devons utiliser cgo . Voici le code résultant. Certains commentaires, qui sont des descriptions des principaux fragments de code, sont numérotés. Ci-dessous, nous en parlerons plus en détail.

 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 } 

Analysons ce code:

  1. La signature de la fonction sum est définie en C (voir le commentaire sur la commande d' import "C" ).
  2. L'implémentation de la fonction sum est définie dans Go (notez la ligne //export - ce mécanisme utilisé par cgo pour établir la connexion du code écrit en Go avec le code écrit en C).
  3. NewImports est une API utilisée pour créer des importations WebAssembly. Dans ce code, "sum" est le nom de la fonction importée par WebAssembly, sum est le pointeur vers la fonction Go et C.sum est le pointeur vers la fonction cgo.
  4. Et enfin, NewInstanceWithImports est un constructeur conçu pour initialiser un module WebAssembly avec des importations.

Lecture des données de la mémoire


L'instance WebAssembly a une mémoire linéaire. Parlons de la façon d'en lire les données. Commençons, comme d'habitude, avec le code Rust, que nous appellerons memory.rs .

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

Le résultat de la compilation de ce code se trouve dans le fichier memory.wasm , qui est utilisé ci-dessous.

La fonction return_hello renvoie un pointeur sur une chaîne. La ligne se termine, comme en C, par un caractère nul.

Maintenant, allez sur le côté Go:

 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! 

La fonction return_hello renvoie un pointeur en tant que valeur i32 . Nous obtenons cette valeur en appelant ToI32 . Ensuite, nous obtenons les données de la mémoire en utilisant instance.Memory.Data() .

Cette fonction renvoie la tranche de mémoire de l'instance WebAssembly. Vous pouvez l'utiliser comme n'importe quelle tranche Go.

Heureusement, nous connaissons la longueur de la ligne que nous voulons lire, donc, pour lire les informations nécessaires, il suffit d'utiliser la construction memory[pointer : pointer+13] . Ensuite, les données lues sont converties en chaîne.

Voici un exemple qui montre des mécanismes de mémoire plus avancés lors de l'utilisation du code WebAssembly de Go.

Repères


Le projet go-ext-wasm, comme nous venons de le voir, dispose d'une API pratique. Il est maintenant temps de parler de ses performances.

Contrairement à PHP ou Ruby, le monde Go a déjà des solutions pour travailler avec du code wasm. En particulier, nous parlons des projets suivants:

  • Life from Perlin Network - Interprète WebAssembly.
  • Go Interpreter's Wagon est un interpréteur et une boîte à outils WebAssembly.

Le matériel du projet php-ext-wasm a utilisé l'algorithme à n corps pour étudier les performances. Il existe de nombreux autres algorithmes adaptés à l'examen des performances des environnements d'exécution de code. Par exemple, il s'agit de l'algorithme de Fibonacci (version récursive) et de l'algorithme ρ de Pollard utilisé dans Life. Il s'agit de l'algorithme de compression Snappy. Ce dernier fonctionne avec succès avec go-ext-wasm, mais pas avec Life ou Wagon. En conséquence, il a été retiré de l'ensemble de test. Le code de test peut être trouvé ici .

Lors des tests, les dernières versions des projets de recherche ont été utilisées. À savoir, ce sont Life 20190521143330-57f3819c2df0 et Wagon 0.4.0.

Les nombres indiqués sur le graphique reflètent les valeurs moyennes obtenues après 10 démarrages du test. L'étude a utilisé le MacBook Pro 15 pouces 2016 avec un processeur Intel Core i7 2,9 GHz et 16 Go de mémoire.

Les résultats des tests sont regroupés le long de l'axe X en fonction des types de tests. L'axe des Y indique le temps, en millisecondes, nécessaire pour terminer le test. Plus l'indicateur est petit, mieux c'est.


Comparaison des performances de Wasmer, Wagon et Life à l'aide d'implémentations de divers algorithmes

Les plateformes Life et Wagon, en moyenne, donnent à peu près les mêmes résultats. Wasmer, en moyenne, est 72 fois plus rapide.

Il est important de noter que Wasmer prend en charge trois backends : Singlepass , Cranelift et LLVM . Le backend par défaut dans la bibliothèque Go est Cranelift ( ici vous pouvez en savoir plus). L'utilisation de LLVM donnera des performances proches de celles natives, mais il a été décidé de commencer par Cranelift, car ce backend donne le meilleur rapport entre le temps de compilation et le temps d'exécution du programme.

Ici, vous pouvez lire les différents backends, leurs avantages et leurs inconvénients, et dans quelles situations il est préférable de les utiliser.

Résumé


Le projet open source go-ext-wasm est une nouvelle bibliothèque Go conçue pour exécuter du code wasm binaire. Il comprend un runtime Wasmer . Sa première version comprend des API, dont le besoin se fait le plus souvent sentir.
Les tests de performance ont montré que Wasmer, en moyenne, est 72 fois plus rapide que Life et Wagon.

Chers lecteurs! Envisagez-vous d'utiliser la possibilité d'exécuter du code wasm dans Go à l'aide de go-ext-wasm?

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


All Articles