- PVSM.RU - https://www.pvsm.ru -
WebAssembly (wasm) — это переносимый бинарный формат инструкций. Один и тот же код wasm-код может выполняться в любой среде. Для того чтобы поддержать данное утверждение, каждый язык, платформа и система должны быть в состоянии выполнять такой код, делая это как можно быстрее и безопаснее.
[1]
Wasmer [2] — это среда для выполнения wasm-кода, написанная на Rust [3]. Совершенно очевидно то, что wasmer можно использовать в любом Rust-приложении. Автор материала, перевод которого мы сегодня публикуем, говорит, что он и другие участники проекта Wasmer успешно внедрили эту среду выполнения wasm-кода в другие языки:
Здесь речь пойдёт о новом проекте — go-ext-wasm [10], который представляет собой библиотеку для Go, предназначенную для выполнения бинарного wasm-кода. Как оказалось, проект go-ext-wasm гораздо быстрее, чем другие подобные решения. Но не будем забегать вперёд. Начнём с рассказа о том, как с ним работать.
Для начала установим wasmer в окружении Go (с поддержкой cgo).
export CGO_ENABLED=1; export CC=gcc; go install github.com/wasmerio/go-ext-wasm/wasmer
Проект go-ext-wasm [10] представляет собой обычную библиотеку 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 [11].
Следующая программа, написанная на 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.
Как мы видели в предыдущем примере, 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 получится код, который можно найти здесь [12].
Экспортированная функция add1
вызывает функцию sum
. Тут нет реализации данной функции, в файле определена лишь её сигнатура. Это — так называемая extern-функция. Для WebAssembly это — импортированная функция. Её реализацию необходимо импортировать.
Реализуем функцию sum
средствами Go. Для этого нам понадобится использовать cgo [13]. Вот получившийся в результате код. Некоторые комментарии, представляющие собой описание основных фрагментов кода, снабжены номерами. Ниже мы поговорим о них подробнее.
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
}
Разберём этот код:
sum
определяется в C (смотрите комментарий над командой import "C"
).sum
определена в Go (обратите внимание на строку //export
— такой механизм cgo использует для установления связи кода, написанного на Go, с кодом, написанным на C).NewImports
— это API, используемый для создания импортов WebAssembly. В данном коде "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!".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]
. Затем прочитанные данные конвертируются в строку.
Вот [14] пример, в котором показаны более продвинутые механизмы работы с памятью при использовании WebAssembly-кода в Go.
Проект go-ext-wasm, в чём мы только что убедились, обладает удобным API. Теперь пришло время поговорить о его производительности.
В отличие от PHP или Ruby, в мире Go уже имеются решения для работы с wasm-кодом. В частности, речь идёт о следующих проектах:
В материале [17] о проекте php-ext-wasm для исследования производительности использовался алгоритм n-body [18]. Существует и множество других алгоритмов, подходящих для исследования производительности сред выполнения кода. Например, это алгоритм Фибоначчи [19] (рекурсивная версия) и ρ-алгоритм Полларда [20], используемые в Life. Это и алгоритм сжатия Snappy. Последний успешно работает с go-ext-wasm, но не с Life или Wagon. В результате он был убран из набора испытаний. Код тестов можно найти здесь [21].
В ходе испытаний использовались самые свежие версии исследуемых проектов. А именно, это Life 20190521143330–57f3819c2df0 и Wagon 0.4.0.
Числа, представленные на диаграмме, отражают усреднённые значения, полученные после 10 запусков теста. В исследовании использовался MacBook Pro 15" 2016 года с процессором Intel Core i7 2.9 ГГц и с 16 Гб памяти.
Результаты тестов сгруппированы по оси X в соответствии с видами тестов. Ось Y показывает время в миллисекундах, необходимое на выполнение теста. Чем показатель меньше — тем лучше.
Сравнение производительности Wasmer, Wagon и Life с помощью реализаций различных алгоритмов
Платформы Life и Wagon, в среднем, дают примерно одинаковые результаты. Wasmer же, в среднем, в 72 раза быстрее.
Важно отметить то, что Wasmer поддерживает три бэкенда: Singlepass [22], Cranelift [23] и LLVM [24]. Бэкенд, используемый по умолчанию в Go-библиотеке — это Cranelift (тут [25] можно узнать подробности о нём). Использование LLVM даст производительность, близкую к нативной, но решено было начать с Cranelift, так как это бэкенд даёт лучшее соотношение между временем компиляции и временем выполнения программы.
Тут [26] можно почитать о разных бэкендах, об их плюсах и минусах, о том, в каких ситуациях их лучше использовать.
Опенсорсный проект go-ext-wasm [10] — это новая Go-библиотека, предназначенная для выполнения бинарного wasm-кода. Она включает в себя среду выполнения Wasmer [2]. Её первая версия включает в себя API, необходимость в которых возникает чаще всего.
Проведённые испытания производительности показали, что Wasmer, в среднем, в 72 раза быстрее чем Life и Wagon.
Уважаемые читатели! Планируете ли вы пользоваться возможностями по выполнению wasm-кода в Go с использованием go-ext-wasm?
Автор: ru_vds
Источник [27]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/319797
Ссылки в тексте:
[1] Image: https://habr.com/ru/company/ruvds/blog/454518/
[2] Wasmer: https://github.com/wasmerio/wasmer
[3] Rust: https://www.rust-lang.org/
[4] C и C++: https://github.com/wasmerio/wasmer/tree/master/lib/runtime-c-api
[5] php-ext-wasm: https://github.com/wasmerio/php-ext-wasm
[6] wasmer: https://pypi.org/project/wasmer/
[7] python-ext-wasm: https://github.com/wasmerio/python-ext-wasm
[8] wasmer: https://rubygems.org/gems/wasmer
[9] ruby-ext-wasm: https://github.com/wasmerio/ruby-ext-wasm
[10] go-ext-wasm: https://github.com/wasmerio/go-ext-wasm
[11] simple.wasm: https://github.com/wasmerio/go-ext-wasm/blob/master/wasmer/test/testdata/examples/simple.wasm
[12] здесь: https://github.com/wasmerio/go-ext-wasm/blob/master/wasmer/test/testdata/examples/imported_function.wasm
[13] cgo: https://blog.golang.org/c-go-cgo
[14] Вот: https://github.com/wasmerio/go-ext-wasm/blob/6934a0fa06558f77884398a2371de182593e6a6c/wasmer/test/example_greet_test.go
[15] Life: https://github.com/perlin-network/life
[16] Wagon: https://github.com/go-interpreter/wagon
[17] материале: https://medium.com/wasmer/php-ext-wasm-migrating-from-wasmi-to-wasmer-4d1014f41c88
[18] n-body: https://benchmarksgame-team.pages.debian.net/benchmarksgame/description/nbody.html
[19] Фибоначчи: https://en.wikipedia.org/wiki/Fibonacci_number
[20] ρ-алгоритм Полларда: https://en.wikipedia.org/wiki/Pollard%27s_rho_algorithm
[21] здесь: https://github.com/wasmerio/go-ext-wasm/tree/master/benchmarks
[22] Singlepass: https://github.com/wasmerio/wasmer/tree/master/lib/singlepass-backend
[23] Cranelift: https://github.com/wasmerio/wasmer/tree/master/lib/clif-backend
[24] LLVM: https://github.com/wasmerio/wasmer/tree/master/lib/llvm-backend
[25] тут: https://github.com/CraneStation/cranelift
[26] Тут: https://medium.com/wasmer/a-webassembly-compiler-tale-9ef37aa3b537
[27] Источник: https://habr.com/ru/post/454518/?utm_campaign=454518
Нажмите здесь для печати.