Wasmer: a biblioteca Go mais rápida para executar o código do WebAssembly

O WebAssembly (wasm) é um formato de instrução binária portátil. O mesmo código wasm code pode ser executado em qualquer ambiente. Para suportar esta declaração, todo idioma, plataforma e sistema deve poder executar esse código, tornando-o o mais rápido e seguro possível.


Wasmer é um tempo de execução wasm escrito em Rust . Obviamente, o wasmer pode ser usado em qualquer aplicação Rust. O autor do material, cuja tradução publicamos hoje, diz que ele e outros participantes do projeto Wasmer implementaram com êxito esse tempo de execução do código wasm em outros idiomas:


Aqui falaremos sobre um novo projeto - go-ext-wasm , que é uma biblioteca para Go, projetada para executar código-binário wasm. Como se viu, o projeto go-ext-wasm é muito mais rápido que outras soluções semelhantes. Mas não vamos nos antecipar. Vamos começar com uma história sobre como trabalhar com ele.

Chamando funções wasm do Go


Para começar, instale o wasmer em um ambiente Go (com suporte ao cgo).

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

O projeto go-ext-wasm é uma biblioteca Go normal. Ao trabalhar com esta biblioteca, a construção de import "github.com/wasmerio/go-ext-wasm/wasmer" .

Agora vamos praticar. Vamos escrever um programa simples que compila no wasm. Usaremos para isso, por exemplo, Rust:

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

Chamamos o arquivo com o programa simple.rs ; como resultado da compilação desse programa, obtemos o arquivo simple.wasm .

O programa a seguir, escrito em Go, executa a função sum do arquivo wasm, passando os números 5 e 37 como argumentos:

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

Aqui, um programa escrito em Go chama uma função de um arquivo wasm que foi obtido pela compilação de código escrito em Rust.

Portanto, o experimento foi um sucesso. Executamos com êxito o código do WebAssembly no Go. Note-se que a conversão do tipo de dados é automatizada. Esses valores de Go que são transmitidos para o código wasm são convertidos para tipos do WebAssembly. O que a função wasm retorna é convertido para tipos Go. Como resultado, trabalhar com funções de arquivos wasm no Go é o mesmo que trabalhar com funções normais do Go.

Funções Call Go a partir do código do WebAssembly


Como vimos no exemplo anterior, os módulos WebAssembly podem exportar funções que podem ser chamadas de fora. Esse é o mecanismo que permite que o código wasm seja executado em vários ambientes.

Ao mesmo tempo, os próprios módulos WebAssembly podem funcionar com funções importadas. Considere o seguinte programa escrito em 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 } 

Nomeie o arquivo com import.rs . A compilação no WebAssembly resultará em um código que pode ser encontrado aqui .

A função add1 exportada chama a função sum . Não há implementação dessa função, apenas sua assinatura é definida no arquivo. Essa é a chamada função externa. Para o WebAssembly, essa é uma função importada. Sua implementação deve ser importada.

Implementamos a função sum usando Go. Para isso, precisamos usar o cgo . Aqui está o código resultante. Alguns comentários, que são descrições dos principais fragmentos de código, são numerados. Abaixo falaremos sobre eles em mais detalhes.

 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 } 

Vamos analisar este código:

  1. A assinatura da função sum é definida em C (consulte o comentário no comando import "C" ).
  2. A implementação da função sum é definida em Go (observe a linha //export - este mecanismo que o cgo usa para estabelecer a conexão do código escrito em Go com o código escrito em C).
  3. NewImports é uma API usada para criar importações do WebAssembly. Nesse código, "sum" é o nome da função importada pelo WebAssembly, sum é o ponteiro para a função Go e C.sum é o ponteiro para a função cgo.
  4. E, finalmente, NewInstanceWithImports é um construtor projetado para inicializar um módulo WebAssembly com importações.

Lendo dados da memória


A instância do WebAssembly possui memória linear. Vamos falar sobre como ler dados dele. Vamos começar, como sempre, com o código Rust, que chamaremos de memory.rs .

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

O resultado da compilação desse código está no arquivo memory.wasm , usado abaixo.

A função return_hello retorna um ponteiro para uma string. A linha termina, como em C, com um caractere nulo.

Agora vá para o lado Ir:

 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! 

A função return_hello retorna um ponteiro como um valor i32 . ToI32 esse valor chamando ToI32 . Em seguida, obtemos os dados da memória usando instance.Memory.Data() .

Esta função retorna a fatia de memória da instância do WebAssembly. Você pode usá-lo como qualquer fatia do Go.

Felizmente, sabemos o comprimento da linha que queremos ler, portanto, para ler as informações necessárias, basta usar o construto memory[pointer : pointer+13] . Em seguida, os dados lidos são convertidos em uma sequência.

Aqui está um exemplo que mostra mecanismos de memória mais avançados ao usar o código WebAssembly da Go.

Benchmarks


O projeto go-ext-wasm, como acabamos de ver, possui uma API conveniente. Agora é hora de falar sobre seu desempenho.

Diferente do PHP ou Ruby, o mundo Go já tem soluções para trabalhar com código wasm. Em particular, estamos falando dos seguintes projetos:

  • Vida da rede Perlin - intérprete do WebAssembly.
  • O vagão da Go Interpreter é um interpretador e kit de ferramentas do WebAssembly.

O material do projeto php-ext-wasm usou o algoritmo n-body para estudar o desempenho. Existem muitos outros algoritmos adequados para examinar o desempenho de ambientes de execução de código. Por exemplo, este é o algoritmo de Fibonacci (versão recursiva) e o algoritmo Pollard ρ usado no Life. Este é o algoritmo de compactação Snappy. O último trabalha com êxito com go-ext-wasm, mas não com Life ou Wagon. Como resultado, ele foi removido do conjunto de testes. O código de teste pode ser encontrado aqui .

Durante os testes, foram utilizadas as versões mais recentes dos projetos de pesquisa. Ou seja, são Life 20190521143330-57f3819c2df0 e Wagon 0.4.0.

Os números mostrados no gráfico refletem os valores médios obtidos após 10 partidas do teste. O estudo usou o 2016 MacBook Pro 15 "com um processador Intel Core i7 2,9 GHz e 16 GB de memória.

Os resultados dos testes são agrupados ao longo do eixo X de acordo com os tipos de testes. O eixo Y mostra o tempo, em milissegundos, necessário para concluir o teste. Quanto menor o indicador, melhor.


Comparação de desempenho de Wasmer, Wagon e Life usando implementações de vários algoritmos

As plataformas Life e Wagon, em média, fornecem aproximadamente os mesmos resultados. O Wasmer, em média, é 72 vezes mais rápido.

É importante notar que o Wasmer suporta três back-ends: Singlepass , Cranelift e LLVM . O back-end padrão na biblioteca Go é Cranelift ( aqui você pode descobrir mais sobre ele). O uso do LLVM fornecerá um desempenho próximo do nativo, mas foi decidido começar com o Cranelift, pois esse back-end fornece a melhor relação entre o tempo de compilação e o tempo de execução do programa.

Aqui você pode ler sobre diferentes back-ends, seus prós e contras e em quais situações é melhor usá-los.

Sumário


O projeto de código aberto go-ext-wasm é uma nova biblioteca Go projetada para executar o código binário do wasm. Inclui um tempo de execução do Wasmer . Sua primeira versão inclui APIs, cuja necessidade surge com mais frequência.
Testes de desempenho mostraram que o Wasmer, em média, é 72 vezes mais rápido que o Life e o Wagon.

Caros leitores! Você planeja usar a capacidade de executar o código wasm no Go usando go-ext-wasm?

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


All Articles