Wasmer:执行WebAssembly代码的最快的Go库

WebAssembly(wasm)是可移植的二进制指令格式。 相同的代码wasm代码可以在任何环境中执行。 为了支持该语句,每种语言,平台和系统都必须能够执行此类代码,从而使其尽可能快且安全。


Wasmer是用Rust编写的wasm运行时。 显然,wasmer可以在任何Rust应用程序中使用。 该材料的作者(我们今天将其翻译发表)说,他和Wasmer项目的其他参与者已成功用其他语言实现了wasm代码运行时:


在这里,我们将讨论一个新项目go-ext-wasm ,它是Go的一个库,旨在执行二进制wasm代码。 事实证明,go-ext-wasm项目比其他类似解决方案要快得多。 但是,让我们不要超越自己。 让我们从一个关于如何与他一起工作的故事开始。

从Go调用wasm函数


首先,请在Go环境中安装wasmer(具有cgo支持)。

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

go-ext-wasm项目是一个常规的Go库。 使用此库时,将使用import "github.com/wasmerio/go-ext-wasm/wasmer"构造import "github.com/wasmerio/go-ext-wasm/wasmer"

现在开始练习。 我们将编写一个在wasm中编译的简单程序。 我们将为此使用,例如Rust:

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

我们使用程序simple.rs调用该文件,编译该程序后,得到的文件为simple.wasm

以下用Go语言编写的程序执行wasm文件中的sum函数,并将数字5和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! } 

在这里,用Go编写的程序从wasm文件调用函数,该wasm文件是通过编译用Rust编写的代码获得的。

因此,该实验取得了成功,我们在Go中成功执行了WebAssembly代码。 应该注意的是,数据类型转换是自动的。 那些传递给wasm代码的Go值将转换为WebAssembly类型。 wasm函数返回的内容强制转换为Go类型。 结果,在Go中使用wasm文件中的函数看起来与使用常规Go函数相同。

从WebAssembly代码中调用Go函数


正如我们在前面的示例中看到的,WebAssembly模块能够导出可以从外部调用的函数。 这是允许wasm代码在各种环境中执行的机制。

同时,WebAssembly模块本身可以与导入的功能一起使用。 考虑以下用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命名该文件。 将其编译为WebAssembly将产生可在此处找到的代码。

导出的add1函数调用sum函数。 此函数没有实现,只有其签名在文件中定义。 这就是所谓的外部函数。 对于WebAssembly,这是导入的功能。 必须导入其实现。

我们使用Go实现sum函数。 为此,我们需要使用cgo 。 这是结果代码。 一些注释,即对主要代码片段的描述,均已编号。 下面我们将更详细地讨论它们。

 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 } 

让我们分析一下这段代码:

  1. sum函数的签名在C中定义(请参见import "C"命令的注释)。
  2. sum函数的实现在Go中定义(请注意//export -cgo使用此机制来建立Go编写的代码与C编写的代码的连接)。
  3. NewImports是用于创建WebAssembly导入的API。 在此代码中, "sum"是WebAssembly导入的函数的名称, sum是Go函数的指针,而C.sum是cgo函数的指针。
  4. 最后, NewInstanceWithImports是一个构造函数,旨在初始化带有导入的WebAssembly模块。

从内存中读取数据


WebAssembly实例具有线性内存。 让我们谈谈如何从中读取数据。 让我们像往常一样从Rust代码开始,我们将其称为memory.rs

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

编译此代码的结果位于memory.wasm文件中,该文件在下面使用。

return_hello函数返回一个指向字符串的指针。 与C中一样,该行以空字符结尾。

现在转到“转到”一侧:

 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! 

return_hello函数返回一个指针作为i32值。 我们通过调用ToI32获得此值。 然后,我们使用instance.Memory.Data()从内存中获取数据。

此函数返回WebAssembly实例的内存片。 您可以像使用任何Go切片一样使用它。

幸运的是,我们知道要读取的行的长度,因此,要读取必要的信息,使用memory[pointer : pointer+13]构造就足够了。 然后,将读取的数据转换为字符串。

这是一个示例,显示了使用Go的WebAssembly代码时更高级的内存机制。

基准测试


正如我们刚刚看到的,go-ext-wasm项目具有一个方便的API。 现在该讨论其性能了。

与PHP或Ruby不同,Go世界已经有了处理wasm代码的解决方案。 特别是,我们正在谈论以下项目:

  • 来自Perlin Network的Life -WebAssembly解释器。
  • Go Interpreter的Wagon是一个WebAssembly解释器和工具包。

php-ext-wasm项目上的材料使用n-body算法来研究性能。 还有许多其他适合检查代码执行环境性能的算法。 例如,这是Life中使用的Fibonacci算法(递归版本)和Pollardρ算法 。 这是Snappy压缩算法。 后者可以通过go-ext-wasm成功运行,但不适用于Life或Wagon。 结果,他被从测试集中删除。 测试代码可以在这里找到。

在测试期间,使用了最新版本的研究项目。 即,这些是Life 20190521143330-57f3819c2df0和Wagon 0.4.0。

图表上显示的数字反映了开始10次测试后获得的平均值。 该研究使用的是2016年款MacBook Pro 15,配备Intel Core i7 2.9 GHz处理器和16 GB内存。

根据测试类型,将测试结果沿X轴分组。 Y轴显示完成测试所需的时间(以毫秒为单位)。 指标越小越好。


使用各种算法的实现比较Wasmer,W​​agon和Life的性能

平均而言,Life和Wagon平台提供的结果大致相同。 Wasmer平均速度要快72倍。

重要的是要注意Wasmer支持三个后端: SinglepassCraneliftLLVM 。 Go库中的默认后端是Cranelift( 在这里可以找到更多信息)。 使用LLVM将使性能接近本机,但是决定从Cranelift开始,因为此后端在编译时间和程序执行时间之间提供了最佳的比率。

在这里,您可以了解不同的后端,它们的优缺点,以及在哪种情况下最好使用它们。

总结


开源项目go-ext-wasm是一个新的Go库,旨在执行二进制wasm代码。 它包括Wasmer运行时 。 它的第一个版本包括API,最常出现的需求是API。
性能测试表明,Wasmer平均比Life和Wagon快72倍。

亲爱的读者们! 您是否打算使用go-ext-wasm在Go中运行wasm代码的功能?

Source: https://habr.com/ru/post/zh-CN454518/


All Articles