From 548c1530d77b5bbb077f45e0bfa9e65c70affdd7 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 26 Jan 2022 14:23:37 -0600 Subject: [PATCH] Lazily load types into `Func` (#3727) * 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` 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 --- crates/wasmtime/src/func.rs | 59 +++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/crates/wasmtime/src/func.rs b/crates/wasmtime/src/func.rs index 3f4b2ad496d9..023cbdac4640 100644 --- a/crates/wasmtime/src/func.rs +++ b/crates/wasmtime/src/func.rs @@ -183,7 +183,12 @@ pub struct Func(Stored); 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, } /// The three ways that a function can be created and referenced from within a @@ -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 { @@ -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 {}", @@ -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"); } } @@ -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); @@ -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 {