Comments 3
Получаем 1 микросекунду, что на порядок больше чем обычный вызов CGO:
И CGO еще медленный. В итоге проигрыш на три порядка, по сравнению с вызовом Go-функции.
Можно было бы посетовать на медленные "шлюзы", соединяющие Go с внешним миром, но я как-то пробовал wasmtime 0.19.0 прямо в Rust — так ведь даже и там не быстро.
Вот такой код:
use anyhow::Result;
use wasmtime::*;
use std::time::{Instant};
fn main() -> Result<()> {
// All wasm objects operate within the context of a "store"
let store = Store::default();
// Modules can be compiled through either the text or binary format
let wat = r#"
(module
(import "" "" (func $host_hello (param i32)))
(func (export "hello")
i32.const 3
call $host_hello)
)
"#;
let module = Module::new(store.engine(), wat)?;
// Host functions can be defined which take/return wasm values and
// execute arbitrary code on the host.
let host_hello = Func::wrap(&store, |_param: i32| {
});
// Instantiation of a module requires specifying its imports and then
// afterwards we can fetch exports by name, as well as asserting the
// type signature of the function with `get0`.
let instance = Instance::new(&store, &module, &[host_hello.into()])?;
let hello = instance
.get_func("hello")
.ok_or(anyhow::format_err!("failed to find `hello` function export"))?
.get0::<()>()?;
// And finally we can call the wasm as if it were a Rust function!
let now = Instant::now();
let num_of_calls = 1000000;
for _ in 0..num_of_calls{
hello()?;
}
println!("Calls per millisecond: {}", num_of_calls / (now.elapsed().as_millis()));
Ok(())
}
… выводит Calls per millisecond: 613, ну т.е. все равно на несколько порядков медленнее, чем native calls.
Mozilla (а wastime вроде бы они делают), однако, утверждает, что снизила затраты на вызов до 5 ns.
wasm3 весьма интересная штука, кстати. Использует tail-call оптимизацию компилятора С и по производительности находится между интерпретаторами и всякими AOT/JIT.
В свое время немного исследовал вопрос производительности. Просто вызов:
Benchmark_wasm_justCall_life_NoAOT-2 12847582 89.7 ns/op
Benchmark_wasm_justCall_velo-2 190448425 6.15 ns/op
Benchmark_wasm_justCall_wasm3-2 13073806 88.6 ns/op
Benchmark_wasm_justCall_wasmer-2 1958692 591 ns/op
Benchmark_wasm_justCall_wasmtime-2 796339 1614 ns/op
velo
— это мой набросок велосипедного интерпретатора WASM, реализовал несколько команд.
Рекурсивный расчет чисел Фибонначи до 20:
Benchmark_wasm_fib20_life_NoAOT-2 201 5866070 ns/op
Benchmark_wasm_fib20_native-2 22645 51889 ns/op
Benchmark_wasm_fib20_velo-2 1212 1001759 ns/op
Benchmark_wasm_fib20_velo_reg-2 1362 874222 ns/op
Benchmark_wasm_fib20_wasm3-2 2696 423079 ns/op
Benchmark_wasm_fib20_wasmer-2 20647 58302 ns/op
Benchmark_wasm_fib20_wasmtime-2 14611 78656 ns/op
velo_reg
— это моя попытка заставить велосипед работать через, условно, "регистры", а не через стек.
wasmer
и wasmtime
вплотную подходят к native
, wasm3
на порядок уступает native Go, но и на порядок быстрее интерпретатора life.
В общем, затраты на вызов go -> wasm "из коробки" просто чудовищны и ощутимо ограничивают область применения.
В общем, затраты на вызов go -> wasm "из коробки" просто чудовищны и ощутимо ограничивают область применения.
Это да. Все более менее живые рантаймы написаны на Rust/C++. Мы сталкиваемся с CGO, а тут бывает нужно копировать память. Как ни крути, дёшево не будет.
Помимо CGO там еще есть замедлители.
Передача параметров через тип interface{} а потом интроспекция настоящих типов с перезаписью куда-то в wasm — ну как тут будет быстро.
Я у себя в "велосипеде" параметры прямо в стек виртуальной машины писал:
vm := NewVeloVM()
f := genFibFunc()
f.FE = VeloInterpreterGas
vm.FDs = []*FuncDescr{f}
sp := len(vm.Stack)
sp--
// fib(0)
{
vm.Stack[sp].I32 = 0
res := VeloInterpreter(vm, f, sp)
assert.Equal(t, int32(0), vm.Stack[res].I32)
}
Соответственно, скорость вызова функции была близка к натуральной. В том проекте, куда я это прицеливал, такой вызов был вполне приемлем. Ну, или можно было бы нагенерировать proxy в нужном количестве.
Для Go есть еще попытка в JIT. Работает, пока в стиле CgoABI все генерировать и вызывать. С GoABI уже проблемы, если нужны множественные переходы границ миров, типа Go -> Wasm -> Go.
В общем, использование Wasm из Go еще надо "допиливать".
Запускаем AssemblyScript в Go