Как стать автором
Обновить

Комментарии 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 еще надо "допиливать".

Зарегистрируйтесь на Хабре, чтобы оставить комментарий