Skip to content

Commit

Permalink
Lazily load types into Func (bytecodealliance#3727)
Browse files Browse the repository at this point in the history
* Lazily load types into `Func`

This commit changes the construction of a `Func` to lazily load the type
information if required instead of always loading the type information
at `Func`-construction time. The main purpose of this change is to
accelerate instantiation of instances which have many imports. Currently
in the fast way of doing this the instantiation loop looks like:

    let mut store = Store::new(&engine, ...);
    let instance = instance_pre.instantiate(&mut store);

In this situation the `instance_pre` will typically load host-defined
functions (defined via `Linker` APIs) into the `Store` as individual
`Func` items and then perform the instantiation process. The operation
of loading a `HostFunc` into a `Store` however currently involves two
expensive operations:

* First a read-only lock is taken on the `RwLock` around engine
  signatures.
* Next a clone of the wasm type is made to pull it out of the engine
  signature registry.

Neither of these is actually necessary for imported functions. The
`FuncType` for imported functions is never used since all comparisons
happen with the intern'd indices instead. The only time a `FuncType` is
used typically is for exported functions when using `Func::typed` or
similar APIs which need type information.

This commit makes this path faster by storing `Option<FuncType>` instead
of `FuncType` within a `Func`. This means that it starts out as `None`
and is only filled in on-demand as necessary. This means that when
instantiating a module with many imports no clones/locks are done.

On a simple microbenchmark where a module with 100 imports is
instantiated this PR improves instantiation time by ~35%. Due to the
rwlock used here and the general inefficiency of pthreads rwlocks the
effect is even more profound when many threads are performing the same
instantiation process. On x86_64 with 8 threads performing instantiation
this PR improves instantiation time by 80% and on arm64 it improves by
97% (wow read-contended glibc rwlocks on arm64 are slow).

Note that much of the improvement here is also from memory
allocatoins/deallocations no longer being performed because dropping
functions within a store no longer requires deallocating the `FuncType`
if it's not present.

A downside of this PR is that `Func::ty` is now unconditionally taking
an rwlock if the type hasn't already been filled in. (it uses the
engine). If this is an issue in the future though we can investigate at
that time using somthing like a `Once` to lazily fill in even when
mutable access to the store isn't available.

* Review comments
  • Loading branch information
alexcrichton committed Feb 2, 2022
1 parent 688c6b8 commit 548c153
Showing 1 changed file with 43 additions and 16 deletions.
59 changes: 43 additions & 16 deletions crates/wasmtime/src/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,12 @@ pub struct Func(Stored<FuncData>);

pub(crate) struct FuncData {
kind: FuncKind,
ty: FuncType,

// This is somewhat expensive to load from the `Engine` and in most
// optimized use cases (e.g. `TypedFunc`) it's not actually needed or it's
// only needed rarely. To handle that this is an optionally-contained field
// which is lazily loaded into as part of `Func::call`.
ty: Option<FuncType>,
}

/// The three ways that a function can be created and referenced from within a
Expand Down Expand Up @@ -677,7 +682,39 @@ impl Func {
///
/// Panics if `store` does not own this function.
pub fn ty(&self, store: impl AsContext) -> FuncType {
store.as_context()[self.0].ty.clone()
self.load_ty(&store.as_context().0)
}

/// Forcibly loads the type of this function from the `Engine`.
///
/// Note that this is a somewhat expensive method since it requires taking a
/// lock as well as cloning a type.
fn load_ty(&self, store: &StoreOpaque) -> FuncType {
FuncType::from_wasm_func_type(
store
.engine()
.signatures()
.lookup_type(self.sig_index(store.store_data()))
.expect("signature should be registered"),
)
}

/// Gets a reference to the `FuncType` for this function.
///
/// Note that this returns both a reference to the type of this function as
/// well as a reference back to the store itself. This enables using the
/// `StoreOpaque` while the `FuncType` is also being used (from the
/// perspective of the borrow-checker) because otherwise the signature would
/// consider `StoreOpaque` borrowed mutable while `FuncType` is in use.
fn ty_ref<'a>(&self, store: &'a mut StoreOpaque) -> (&'a FuncType, &'a StoreOpaque) {
// If we haven't loaded our type into the store yet then do so lazily at
// this time.
if store.store_data()[self.0].ty.is_none() {
let ty = self.load_ty(store);
store.store_data_mut()[self.0].ty = Some(ty);
}

(store.store_data()[self.0].ty.as_ref().unwrap(), store)
}

pub(crate) fn sig_index(&self, data: &StoreData) -> VMSharedSignatureIndex {
Expand Down Expand Up @@ -843,7 +880,7 @@ impl Func {
// this function. This involves checking to make sure we have the right
// number and types of arguments as well as making sure everything is
// from the same `Store`.
let ty = &store[self.0].ty;
let (ty, opaque) = self.ty_ref(store.0);
if ty.params().len() != params.len() {
bail!(
"expected {} arguments, got {}",
Expand All @@ -866,7 +903,7 @@ impl Func {
ty
);
}
if !arg.comes_from_same_store(store.0) {
if !arg.comes_from_same_store(opaque) {
bail!("cross-`Store` values are not currently supported");
}
}
Expand Down Expand Up @@ -904,7 +941,7 @@ impl Func {
}

for ((i, slot), val) in results.iter_mut().enumerate().zip(&values_vec) {
let ty = store[self.0].ty.results().nth(i).unwrap();
let ty = self.ty_ref(store.0).0.results().nth(i).unwrap();
*slot = unsafe { Val::from_raw(&mut *store, *val, ty) };
}
values_vec.truncate(0);
Expand All @@ -930,17 +967,7 @@ impl Func {
}

fn from_func_kind(kind: FuncKind, store: &mut StoreOpaque) -> Self {
// Signatures should always be registered in the engine's registry of
// shared signatures, so we should be able to unwrap safely here.
let ty = unsafe { kind.export().anyfunc.as_ref().type_index };
let ty = FuncType::from_wasm_func_type(
store
.engine()
.signatures()
.lookup_type(ty)
.expect("signature should be registered"),
);
Func(store.store_data_mut().insert(FuncData { kind, ty }))
Func(store.store_data_mut().insert(FuncData { kind, ty: None }))
}

pub(crate) fn vmimport(&self, store: &mut StoreOpaque) -> VMFunctionImport {
Expand Down

0 comments on commit 548c153

Please sign in to comment.