-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Refactor the representation of Func
#10897
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
Refactor the representation of Func
#10897
Conversation
This commit rewrites the internals of `wasmtime::Func`. The `Stored` type is no longer used meaning that it's now free to create a `wasmtime::Func` at any time. It is effectively a store-tagged `NonNull<VMFuncRef>`. This required a few internal changes to how functions are passed around: * Previously the insertion of a `wasm_call`-less `VMFuncRef` was deferred until the raw pointer was loaded. Now the insertion happens immediately as soon as the function is placed within a store. This is done to ensure that a `Func` corresponds to one exact `VMFuncRef` and that's it, and lazily filled in versions within the store are still lazily filled in but they're more eagerly allocated. This isn't expected to have much of an impact perf-wise since all these lazily allocated functions were already almost guaranteed to get lazily allocated anyway. * Filling in "holes" in `VMFuncRef`, notably the `wasm_call` field, no longer happens lazily when the `VMFuncRef` is demanded. Instead during instantiation a pass is made to fill in holes with the new module being instantiated (after registration). Additionally when a lazily-allocated `VMFuncRef` is created the module registry is checked immediately. This means that all store-local `VMFuncRef` values are either filled in immediately or filled in during instantiation. This notably means that the previous logic in `Func::vmimport` is now "just" an `.unwrap()` with a lot of comments saying why the unwrap shouldn't panic. * To implement this commit a previous optimization for the `Func` API was removed as well, namely `Func::call` will become slower after this commit. The `Func::call` API is a dynamically-typed API which requires run-time type-checking of arguments. Previously a `FuncType` was loaded into a cache once-per-`Func` which helped amortize the cost of using `Func::call` repeatedly. Now, though, there's no natural place to put such a cache since `Func` no longer has dedicated storage within a `Store`. Historically this optimization was added for the C API before the `*_call_unchecked` APIs existed, but nowadays the `*_call_unchecked` APIs should suffice for performance-critical applications where needed. In the future it might also be possible to have a hash map in the `Store` of a `VMSharedTypeIndex` to `FuncType` which is lazily populated based on calls to `Func::call`, but that feels a bit overkill nowadays for a possibly rarely-used map. * The no-longer-necessary `RootedHostFunc` type is now gone as its unsafety and various contracts are subsumed by other preexisting `unsafe` blocks. * The specific drop order between `StoreOpaque::store_data` and `StoreOpaque::rooted_host_funcs` is removed. The `rooted_host_funcs` field now lives in the `func_refs` field and the `FuncKind` type, where the destructors came from before, is no more. * The `func_refs` field has grown storage locations for a variety of "keep this thing alive as long as the store" related to functions and such. Closes bytecodealliance#10868
I'll also note that this is split out of #10870 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice!!
// Double-check that the C representation in `extern.h` matches our in-Rust | ||
// representation here in terms of size/alignment/etc. | ||
const _: () = { | ||
#[repr(C)] | ||
struct C(u64, *mut u8); | ||
assert!(core::mem::size_of::<C>() == core::mem::size_of::<Func>()); | ||
assert!(core::mem::align_of::<C>() == core::mem::align_of::<Func>()); | ||
assert!(core::mem::offset_of!(Func, store) == 0); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kind of an aside but should all of these assertions actually live in the wasmtime-c-api
crate so that we don't need to have a dummy struct which itself needs to be kept in sync with the struct that we actually care about? We've removed one syncing problem but added another, so we haven't really changed anything. I guess the only assertion that would remain in the wasmtime
crate would be the one for the offset of the store
field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I think you might be assuming there's a struct wasmtime_func_t { ... }
in Rust in crates/c-api
but that doesn't exist. In that sense this struct C
is the only C-version-written-in-Rust anywhere in the codebase, and I figured here was a good as place as any to test things out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I was indeed
/// The ways that a function can be created and referenced from within a store. | ||
enum FuncKind { | ||
/// A function already owned by the store via some other means. This is | ||
/// used, for example, when creating a `Func` from an instance's exported | ||
/// function. The instance's `InstanceHandle` is already owned by the store | ||
/// and we just have some pointers into that which represent how to call the | ||
/// function. | ||
StoreOwned { export: ExportFunction }, | ||
|
||
/// A function is shared across possibly other stores, hence the `Arc`. This | ||
/// variant happens when a `Linker`-defined function is instantiated within | ||
/// a `Store` (e.g. via `Linker::get` or similar APIs). The `Arc` here | ||
/// indicates that there's some number of other stores holding this function | ||
/// too, so dropping this may not deallocate the underlying | ||
/// `InstanceHandle`. | ||
SharedHost(Arc<HostFunc>), | ||
|
||
/// A uniquely-owned host function within a `Store`. This comes about with | ||
/// `Func::new` or similar APIs. The `HostFunc` internally owns the | ||
/// `InstanceHandle` and that will get dropped when this `HostFunc` itself | ||
/// is dropped. | ||
/// | ||
/// Note that this is intentionally placed behind a `Box` to minimize the | ||
/// size of this enum since the most common variant for high-performance | ||
/// situations is `SharedHost` and `StoreOwned`, so this ideally isn't | ||
/// larger than those two. | ||
Host(Box<HostFunc>), | ||
|
||
/// A reference to a `HostFunc`, but one that's "rooted" in the `Store` | ||
/// itself. | ||
/// | ||
/// This variant is created when an `InstancePre<T>` is instantiated in to a | ||
/// `Store<T>`. In that situation the `InstancePre<T>` already has a list of | ||
/// host functions that are packaged up in an `Arc`, so the `Arc<[T]>` is | ||
/// cloned once into the `Store` to avoid each individual function requiring | ||
/// an `Arc::clone`. | ||
/// | ||
/// The lifetime management of this type is `unsafe` because | ||
/// `RootedHostFunc` is a small wrapper around `NonNull<HostFunc>`. To be | ||
/// safe this is required that the memory of the host function is pinned | ||
/// elsewhere (e.g. the `Arc` in the `Store`). | ||
RootedHost(RootedHostFunc), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sooooooo nice to get rid of FuncKind
Co-authored-by: Nick Fitzgerald <fitzgen@gmail.com>
Co-authored-by: Nick Fitzgerald <fitzgen@gmail.com>
This commit rewrites the internals of
wasmtime::Func
. TheStored
type is no longer used meaning that it's now free to create awasmtime::Func
at any time. It is effectively a store-taggedNonNull<VMFuncRef>
. This required a few internal changes to how functions are passed around:Previously the insertion of a
wasm_call
-lessVMFuncRef
was deferred until the raw pointer was loaded. Now the insertion happens immediately as soon as the function is placed within a store. This is done to ensure that aFunc
corresponds to one exactVMFuncRef
and that's it, and lazily filled in versions within the store are still lazily filled in but they're more eagerly allocated. This isn't expected to have much of an impact perf-wise since all these lazily allocated functions were already almost guaranteed to get lazily allocated anyway.Filling in "holes" in
VMFuncRef
, notably thewasm_call
field, no longer happens lazily when theVMFuncRef
is demanded. Instead during instantiation a pass is made to fill in holes with the new module being instantiated (after registration). Additionally when a lazily-allocatedVMFuncRef
is created the module registry is checked immediately. This means that all store-localVMFuncRef
values are either filled in immediately or filled in during instantiation. This notably means that the previous logic inFunc::vmimport
is now "just" an.unwrap()
with a lot of comments saying why the unwrap shouldn't panic.To implement this commit a previous optimization for the
Func
API was removed as well, namelyFunc::call
will become slower after this commit. TheFunc::call
API is a dynamically-typed API which requires run-time type-checking of arguments. Previously aFuncType
was loaded into a cache once-per-Func
which helped amortize the cost of usingFunc::call
repeatedly. Now, though, there's no natural place to put such a cache sinceFunc
no longer has dedicated storage within aStore
. Historically this optimization was added for the C API before the*_call_unchecked
APIs existed, but nowadays the*_call_unchecked
APIs should suffice for performance-critical applications where needed. In the future it might also be possible to have a hash map in theStore
of aVMSharedTypeIndex
toFuncType
which is lazily populated based on calls toFunc::call
, but that feels a bit overkill nowadays for a possibly rarely-used map.The no-longer-necessary
RootedHostFunc
type is now gone as its unsafety and various contracts are subsumed by other preexistingunsafe
blocks.The specific drop order between
StoreOpaque::store_data
andStoreOpaque::rooted_host_funcs
is removed. Therooted_host_funcs
field now lives in thefunc_refs
field and theFuncKind
type, where the destructors came from before, is no more.The
func_refs
field has grown storage locations for a variety of "keep this thing alive as long as the store" related to functions and such.Closes #10868