Reduce contention on the global module rwlock (#4041)

* Reduce contention on the global module rwlock

This commit intendes to close #4025 by reducing contention on the global
rwlock Wasmtime has for module information during instantiation and
dropping a store. Currently registration of a module into this global
map happens during instantiation, but this can be a hot path as
embeddings may want to, in parallel, instantiate modules.

Instead this switches to a strategy of inserting into the global module
map when a `Module` is created and then removing it from the map when
the `Module` is dropped. Registration in a `Store` now preserves the
entire `Module` within the store as opposed to trying to only save it
piecemeal. In reality the only piece that wasn't saved within a store
was the `TypeTables` which was pretty inconsequential for core wasm
modules anyway.

This means that instantiation should now clone a singluar `Arc` into a
`Store` per `Module` (previously it cloned two) with zero managemnt on
the global rwlock as that happened at `Module` creation time.
Additionally dropping a `Store` again involves zero rwlock management
and only a single `Arc` drop per-instantiated module (previously it was
two).

In the process of doing this I also went ahead and removed the
`Module::new_with_name` API. This has been difficult to support
historically with various variations on the internals of `ModuleInner`
because it involves mutating a `Module` after it's been created. My hope
is that this API is pretty rarely used and/or isn't super important, so
it's ok to remove.

Finally this change removes some internal `Arc` layerings that are no
longer necessary, attempting to use either `T` or `&T` where possible
without dealing with the overhead of an `Arc`.

Closes #4025

* Move back to a `BTreeMap` in `ModuleRegistry`
This commit is contained in:
Alex Crichton
2022-04-19 15:13:47 -05:00
committed by GitHub
parent 3394c2bb91
commit 90791a0e32
7 changed files with 135 additions and 159 deletions

View File

@@ -398,7 +398,7 @@ impl CompiledModule {
info: Option<CompiledModuleInfo>, info: Option<CompiledModuleInfo>,
profiler: &dyn ProfilingAgent, profiler: &dyn ProfilingAgent,
id_allocator: &CompiledModuleIdAllocator, id_allocator: &CompiledModuleIdAllocator,
) -> Result<Arc<Self>> { ) -> Result<Self> {
// Transfer ownership of `obj` to a `CodeMemory` object which will // Transfer ownership of `obj` to a `CodeMemory` object which will
// manage permissions, such as the executable bit. Once it's located // manage permissions, such as the executable bit. Once it's located
// there we also publish it for being able to execute. Note that this // there we also publish it for being able to execute. Note that this
@@ -454,7 +454,7 @@ impl CompiledModule {
}; };
ret.register_debug_and_profiling(profiler)?; ret.register_debug_and_profiling(profiler)?;
Ok(Arc::new(ret)) Ok(ret)
} }
fn register_debug_and_profiling(&mut self, profiler: &dyn ProfilingAgent) -> Result<()> { fn register_debug_and_profiling(&mut self, profiler: &dyn ProfilingAgent) -> Result<()> {

View File

@@ -99,6 +99,7 @@
//! Examination of Deferred Reference Counting and Cycle Detection* by Quinane: //! Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
//! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf> //! <https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf>
use std::alloc::Layout;
use std::any::Any; use std::any::Any;
use std::cell::UnsafeCell; use std::cell::UnsafeCell;
use std::cmp; use std::cmp;
@@ -108,7 +109,6 @@ use std::mem;
use std::ops::Deref; use std::ops::Deref;
use std::ptr::{self, NonNull}; use std::ptr::{self, NonNull};
use std::sync::atomic::{self, AtomicUsize, Ordering}; use std::sync::atomic::{self, AtomicUsize, Ordering};
use std::{alloc::Layout, sync::Arc};
use wasmtime_environ::StackMap; use wasmtime_environ::StackMap;
/// An external reference to some opaque data. /// An external reference to some opaque data.
@@ -814,7 +814,7 @@ impl VMExternRefActivationsTable {
/// program counter value. /// program counter value.
pub trait ModuleInfoLookup { pub trait ModuleInfoLookup {
/// Lookup the module information from a program counter value. /// Lookup the module information from a program counter value.
fn lookup(&self, pc: usize) -> Option<Arc<dyn ModuleInfo>>; fn lookup(&self, pc: usize) -> Option<&dyn ModuleInfo>;
} }
/// Used by the runtime to query module information. /// Used by the runtime to query module information.

View File

@@ -105,9 +105,9 @@ struct ModuleInner {
/// executed. /// executed.
module: Arc<CompiledModule>, module: Arc<CompiledModule>,
/// Type information of this module. /// Type information of this module.
types: Arc<TypeTables>, types: TypeTables,
/// Registered shared signature for the module. /// Registered shared signature for the module.
signatures: Arc<SignatureCollection>, signatures: SignatureCollection,
/// A set of initialization images for memories, if any. /// A set of initialization images for memories, if any.
/// ///
/// Note that this is behind a `OnceCell` to lazily create this image. On /// Note that this is behind a `OnceCell` to lazily create this image. On
@@ -193,22 +193,6 @@ impl Module {
Self::from_binary(engine, &bytes) Self::from_binary(engine, &bytes)
} }
/// Creates a new WebAssembly `Module` from the given in-memory `binary`
/// data. The provided `name` will be used in traps/backtrace details.
///
/// See [`Module::new`] for other details.
#[cfg(compiler)]
#[cfg_attr(nightlydoc, doc(cfg(feature = "cranelift")))] // see build.rs
pub fn new_with_name(engine: &Engine, bytes: impl AsRef<[u8]>, name: &str) -> Result<Module> {
let mut module = Self::new(engine, bytes.as_ref())?;
Arc::get_mut(&mut Arc::get_mut(&mut module.inner).unwrap().module)
.unwrap()
.module_mut()
.expect("mutable module")
.name = Some(name.to_string());
Ok(module)
}
/// Creates a new WebAssembly `Module` from the contents of the given /// Creates a new WebAssembly `Module` from the contents of the given
/// `file` on disk. /// `file` on disk.
/// ///
@@ -332,7 +316,7 @@ impl Module {
} }
}; };
Self::from_parts(engine, mmap, info, Arc::new(types)) Self::from_parts(engine, mmap, info, types)
} }
/// Converts an input binary-encoded WebAssembly module to compilation /// Converts an input binary-encoded WebAssembly module to compilation
@@ -487,23 +471,29 @@ impl Module {
engine: &Engine, engine: &Engine,
mmap: MmapVec, mmap: MmapVec,
info: Option<CompiledModuleInfo>, info: Option<CompiledModuleInfo>,
types: Arc<TypeTables>, types: TypeTables,
) -> Result<Self> { ) -> Result<Self> {
let module = CompiledModule::from_artifacts( let module = Arc::new(CompiledModule::from_artifacts(
mmap, mmap,
info, info,
&*engine.config().profiler, &*engine.config().profiler,
engine.unique_id_allocator(), engine.unique_id_allocator(),
)?; )?);
// Validate the module can be used with the current allocator // Validate the module can be used with the current allocator
engine.allocator().validate(module.module())?; engine.allocator().validate(module.module())?;
let signatures = Arc::new(SignatureCollection::new_for_module( let signatures = SignatureCollection::new_for_module(
engine.signatures(), engine.signatures(),
&types, &types,
module.trampolines().map(|(idx, f, _)| (idx, f)), module.trampolines().map(|(idx, f, _)| (idx, f)),
)); );
// We're about to create a `Module` for real now so enter this module
// into the global registry of modules so we can resolve traps
// appropriately. Note that the corresponding `unregister` happens below
// in `Drop for ModuleInner`.
registry::register(engine, &module);
Ok(Self { Ok(Self {
inner: Arc::new(ModuleInner { inner: Arc::new(ModuleInner {
@@ -564,7 +554,7 @@ impl Module {
SerializedModule::new(self).to_bytes(&self.inner.engine.config().module_version) SerializedModule::new(self).to_bytes(&self.inner.engine.config().module_version)
} }
pub(crate) fn compiled_module(&self) -> &Arc<CompiledModule> { pub(crate) fn compiled_module(&self) -> &CompiledModule {
&self.inner.module &self.inner.module
} }
@@ -572,11 +562,11 @@ impl Module {
self.compiled_module().module() self.compiled_module().module()
} }
pub(crate) fn types(&self) -> &Arc<TypeTables> { pub(crate) fn types(&self) -> &TypeTables {
&self.inner.types &self.inner.types
} }
pub(crate) fn signatures(&self) -> &Arc<SignatureCollection> { pub(crate) fn signatures(&self) -> &SignatureCollection {
&self.inner.signatures &self.inner.signatures
} }
@@ -599,8 +589,6 @@ impl Module {
/// let module = Module::new(&engine, "(module)")?; /// let module = Module::new(&engine, "(module)")?;
/// assert_eq!(module.name(), None); /// assert_eq!(module.name(), None);
/// ///
/// let module = Module::new_with_name(&engine, "(module)", "bar")?;
/// assert_eq!(module.name(), Some("bar"));
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -798,6 +786,10 @@ impl Module {
self.inner.clone() self.inner.clone()
} }
pub(crate) fn module_info(&self) -> &dyn wasmtime_runtime::ModuleInfo {
&*self.inner
}
/// Returns the range of bytes in memory where this module's compilation /// Returns the range of bytes in memory where this module's compilation
/// image resides. /// image resides.
/// ///
@@ -917,6 +909,38 @@ impl wasmtime_runtime::ModuleRuntimeInfo for ModuleInner {
} }
} }
impl wasmtime_runtime::ModuleInfo for ModuleInner {
fn lookup_stack_map(&self, pc: usize) -> Option<&wasmtime_environ::StackMap> {
let text_offset = pc - self.module.code().as_ptr() as usize;
let (index, func_offset) = self.module.func_by_text_offset(text_offset)?;
let info = self.module.func_info(index);
// Do a binary search to find the stack map for the given offset.
let index = match info
.stack_maps
.binary_search_by_key(&func_offset, |i| i.code_offset)
{
// Found it.
Ok(i) => i,
// No stack map associated with this PC.
//
// Because we know we are in Wasm code, and we must be at some kind
// of call/safepoint, then the Cranelift backend must have avoided
// emitting a stack map for this location because no refs were live.
Err(_) => return None,
};
Some(&info.stack_maps[index].stack_map)
}
}
impl Drop for ModuleInner {
fn drop(&mut self) {
registry::unregister(&self.module);
}
}
/// A barebones implementation of ModuleRuntimeInfo that is useful for /// A barebones implementation of ModuleRuntimeInfo that is useful for
/// cases where a purpose-built environ::Module is used and a full /// cases where a purpose-built environ::Module is used and a full
/// CompiledModule does not exist (for example, for tests or for the /// CompiledModule does not exist (for example, for tests or for the

View File

@@ -1,11 +1,11 @@
//! Implements a registry of modules for a store. //! Implements a registry of modules for a store.
use crate::{signatures::SignatureCollection, Module}; use crate::{Engine, Module};
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use wasmtime_environ::{EntityRef, FilePos, StackMap, TrapCode}; use wasmtime_environ::{EntityRef, FilePos, TrapCode};
use wasmtime_jit::CompiledModule; use wasmtime_jit::CompiledModule;
use wasmtime_runtime::{ModuleInfo, VMCallerCheckedAnyfunc, VMTrampoline}; use wasmtime_runtime::{ModuleInfo, VMCallerCheckedAnyfunc, VMTrampoline};
@@ -15,30 +15,38 @@ lazy_static::lazy_static! {
/// Used for registering modules with a store. /// Used for registering modules with a store.
/// ///
/// The map is from the ending (exclusive) address for the module code to /// Note that the primary reason for this registry is to ensure that everything
/// the registered module. /// in `Module` is kept alive for the duration of a `Store`. At this time we
/// /// need "basically everything" within a `Moudle` to stay alive once it's
/// The `BTreeMap` is used to quickly locate a module based on a program counter value. /// instantiated within a store. While there's some smaller portions that could
/// theoretically be omitted as they're not needed by the store they're
/// currently small enough to not worry much about.
#[derive(Default)] #[derive(Default)]
pub struct ModuleRegistry { pub struct ModuleRegistry {
modules_with_code: BTreeMap<usize, Arc<RegisteredModule>>, // Keyed by the end address of the module's code in memory.
modules_without_code: Vec<Arc<CompiledModule>>, modules_with_code: BTreeMap<usize, Module>,
// Preserved for keeping data segments alive or similar
modules_without_code: Vec<Module>,
}
fn start(module: &Module) -> usize {
assert!(!module.compiled_module().code().is_empty());
module.compiled_module().code().as_ptr() as usize
} }
impl ModuleRegistry { impl ModuleRegistry {
/// Fetches information about a registered module given a program counter value. /// Fetches information about a registered module given a program counter value.
pub fn lookup_module(&self, pc: usize) -> Option<Arc<dyn ModuleInfo>> { pub fn lookup_module(&self, pc: usize) -> Option<&dyn ModuleInfo> {
self.module(pc) self.module(pc).map(|m| m.module_info())
.map(|m| -> Arc<dyn ModuleInfo> { m.clone() })
} }
fn module(&self, pc: usize) -> Option<&Arc<RegisteredModule>> { fn module(&self, pc: usize) -> Option<&Module> {
let (end, info) = self.modules_with_code.range(pc..).next()?; let (end, module) = self.modules_with_code.range(pc..).next()?;
if pc < info.start || *end < pc { if pc < start(module) || *end < pc {
return None; return None;
} }
Some(module)
Some(info)
} }
/// Registers a new module with the registry. /// Registers a new module with the registry.
@@ -52,92 +60,40 @@ impl ModuleRegistry {
// module in the future. For that reason we continue to register empty // module in the future. For that reason we continue to register empty
// modules and retain them. // modules and retain them.
if compiled_module.finished_functions().len() == 0 { if compiled_module.finished_functions().len() == 0 {
self.modules_without_code.push(compiled_module.clone()); self.modules_without_code.push(module.clone());
return; return;
} }
// The module code range is exclusive for end, so make it inclusive as it // The module code range is exclusive for end, so make it inclusive as it
// may be a valid PC value // may be a valid PC value
let code = compiled_module.code(); let start_addr = start(module);
assert!(!code.is_empty()); let end_addr = start_addr + compiled_module.code().len() - 1;
let start = code.as_ptr() as usize;
let end = start + code.len() - 1;
// Ensure the module isn't already present in the registry // Ensure the module isn't already present in the registry
// This is expected when a module is instantiated multiple times in the // This is expected when a module is instantiated multiple times in the
// same store // same store
if let Some(m) = self.modules_with_code.get(&end) { if let Some(m) = self.modules_with_code.get(&end_addr) {
assert_eq!(m.start, start); assert_eq!(start(m), start_addr);
return; return;
} }
// Assert that this module's code doesn't collide with any other registered modules // Assert that this module's code doesn't collide with any other
if let Some((_, prev)) = self.modules_with_code.range(end..).next() { // registered modules
assert!(prev.start > end); if let Some((_, prev)) = self.modules_with_code.range(end_addr..).next() {
assert!(start(prev) > end_addr);
}
if let Some((prev_end, _)) = self.modules_with_code.range(..=start_addr).next_back() {
assert!(*prev_end < start_addr);
} }
if let Some((prev_end, _)) = self.modules_with_code.range(..=start).next_back() { let prev = self.modules_with_code.insert(end_addr, module.clone());
assert!(*prev_end < start);
}
let prev = self.modules_with_code.insert(
end,
Arc::new(RegisteredModule {
start,
module: compiled_module.clone(),
signatures: module.signatures().clone(),
}),
);
assert!(prev.is_none()); assert!(prev.is_none());
GLOBAL_MODULES.write().unwrap().register(start, end, module);
} }
/// Looks up a trampoline from an anyfunc. /// Looks up a trampoline from an anyfunc.
pub fn lookup_trampoline(&self, anyfunc: &VMCallerCheckedAnyfunc) -> Option<VMTrampoline> { pub fn lookup_trampoline(&self, anyfunc: &VMCallerCheckedAnyfunc) -> Option<VMTrampoline> {
let module = self.module(anyfunc.func_ptr.as_ptr() as usize)?; let module = self.module(anyfunc.func_ptr.as_ptr() as usize)?;
module.signatures.trampoline(anyfunc.type_index) module.signatures().trampoline(anyfunc.type_index)
}
}
impl Drop for ModuleRegistry {
fn drop(&mut self) {
let mut info = GLOBAL_MODULES.write().unwrap();
for end in self.modules_with_code.keys() {
info.unregister(*end);
}
}
}
struct RegisteredModule {
start: usize,
module: Arc<CompiledModule>,
signatures: Arc<SignatureCollection>,
}
impl ModuleInfo for RegisteredModule {
fn lookup_stack_map(&self, pc: usize) -> Option<&StackMap> {
let text_offset = pc - self.start;
let (index, func_offset) = self.module.func_by_text_offset(text_offset)?;
let info = self.module.func_info(index);
// Do a binary search to find the stack map for the given offset.
let index = match info
.stack_maps
.binary_search_by_key(&func_offset, |i| i.code_offset)
{
// Found it.
Ok(i) => i,
// No stack map associated with this PC.
//
// Because we know we are in Wasm code, and we must be at some kind
// of call/safepoint, then the Cranelift backend must have avoided
// emitting a stack map for this location because no refs were live.
Err(_) => return None,
};
Some(&info.stack_maps[index].stack_map)
} }
} }
@@ -146,11 +102,6 @@ struct GlobalRegisteredModule {
start: usize, start: usize,
module: Arc<CompiledModule>, module: Arc<CompiledModule>,
wasm_backtrace_details_env_used: bool, wasm_backtrace_details_env_used: bool,
/// Note that modules can be instantiated in many stores, so the purpose of
/// this field is to keep track of how many stores have registered a
/// module. Information is only removed from the global registry when this
/// reference count reaches 0.
references: usize,
} }
/// This is the global module registry that stores information for all modules /// This is the global module registry that stores information for all modules
@@ -173,14 +124,12 @@ impl GlobalModuleRegistry {
/// Returns whether the `pc`, according to globally registered information, /// Returns whether the `pc`, according to globally registered information,
/// is a wasm trap or not. /// is a wasm trap or not.
pub(crate) fn is_wasm_trap_pc(pc: usize) -> bool { pub(crate) fn is_wasm_trap_pc(pc: usize) -> bool {
let modules = GLOBAL_MODULES.read().unwrap(); let (module, text_offset) = match GLOBAL_MODULES.read().unwrap().module(pc) {
Some((module, offset)) => (module.module.clone(), offset),
None => return false,
};
match modules.module(pc) { wasmtime_environ::lookup_trap_code(module.trap_data(), text_offset).is_some()
Some((entry, text_offset)) => {
wasmtime_environ::lookup_trap_code(entry.module.trap_data(), text_offset).is_some()
}
None => false,
}
} }
/// Returns, if found, the corresponding module for the `pc` as well as the /// Returns, if found, the corresponding module for the `pc` as well as the
@@ -223,36 +172,43 @@ impl GlobalModuleRegistry {
let (module, offset) = self.module(pc)?; let (module, offset) = self.module(pc)?;
wasmtime_environ::lookup_trap_code(module.module.trap_data(), offset) wasmtime_environ::lookup_trap_code(module.module.trap_data(), offset)
} }
}
/// Registers a new region of code, described by `(start, end)` and with /// Registers a new region of code.
/// the given function information, with the global information. ///
fn register(&mut self, start: usize, end: usize, module: &Module) { /// Must not have been previously registered and must be `unregister`'d to
let info = self.0.entry(end).or_insert_with(|| GlobalRegisteredModule { /// prevent leaking memory.
start, ///
module: module.compiled_module().clone(), /// This is required to enable traps to work correctly since the signal handler
wasm_backtrace_details_env_used: module /// will lookup in the `GLOBAL_MODULES` list to determine which a particular pc
.engine() /// is a trap or not.
.config() pub fn register(engine: &Engine, module: &Arc<CompiledModule>) {
.wasm_backtrace_details_env_used, let code = module.code();
references: 0, if code.is_empty() {
}); return;
// Note that ideally we'd debug_assert that the information previously
// stored, if any, matches the `functions` we were given, but for now we
// just do some simple checks to hope it's the same.
assert_eq!(info.start, start);
info.references += 1;
} }
let start = code.as_ptr() as usize;
let end = start + code.len() - 1;
let module = GlobalRegisteredModule {
start,
wasm_backtrace_details_env_used: engine.config().wasm_backtrace_details_env_used,
module: module.clone(),
};
let prev = GLOBAL_MODULES.write().unwrap().0.insert(end, module);
assert!(prev.is_none());
}
/// Unregisters a region of code (keyed by the `end` address) from the /// Unregisters a module from the global map.
/// global information. ///
fn unregister(&mut self, end: usize) { /// Must hae been previously registered with `register`.
let info = self.0.get_mut(&end).unwrap(); pub fn unregister(module: &Arc<CompiledModule>) {
info.references -= 1; let code = module.code();
if info.references == 0 { if code.is_empty() {
self.0.remove(&end); return;
}
} }
let end = (code.as_ptr() as usize) + code.len() - 1;
let module = GLOBAL_MODULES.write().unwrap().0.remove(&end);
assert!(module.is_some());
} }
impl GlobalRegisteredModule { impl GlobalRegisteredModule {

View File

@@ -47,7 +47,6 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
use wasmtime_environ::{FlagValue, Tunables, TypeTables}; use wasmtime_environ::{FlagValue, Tunables, TypeTables};
use wasmtime_jit::{subslice_range, CompiledModuleInfo}; use wasmtime_jit::{subslice_range, CompiledModuleInfo};
use wasmtime_runtime::MmapVec; use wasmtime_runtime::MmapVec;
@@ -206,7 +205,7 @@ impl<'a> SerializedModule<'a> {
pub fn into_module(self, engine: &Engine) -> Result<Module> { pub fn into_module(self, engine: &Engine) -> Result<Module> {
let (mmap, info, types) = self.into_parts(engine)?; let (mmap, info, types) = self.into_parts(engine)?;
Module::from_parts(engine, mmap, info, Arc::new(types)) Module::from_parts(engine, mmap, info, types)
} }
pub fn into_parts( pub fn into_parts(

View File

@@ -1960,7 +1960,7 @@ impl Drop for StoreOpaque {
} }
impl wasmtime_runtime::ModuleInfoLookup for ModuleRegistry { impl wasmtime_runtime::ModuleInfoLookup for ModuleRegistry {
fn lookup(&self, pc: usize) -> Option<Arc<dyn ModuleInfo>> { fn lookup(&self, pc: usize) -> Option<&dyn ModuleInfo> {
self.lookup_module(pc) self.lookup_module(pc)
} }
} }

View File

@@ -27,8 +27,5 @@ fn test_module_name() -> anyhow::Result<()> {
let module = Module::new(&engine, wat)?; let module = Module::new(&engine, wat)?;
assert_eq!(module.name(), Some("from_name_section")); assert_eq!(module.name(), Some("from_name_section"));
let module = Module::new_with_name(&engine, wat, "override")?;
assert_eq!(module.name(), Some("override"));
Ok(()) Ok(())
} }