[Перевод] Wasmer: самая быстрая Go-библиотека для выполнения WebAssembly-кода

WebAssembly (wasm) — это переносимый бинарный формат инструкций. Один и тот же код wasm-код может выполняться в любой среде. Для того чтобы поддержать данное утверждение, каждый язык, платформа и система должны быть в состоянии выполнять такой код, делая это как можно быстрее и безопаснее.

6bba7gvszh2fqjm_zupvfk90fea.png
Wasmer — это среда для выполнения wasm-кода, написанная на Rust. Совершенно очевидно то, что wasmer можно использовать в любом Rust-приложении. Автор материала, перевод которого мы сегодня публикуем, говорит, что он и другие участники проекта Wasmer успешно внедрили эту среду выполнения wasm-кода в другие языки:

  • В C и C++ это реализовано посредством привязок.
  • В PHP — это расширение php-ext-wasm.
  • В Python — это пакет wasmer, опубликованный в PyPi, работа над которым идёт в репозитории python-ext-wasm.
  • В Ruby — это гем wasmer, который можно найти на RubyGems. Его код находится в репозитории ruby-ext-wasm.


Здесь речь пойдёт о новом проекте — go-ext-wasm, который представляет собой библиотеку для Go, предназначенную для выполнения бинарного wasm-кода. Как оказалось, проект go-ext-wasm гораздо быстрее, чем другие подобные решения. Но не будем забегать вперёд. Начнём с рассказа о том, как с ним работать.

Вызов wasm-функций из Go


Для начала установим wasmer в окружении Go (с поддержкой 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".

Теперь приступим к практике. Напишем простую программу, которая компилируется в wasm. Воспользуемся для этого, например, Rust:

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


Файл с программой назовём simple.rs, в результате компиляции этой программы получится файл simple.wasm.

Следующая программа, написанная на Go, выполняет функцию sum из wasm-файла, передавая ей в виде аргументов числа 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-файла, который был получен в результате компиляции кода, написанного на Rust.

Итак, эксперимент удался, мы успешно выполнили WebAssembly-код в Go. Надо отметить, что преобразование типов данных автоматизировано. Те значения Go, которые передаются в wasm-код, приводятся к типам WebAssembly. То, что возвращает wasm-функция, приводится к типам Go. В результате работа с функциями из wasm-файлов в Go выглядит так же, как работа с обычными функциями Go.

Вызов функций Go из WebAssembly-кода


Как мы видели в предыдущем примере, 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. Тут нет реализации данной функции, в файле определена лишь её сигнатура. Это — так называемая extern-функция. Для WebAssembly это — импортированная функция. Её реализацию необходимо импортировать.

Реализуем функцию sum средствами Go. Для этого нам понадобится использовать cgo. Вот получившийся в результате код. Некоторые комментарии, представляющие собой описание основных фрагментов кода, снабжены номерами. Ниже мы поговорим о них подробнее.

package main

// // 1. Объявляем сигнатуру функции `sum` (обратите внимание на cgo).
//
// #include 
//
// 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 — это API, используемый для создания импортов WebAssembly. В данном коде "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, нулевым символом.

Теперь переходим на сторону 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!


Функция return_hello возвращает указатель в виде значения i32. Мы получаем это значение, вызывая ToI32. Затем мы получаем данные из памяти с помощью instance.Memory.Data().

Эта функция возвращает слайс памяти экземпляра WebAssembly. Им можно пользоваться как любым слайсом Go.

Мы, к счастью, знаем длину строки, которую хотим прочесть, поэтому для чтения нужной информации достаточно воспользоваться конструкцией memory[pointer : pointer+13]. Затем прочитанные данные конвертируются в строку.

Вот пример, в котором показаны более продвинутые механизмы работы с памятью при использовании WebAssembly-кода в Go.

Бенчмарки


Проект go-ext-wasm, в чём мы только что убедились, обладает удобным API. Теперь пришло время поговорить о его производительности.

В отличие от PHP или Ruby, в мире Go уже имеются решения для работы с wasm-кодом. В частности, речь идёт о следующих проектах:

  • Life от Perlin Network — интерпретатор WebAssembly.
  • Wagon от Go Interpreter — интерпретатор WebAssembly и набор инструментов.


В материале о проекте php-ext-wasm для исследования производительности использовался алгоритм n-body. Существует и множество других алгоритмов, подходящих для исследования производительности сред выполнения кода. Например, это алгоритм Фибоначчи (рекурсивная версия) и ρ-алгоритм Полларда, используемые в Life. Это и алгоритм сжатия Snappy. Последний успешно работает с go-ext-wasm, но не с Life или Wagon. В результате он был убран из набора испытаний. Код тестов можно найти здесь.

В ходе испытаний использовались самые свежие версии исследуемых проектов. А именно, это Life 20190521143330–57f3819c2df0 и Wagon 0.4.0.

Числа, представленные на диаграмме, отражают усреднённые значения, полученные после 10 запусков теста. В исследовании использовался MacBook Pro 15» 2016 года с процессором Intel Core i7 2.9 ГГц и с 16 Гб памяти.

Результаты тестов сгруппированы по оси X в соответствии с видами тестов. Ось Y показывает время в миллисекундах, необходимое на выполнение теста. Чем показатель меньше — тем лучше.

8108c23213493ab6ade095909770b58e.png
Сравнение производительности Wasmer, Wagon и Life с помощью реализаций различных алгоритмов

Платформы Life и Wagon, в среднем, дают примерно одинаковые результаты. Wasmer же, в среднем, в 72 раза быстрее.

Важно отметить то, что Wasmer поддерживает три бэкенда: Singlepass, Cranelift и LLVM. Бэкенд, используемый по умолчанию в Go-библиотеке — это Cranelift (тут можно узнать подробности о нём). Использование LLVM даст производительность, близкую к нативной, но решено было начать с Cranelift, так как это бэкенд даёт лучшее соотношение между временем компиляции и временем выполнения программы.

Тут можно почитать о разных бэкендах, об их плюсах и минусах, о том, в каких ситуациях их лучше использовать.

Итоги


Опенсорсный проект go-ext-wasm — это новая Go-библиотека, предназначенная для выполнения бинарного wasm-кода. Она включает в себя среду выполнения Wasmer. Её первая версия включает в себя API, необходимость в которых возникает чаще всего.
Проведённые испытания производительности показали, что Wasmer, в среднем, в 72 раза быстрее чем Life и Wagon.

Уважаемые читатели! Планируете ли вы пользоваться возможностями по выполнению wasm-кода в Go с использованием go-ext-wasm?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru