⬆️ ⬇️

Wasmer: the fastest go-library for running WebAssembly code

WebAssembly (wasm) is a portable binary instruction format. The same wasm code can be executed in any environment. In order to support this statement, every language, platform and system must be able to execute such code, making it as fast and safe as possible.





Wasmer is a medium for executing a wasm code written in Rust . Obviously, wasmer can be used in any Rust application. The author of the material, the translation of which we are publishing today, says that he and other participants in the Wasmer project have successfully implemented this wasm-execution environment in other languages:





Here we will discuss a new project - go-ext-wasm , which is a library for Go, designed to execute a binary wasm code. As it turned out, the go-ext-wasm project is much faster than other similar solutions. But let's not get ahead. Let's start with a story about how to work with him.

')

Calling wasm functions from Go



First, install a wasmer in the Go environment (with cgo support).



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


The go-ext-wasm project is a regular Go library. When working with this library, the import "github.com/wasmerio/go-ext-wasm/wasmer" construction is used import "github.com/wasmerio/go-ext-wasm/wasmer" .



Now we will start practice. We write a simple program that is compiled into wasm. Let's use for this, for example, Rust:



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


The file with the program is called simple.rs , as a result of the compilation of this program, the file simple.wasm is obtained .



The following program, written in Go, performs the function sum from a wasm file, passing in the form of arguments the numbers 5 and 37:



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


Here, a program written in Go calls a function from a wasm-file, which was obtained as a result of compiling code written in Rust.



So, the experiment was a success, we successfully executed the WebAssembly code in Go. It should be noted that data type conversion is automated. Those Go values ​​that are passed to the wasm code are cast to the WebAssembly types. What the wasm function returns is cast to Go types. As a result, working with functions from wasm files in Go looks the same as working with ordinary Go functions.



Calling Go functions from WebAsse assembly code



As we saw in the previous example, WebAsse-modules are able to export functions that can be called externally. This is the mechanism that allows the wasm code to be executed in various environments.



In this case, WebAssembly modules themselves can work with imported functions. Consider the following program written in 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 } 


import.rs file with it import.rs . As a result of its compilation in WebAssembly, you get code that can be found here .



The exported function add1 calls the function sum . There is no implementation of this function, only its signature is defined in the file. This is the so-called extern-function. For WebAssembly, this is an imported function. Its implementation must be imported.



We implement the sum function using Go. For this we need to use cgo . Here is the resulting code. Some comments, which are descriptions of the main code fragments, are numbered. Below we talk about them in more detail.



 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 } 


Let's sort this code:



  1. The signature of the sum function is defined in C (see the comment above the import "C" command).
  2. The implementation of the sum function is defined in Go (note the //export line — such a mechanism cgo uses to link the code written in Go to the code written in C).
  3. NewImports is the API used to create WebAssembly imports. In this code, "sum" is the name of the function imported by WebAssembly, sum is the pointer to the Go function, and C.sum is the pointer to the cgo function.
  4. And finally, NewInstanceWithImports is a constructor designed to initialize the module WebAssembly with imports.


Reading data from memory



The WebAssembly instance has linear memory. Let's talk about how to read data from it. Let's start, as usual, with a Rust-code, which we call memory.rs .



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


The result of compiling this code is in the memory.wasm file, which is used below.



The return_hello function returns a pointer to a string. The line ends, as in C, with a null character.



Now go to the Go side:



 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! 


The return_hello function returns a pointer as an i32 value. We get this value by calling ToI32 . Then we retrieve the data from memory using instance.Memory.Data() .



This function returns a memory slice of an instance of WebAssembly. It can be used like any Go slice.



We, fortunately, know the length of the line we want to read, so to read the necessary information, it is enough to use the construction of memory[pointer : pointer+13] . Then the read data is converted to a string.



Here is an example that shows more advanced memory handling mechanisms when using WebAssembly code in Go.



Benchmarks



The go-ext-wasm project, as we have just seen, has a convenient API. Now it's time to talk about its performance.



Unlike PHP or Ruby, in the world of Go, there are already solutions for working with wasm code. In particular, we are talking about the following projects:





In the material about the php-ext-wasm project, the n-body algorithm was used to study performance. There are many other algorithms that are suitable for studying the performance of code execution environments. For example, this is the Fibonacci algorithm (recursive version) and the Pollard ρ-algorithm used in Life. This is the Snappy compression algorithm. The latter successfully works with go-ext-wasm, but not with Life or Wagon. As a result, he was removed from the test suite. Test code can be found here .



During the tests, the latest versions of the studied projects were used. Namely, it is Life 20190521143330–57f3819c2df0 and Wagon 0.4.0.



The numbers shown in the diagram reflect the averaged values ​​obtained after 10 test runs. The study used a MacBook Pro 15 "2016 with an Intel Core i7 2.9 GHz processor and 16 GB of memory.



Test results are grouped along the X axis according to the types of tests. The y-axis shows the time in milliseconds required to run the test. The lower the index, the better.





Comparison of Wasmer, Wagon and Life performance using implementations of various algorithms



The Life and Wagon platforms, on average, give roughly the same results. Wasmer, on average, is 72 times faster.



It is important to note that Wasmer supports three backends: Singlepass , Cranelift and LLVM . The backend used by default in the Go library is Cranelift ( here you can find out more about it). Using LLVM will give performance close to native, but it was decided to start with Cranelift, since this backend gives the best balance between compile time and program execution time.



Here you can read about different backends, their pros and cons, about the situations in which they should be used.



Results



The open source project go-ext-wasm is a new Go-library designed for executing binary wasm-code. It includes the Wasmer runtime environment . Its first version includes an API, the need for which occurs most often.

Performance tests showed that Wasmer, on average, was 72 times faster than Life and the Wagon.



Dear readers! Do you plan to use the capabilities for executing a wasm code in Go using go-ext-wasm?



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



All Articles