Skip to content

FIXES #137: make calling wasm from python 7x faster #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c724ce1
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 25, 2023
d0dcca2
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 25, 2023
52a0f1d
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 25, 2023
da545aa
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 26, 2023
68fa2a8
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 26, 2023
f00d32a
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 26, 2023
7bee159
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 27, 2023
d0913e8
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 27, 2023
d383a05
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Mar 28, 2023
383cad6
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 2, 2023
dc3d697
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 2, 2023
1446d79
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 4, 2023
1ca73e9
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 4, 2023
e6c054b
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 4, 2023
8bc29e6
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 4, 2023
37b6067
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 4, 2023
2b1d2c0
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
8417fba
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
5bb5c39
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
a67ecc0
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
0388b16
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
4cd48d3
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
9285e6c
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
818bb8d
FIXES #137: make calling wasm from python 7x faster
muayyad-alsadi Apr 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 56 additions & 33 deletions wasmtime/_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Callable, Optional, Generic, TypeVar, List, Union, Tuple, cast as cast_type, Sequence
from ._exportable import AsExtern
from ._store import Storelike

from ._bindings import wasmtime_val_raw_t

T = TypeVar('T')
FUNCTIONS: "Slab[Tuple]"
Expand All @@ -16,6 +16,13 @@

class Func:
_func: ffi.wasmtime_func_t
_ty: FuncType
_params_n: int
_results_n: int
_params_str: list[str]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it still performant to store strings here? I would expect naively that storing the integer kinds would be more efficient since then checking for funcref/externref would be integer comparisons instead of string comparisons.

_results_str: list[str]
_results_str0: str
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be best stored as Optional[str] to avoid having a dummy value listed for empty results.

_vals_raw_type: ctypes.Array[wasmtime_val_raw_t]

def __init__(self, store: Storelike, ty: FuncType, func: Callable, access_caller: bool = False):
"""
Expand All @@ -27,11 +34,11 @@ def __init__(self, store: Storelike, ty: FuncType, func: Callable, access_caller
set to `True` then the first argument given to `func` is an instance of
type `Caller` below.
"""

if not isinstance(store, Store):
raise TypeError("expected a Store")
if not isinstance(ty, FuncType):
raise TypeError("expected a FuncType")
self._init_call(ty)
idx = FUNCTIONS.allocate((func, ty.results, access_caller))
_func = ffi.wasmtime_func_t()
ffi.wasmtime_func_new(
Expand All @@ -56,6 +63,36 @@ def type(self, store: Storelike) -> FuncType:
ptr = ffi.wasmtime_func_type(store._context, byref(self._func))
return FuncType._from_ptr(ptr, None)

def _create_raw_vals(self, *params: IntoVal) -> ctypes.Array[wasmtime_val_raw_t]:
raw = self._vals_raw_type()
for i, param_str in enumerate(self._params_str):
setattr(raw[i], param_str, params[i])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that this conversion is quite correct. The IntoVal type could also be Val, for example, to handle things like v128/funcref/externref/etc. In such a case the raw attribute of raw[i] can't be set to that value, and I'm not sure what happens otherwise during such a process.

Additionally it's not safe to simply set the externref and funcref fields. Those need to go through the methods in the Val type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For an example you can see the logic in Value._convert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea about externref and funcref

the code related to v128 says it's not implemented

        elif kind == WASMTIME_V128.value:
            raise Exception("unimplemented v128 type")

but no problem I've just pushed an example and make sure it works

https://github.com/muayyad-alsadi/wasmtime-py/blob/wip-speed-call/examples/simd_i8x16.py#L15

would you please give me examples about externref and funcref

return raw

def _extract_return(self, vals_raw: ctypes.Array[wasmtime_val_raw_t]) -> Union[IntoVal, Sequence[IntoVal], None]:
if self._results_n==0:
return None
if self._results_n==1:
return getattr(vals_raw[0], self._results_str0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not sufficient to simply call getattr here, namely for the funcref and externref types. You can see what the logic in Value._value does, for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to move all the logic in the constructor (or in _init_call) so that this logic is run once the module is loaded not on every single call. and hence the speed boost.

I need a minimal reproducible example about the two missing types, in a way similar to my SIMD example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the example it's test_refs.py I'll look into it and make sure it works

# we can use tuple construct, but I'm using list for compatability
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment can be removed. Either the result should continue to be a list in which case there's no need for the comment, or the result should change to a tuple, in which case there's no need for the comment.

return [getattr(val_raw, ret_str) for val_raw, ret_str in zip(vals_raw, self._results_str)]

def _init_call(self, ty):
"""init signature properties used by call"""
self._ty = ty
ty_params = ty.params
ty_results = ty.results
params_n = len(ty_params)
results_n = len(ty_results)
self._params_str = (str(i) for i in ty_params)
self._results_str = (str(i) for i in ty_results)
self._results_str0 = str(ty_results[0]) if results_n else None
self._params_n = params_n
self._results_n = results_n
n = max(params_n, results_n)
self._vals_raw_type = wasmtime_val_raw_t*n


def __call__(self, store: Storelike, *params: IntoVal) -> Union[IntoVal, Sequence[IntoVal], None]:
"""
Calls this function with the given parameters
Expand All @@ -70,45 +107,31 @@ def __call__(self, store: Storelike, *params: IntoVal) -> Union[IntoVal, Sequenc
Note that you can also use the `__call__` method and invoke a `Func` as
if it were a function directly.
"""

ty = self.type(store)
param_tys = ty.params
if len(params) > len(param_tys):
if getattr(self, "_ty", None) is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of falling back to getattr can the mypy-listed-type of self._ty be Optional?

self._init_call(self.type(store))
params_n = len(params)
if params_n > self._params_n:
raise WasmtimeError("too many parameters provided: given %s, expected %s" %
(len(params), len(param_tys)))
if len(params) < len(param_tys):
(params_n, self._params_n))
if params_n < self._params_n:
raise WasmtimeError("too few parameters provided: given %s, expected %s" %
(len(params), len(param_tys)))

param_vals = [Val._convert(ty, params[i]) for i, ty in enumerate(param_tys)]
params_ptr = (ffi.wasmtime_val_t * len(params))()
for i, val in enumerate(param_vals):
params_ptr[i] = val._unwrap_raw()

result_tys = ty.results
results_ptr = (ffi.wasmtime_val_t * len(result_tys))()

(params_n, self._params_n))
vals_raw = self._create_raw_vals(*params)
vals_raw_ptr = ctypes.cast(vals_raw, ctypes.POINTER(wasmtime_val_raw_t))
# according to https://docs.wasmtime.dev/c-api/func_8h.html#a3b54596199641a8647a7cd89f322966f
# it's safe to call wasmtime_func_call_unchecked because
# - we allocate enough space to hold all the parameters and all the results
# - we set proper types by reading types from ty
# - but not sure about "Values such as externref and funcref are valid within the store being called"
with enter_wasm(store) as trap:
error = ffi.wasmtime_func_call(
error = ffi.wasmtime_func_call_unchecked(
store._context,
byref(self._func),
params_ptr,
len(params),
results_ptr,
len(result_tys),
vals_raw_ptr,
trap)
if error:
raise WasmtimeError._from_ptr(error)

results = []
for i in range(0, len(result_tys)):
results.append(Val(results_ptr[i]).value)
if len(results) == 0:
return None
elif len(results) == 1:
return results[0]
else:
return results
return self._extract_return(vals_raw)

def _as_extern(self) -> ffi.wasmtime_extern_t:
union = ffi.wasmtime_extern_union(func=self._func)
Expand Down