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() {
在这里,用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
让我们分析一下这段代码:
sum
函数的签名在C中定义(请参见import "C"
命令的注释)。sum
函数的实现在Go中定义(请注意//export
-cgo使用此机制来建立Go编写的代码与C编写的代码的连接)。NewImports
是用于创建WebAssembly导入的API。 在此代码中, "sum"
是WebAssembly导入的函数的名称, sum
是Go函数的指针,而C.sum
是cgo函数的指针。- 最后,
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
函数返回一个指针作为
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,Wagon和Life的性能平均而言,Life和Wagon平台提供的结果大致相同。 Wasmer平均速度要快72倍。
重要的是要注意Wasmer支持三个后端:
Singlepass ,
Cranelift和
LLVM 。 Go库中的默认后端是Cranelift(
在这里可以找到更多信息)。 使用LLVM将使性能接近本机,但是决定从Cranelift开始,因为此后端在编译时间和程序执行时间之间提供了最佳的比率。
在这里,您可以了解不同的后端,它们的优缺点,以及在哪种情况下最好使用它们。
总结
开源项目
go-ext-wasm是一个新的Go库,旨在执行二进制wasm代码。 它包括
Wasmer运行时 。 它的第一个版本包括API,最常出现的需求是API。
性能测试表明,Wasmer平均比Life和Wagon快72倍。
亲爱的读者们! 您是否打算使用go-ext-wasm在Go中运行wasm代码的功能?
