externref: implement stack map-based garbage collection
For host VM code, we use plain reference counting, where cloning increments
the reference count, and dropping decrements it. We can avoid many of the
on-stack increment/decrement operations that typically plague the
performance of reference counting via Rust's ownership and borrowing system.
Moving a `VMExternRef` avoids mutating its reference count, and borrowing it
either avoids the reference count increment or delays it until if/when the
`VMExternRef` is cloned.
When passing a `VMExternRef` into compiled Wasm code, we don't want to do
reference count mutations for every compiled `local.{get,set}`, nor for
every function call. Therefore, we use a variation of **deferred reference
counting**, where we only mutate reference counts when storing
`VMExternRef`s somewhere that outlives the activation: into a global or
table. Simultaneously, we over-approximate the set of `VMExternRef`s that
are inside Wasm function activations. Periodically, we walk the stack at GC
safe points, and use stack map information to precisely identify the set of
`VMExternRef`s inside Wasm activations. Then we take the difference between
this precise set and our over-approximation, and decrement the reference
count for each of the `VMExternRef`s that are in our over-approximation but
not in the precise set. Finally, the over-approximation is replaced with the
precise set.
The `VMExternRefActivationsTable` implements the over-approximized set of
`VMExternRef`s referenced by Wasm activations. Calling a Wasm function and
passing it a `VMExternRef` moves the `VMExternRef` into the table, and the
compiled Wasm function logically "borrows" the `VMExternRef` from the
table. Similarly, `global.get` and `table.get` operations clone the gotten
`VMExternRef` into the `VMExternRefActivationsTable` and then "borrow" the
reference out of the table.
When a `VMExternRef` is returned to host code from a Wasm function, the host
increments the reference count (because the reference is logically
"borrowed" from the `VMExternRefActivationsTable` and the reference count
from the table will be dropped at the next GC).
For more general information on deferred reference counting, see *An
Examination of Deferred Reference Counting and Cycle Detection* by Quinane:
https://openresearch-repository.anu.edu.au/bitstream/1885/42030/2/hon-thesis.pdf
cc #929
Fixes #1804
This commit is contained in:
@@ -265,14 +265,14 @@ impl Func {
|
||||
// values produced are correct. There could be a bug in `func` that
|
||||
// produces the wrong number or wrong types of values, and we need
|
||||
// to catch that here.
|
||||
for (i, (ret, ty)) in returns.iter_mut().zip(ty_clone.results()).enumerate() {
|
||||
for (i, (ret, ty)) in returns.into_iter().zip(ty_clone.results()).enumerate() {
|
||||
if ret.ty() != *ty {
|
||||
return Err(Trap::new(
|
||||
"function attempted to return an incompatible value",
|
||||
));
|
||||
}
|
||||
unsafe {
|
||||
ret.write_value_to(values_vec.add(i));
|
||||
ret.write_value_to(&store, values_vec.add(i));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -535,7 +535,7 @@ impl Func {
|
||||
|
||||
// Store the argument values into `values_vec`.
|
||||
let param_tys = my_ty.params().iter();
|
||||
for ((arg, slot), ty) in params.iter().zip(&mut values_vec).zip(param_tys) {
|
||||
for ((arg, slot), ty) in params.iter().cloned().zip(&mut values_vec).zip(param_tys) {
|
||||
if arg.ty() != *ty {
|
||||
bail!(
|
||||
"argument type mismatch: found {} but expected {}",
|
||||
@@ -547,7 +547,7 @@ impl Func {
|
||||
bail!("cross-`Store` values are not currently supported");
|
||||
}
|
||||
unsafe {
|
||||
arg.write_value_to(slot);
|
||||
arg.write_value_to(&self.instance.store, slot);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ use crate::{Engine, Export, Extern, Func, Global, Memory, Module, Store, Table,
|
||||
use anyhow::{bail, Error, Result};
|
||||
use std::any::Any;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use wasmtime_environ::EntityIndex;
|
||||
use wasmtime_jit::{CompiledModule, Resolver};
|
||||
use wasmtime_runtime::{InstantiationError, VMContext, VMFunctionBody};
|
||||
use wasmtime_runtime::{
|
||||
InstantiationError, StackMapRegistry, VMContext, VMExternRefActivationsTable, VMFunctionBody,
|
||||
};
|
||||
|
||||
struct SimpleResolver<'a> {
|
||||
imports: &'a [Extern],
|
||||
@@ -24,6 +28,8 @@ fn instantiate(
|
||||
compiled_module: &CompiledModule,
|
||||
imports: &[Extern],
|
||||
host: Box<dyn Any>,
|
||||
externref_activations_table: Rc<VMExternRefActivationsTable>,
|
||||
stack_map_registry: Arc<StackMapRegistry>,
|
||||
) -> Result<StoreInstanceHandle, Error> {
|
||||
// For now we have a restriction that the `Store` that we're working
|
||||
// with is the same for everything involved here.
|
||||
@@ -50,6 +56,8 @@ fn instantiate(
|
||||
config.memory_creator.as_ref().map(|a| a as _),
|
||||
store.interrupts().clone(),
|
||||
host,
|
||||
externref_activations_table,
|
||||
stack_map_registry,
|
||||
)?;
|
||||
|
||||
// After we've created the `InstanceHandle` we still need to run
|
||||
@@ -183,10 +191,27 @@ impl Instance {
|
||||
bail!("cross-`Engine` instantiation is not currently supported");
|
||||
}
|
||||
|
||||
let info = module.register_frame_info();
|
||||
store.register_jit_code(module.compiled_module().jit_code_ranges());
|
||||
let host_info = Box::new({
|
||||
let frame_info_registration = module.register_frame_info();
|
||||
store.register_jit_code(module.compiled_module().jit_code_ranges());
|
||||
|
||||
let handle = instantiate(store, module.compiled_module(), imports, Box::new(info))?;
|
||||
// We need to make sure that we keep this alive as long as the instance
|
||||
// is alive, or else we could miss GC roots, reclaim objects too early,
|
||||
// and get user-after-frees.
|
||||
let stack_map_registration =
|
||||
unsafe { module.register_stack_maps(&*store.stack_map_registry()) };
|
||||
|
||||
(frame_info_registration, stack_map_registration)
|
||||
});
|
||||
|
||||
let handle = instantiate(
|
||||
store,
|
||||
module.compiled_module(),
|
||||
imports,
|
||||
host_info,
|
||||
store.externref_activations_table().clone(),
|
||||
store.stack_map_registry().clone(),
|
||||
)?;
|
||||
|
||||
Ok(Instance {
|
||||
handle,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use wasmparser::validate;
|
||||
use wasmtime_jit::CompiledModule;
|
||||
use wasmtime_runtime::{StackMapRegistration, StackMapRegistry};
|
||||
|
||||
/// A compiled WebAssembly module, ready to be instantiated.
|
||||
///
|
||||
@@ -80,6 +81,7 @@ pub struct Module {
|
||||
engine: Engine,
|
||||
compiled: Arc<CompiledModule>,
|
||||
frame_info_registration: Arc<Mutex<Option<Option<Arc<GlobalFrameInfoRegistration>>>>>,
|
||||
stack_map_registration: Arc<Mutex<Option<Option<Arc<StackMapRegistration>>>>>,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
@@ -307,6 +309,7 @@ impl Module {
|
||||
engine: engine.clone(),
|
||||
compiled: Arc::new(compiled),
|
||||
frame_info_registration: Arc::new(Mutex::new(None)),
|
||||
stack_map_registration: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -534,6 +537,41 @@ impl Module {
|
||||
*info = Some(ret.clone());
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Register this module's stack maps.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The same as `wasmtime_runtime::StackMapRegistry::register_stack_maps`.
|
||||
pub(crate) unsafe fn register_stack_maps(
|
||||
&self,
|
||||
registry: &Arc<StackMapRegistry>,
|
||||
) -> Option<Arc<StackMapRegistration>> {
|
||||
let mut registration = self.stack_map_registration.lock().unwrap();
|
||||
if let Some(registration) = &*registration {
|
||||
return registration.clone();
|
||||
}
|
||||
|
||||
let module = &self.compiled;
|
||||
let ret = registry
|
||||
.register_stack_maps(
|
||||
module
|
||||
.finished_functions()
|
||||
.values()
|
||||
.zip(module.stack_maps().values())
|
||||
.map(|(func, stack_maps)| {
|
||||
let ptr = (**func).as_ptr();
|
||||
let len = (**func).len();
|
||||
let start = ptr as usize;
|
||||
let end = ptr as usize + len;
|
||||
let range = start..end;
|
||||
(range, &stack_maps[..])
|
||||
}),
|
||||
)
|
||||
.map(Arc::new);
|
||||
*registration = Some(ret.clone());
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
fn _assert_send_sync() {
|
||||
|
||||
5
crates/wasmtime/src/ref.rs
Normal file → Executable file
5
crates/wasmtime/src/ref.rs
Normal file → Executable file
@@ -36,6 +36,11 @@ impl ExternRef {
|
||||
&*self.inner
|
||||
}
|
||||
|
||||
/// Get the reference count for this `ExternRef`.
|
||||
pub fn get_reference_count(&self) -> usize {
|
||||
self.inner.get_reference_count()
|
||||
}
|
||||
|
||||
/// Does this `ExternRef` point to the same inner value as `other`?0
|
||||
///
|
||||
/// This is *only* pointer equality, and does *not* run any inner value's
|
||||
|
||||
@@ -19,7 +19,8 @@ use wasmtime_jit::{native, CompilationStrategy, Compiler};
|
||||
use wasmtime_profiling::{JitDumpAgent, NullProfilerAgent, ProfilingAgent, VTuneAgent};
|
||||
use wasmtime_runtime::{
|
||||
debug_builtins, InstanceHandle, RuntimeMemoryCreator, SignalHandler, SignatureRegistry,
|
||||
VMExternRef, VMInterrupts, VMSharedSignatureIndex,
|
||||
StackMapRegistry, VMExternRef, VMExternRefActivationsTable, VMInterrupts,
|
||||
VMSharedSignatureIndex,
|
||||
};
|
||||
|
||||
// Runtime Environment
|
||||
@@ -194,10 +195,15 @@ impl Config {
|
||||
self.validating_config
|
||||
.operator_config
|
||||
.enable_reference_types = enable;
|
||||
// The reference types proposal depends on the bulk memory proposal
|
||||
self.flags
|
||||
.set("enable_safepoints", if enable { "true" } else { "false" })
|
||||
.unwrap();
|
||||
|
||||
// The reference types proposal depends on the bulk memory proposal.
|
||||
if enable {
|
||||
self.wasm_bulk_memory(true);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
@@ -724,6 +730,7 @@ pub struct Engine {
|
||||
struct EngineInner {
|
||||
config: Config,
|
||||
compiler: Compiler,
|
||||
stack_map_registry: Arc<StackMapRegistry>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -735,6 +742,7 @@ impl Engine {
|
||||
inner: Arc::new(EngineInner {
|
||||
config: config.clone(),
|
||||
compiler: config.build_compiler(),
|
||||
stack_map_registry: Arc::new(StackMapRegistry::default()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -792,6 +800,8 @@ pub(crate) struct StoreInner {
|
||||
signal_handler: RefCell<Option<Box<SignalHandler<'static>>>>,
|
||||
jit_code_ranges: RefCell<Vec<(usize, usize)>>,
|
||||
host_info: RefCell<HashMap<HostInfoKey, Rc<RefCell<dyn Any>>>>,
|
||||
externref_activations_table: Rc<VMExternRefActivationsTable>,
|
||||
stack_map_registry: Arc<StackMapRegistry>,
|
||||
}
|
||||
|
||||
struct HostInfoKey(VMExternRef);
|
||||
@@ -832,6 +842,8 @@ impl Store {
|
||||
signal_handler: RefCell::new(None),
|
||||
jit_code_ranges: RefCell::new(Vec::new()),
|
||||
host_info: RefCell::new(HashMap::new()),
|
||||
externref_activations_table: Rc::new(VMExternRefActivationsTable::new()),
|
||||
stack_map_registry: engine.inner.stack_map_registry.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1074,6 +1086,22 @@ impl Store {
|
||||
bail!("interrupts aren't enabled for this `Store`")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn externref_activations_table(&self) -> &Rc<VMExternRefActivationsTable> {
|
||||
&self.inner.externref_activations_table
|
||||
}
|
||||
|
||||
pub(crate) fn stack_map_registry(&self) -> &Arc<StackMapRegistry> {
|
||||
&self.inner.engine.inner.stack_map_registry
|
||||
}
|
||||
|
||||
/// Perform garbage collection of `ExternRef`s.
|
||||
pub fn gc(&self) {
|
||||
wasmtime_runtime::gc(
|
||||
&*self.inner.stack_map_registry,
|
||||
&*self.inner.externref_activations_table,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Store {
|
||||
|
||||
@@ -46,6 +46,8 @@ pub(crate) fn create_handle(
|
||||
signatures.into_boxed_slice(),
|
||||
state,
|
||||
store.interrupts().clone(),
|
||||
store.externref_activations_table().clone(),
|
||||
store.stack_map_registry().clone(),
|
||||
)?;
|
||||
Ok(store.add_instance(handle))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use super::create_handle::create_handle;
|
||||
use crate::trampoline::StoreInstanceHandle;
|
||||
use crate::{FuncType, Store, Trap};
|
||||
use crate::{FuncType, Store, Trap, ValType};
|
||||
use anyhow::{bail, Result};
|
||||
use std::any::Any;
|
||||
use std::cmp;
|
||||
@@ -11,7 +11,9 @@ use std::mem;
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use wasmtime_environ::entity::PrimaryMap;
|
||||
use wasmtime_environ::isa::TargetIsa;
|
||||
use wasmtime_environ::{ir, settings, CompiledFunction, EntityIndex, Module};
|
||||
use wasmtime_environ::{
|
||||
ir, settings, settings::Configurable, CompiledFunction, EntityIndex, Module,
|
||||
};
|
||||
use wasmtime_jit::trampoline::ir::{
|
||||
ExternalName, Function, InstBuilder, MemFlags, StackSlotData, StackSlotKind,
|
||||
};
|
||||
@@ -210,7 +212,14 @@ pub fn create_handle_with_function(
|
||||
) -> Result<(StoreInstanceHandle, VMTrampoline)> {
|
||||
let isa = {
|
||||
let isa_builder = native::builder();
|
||||
let flag_builder = settings::builder();
|
||||
let mut flag_builder = settings::builder();
|
||||
|
||||
if ft.params().iter().any(|p| *p == ValType::ExternRef)
|
||||
|| ft.results().iter().any(|r| *r == ValType::ExternRef)
|
||||
{
|
||||
flag_builder.set("enable_safepoints", "true").unwrap();
|
||||
}
|
||||
|
||||
isa_builder.finish(settings::Flags::new(flag_builder))
|
||||
};
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ impl ValType {
|
||||
ValType::F32 => Some(ir::types::F32),
|
||||
ValType::F64 => Some(ir::types::F64),
|
||||
ValType::V128 => Some(ir::types::I8X16),
|
||||
ValType::ExternRef => Some(ir::types::R64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -117,6 +118,7 @@ impl ValType {
|
||||
ir::types::F32 => Some(ValType::F32),
|
||||
ir::types::F64 => Some(ValType::F64),
|
||||
ir::types::I8X16 => Some(ValType::V128),
|
||||
ir::types::R64 => Some(ValType::ExternRef),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,15 +79,22 @@ impl Val {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn write_value_to(&self, p: *mut u128) {
|
||||
pub(crate) unsafe fn write_value_to(self, store: &Store, p: *mut u128) {
|
||||
match self {
|
||||
Val::I32(i) => ptr::write(p as *mut i32, *i),
|
||||
Val::I64(i) => ptr::write(p as *mut i64, *i),
|
||||
Val::F32(u) => ptr::write(p as *mut u32, *u),
|
||||
Val::F64(u) => ptr::write(p as *mut u64, *u),
|
||||
Val::V128(b) => ptr::write(p as *mut u128, *b),
|
||||
Val::I32(i) => ptr::write(p as *mut i32, i),
|
||||
Val::I64(i) => ptr::write(p as *mut i64, i),
|
||||
Val::F32(u) => ptr::write(p as *mut u32, u),
|
||||
Val::F64(u) => ptr::write(p as *mut u64, u),
|
||||
Val::V128(b) => ptr::write(p as *mut u128, b),
|
||||
Val::ExternRef(None) => ptr::write(p, 0),
|
||||
Val::ExternRef(Some(x)) => ptr::write(p as *mut *mut u8, x.inner.clone().into_raw()),
|
||||
Val::ExternRef(Some(x)) => {
|
||||
let externref_ptr = x.inner.as_raw();
|
||||
if let Err(inner) = store.externref_activations_table().try_insert(x.inner) {
|
||||
store.gc();
|
||||
store.externref_activations_table().insert_slow_path(inner);
|
||||
}
|
||||
ptr::write(p as *mut *mut u8, externref_ptr)
|
||||
}
|
||||
_ => unimplemented!("Val::write_value_to"),
|
||||
}
|
||||
}
|
||||
@@ -105,7 +112,7 @@ impl Val {
|
||||
Val::ExternRef(None)
|
||||
} else {
|
||||
Val::ExternRef(Some(ExternRef {
|
||||
inner: VMExternRef::from_raw(raw),
|
||||
inner: VMExternRef::clone_from_raw(raw),
|
||||
store: store.weak(),
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user