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:
@@ -3,6 +3,7 @@
|
||||
//! `InstanceHandle` is a reference-counting handle for an `Instance`.
|
||||
|
||||
use crate::export::Export;
|
||||
use crate::externref::{StackMapRegistry, VMExternRefActivationsTable};
|
||||
use crate::imports::Imports;
|
||||
use crate::memory::{DefaultMemoryCreator, RuntimeLinearMemory, RuntimeMemoryCreator};
|
||||
use crate::table::Table;
|
||||
@@ -20,6 +21,7 @@ use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::{mem, ptr, slice};
|
||||
use thiserror::Error;
|
||||
@@ -72,6 +74,19 @@ pub(crate) struct Instance {
|
||||
/// interrupted.
|
||||
pub(crate) interrupts: Arc<VMInterrupts>,
|
||||
|
||||
/// A handle to the (over-approximized) set of `externref`s that Wasm code
|
||||
/// is using.
|
||||
///
|
||||
/// The `vmctx` also holds a raw pointer to the table and relies on this
|
||||
/// member to keep it alive.
|
||||
pub(crate) externref_activations_table: Rc<VMExternRefActivationsTable>,
|
||||
|
||||
/// A handle to the stack map registry for this thread.
|
||||
///
|
||||
/// The `vmctx` also holds a raw pointer to the registry and relies on this
|
||||
/// member to keep it alive.
|
||||
pub(crate) stack_map_registry: Arc<StackMapRegistry>,
|
||||
|
||||
/// Additional context used by compiled wasm code. This field is last, and
|
||||
/// represents a dynamically-sized array that extends beyond the nominal
|
||||
/// end of the struct (similar to a flexible array member).
|
||||
@@ -238,6 +253,16 @@ impl Instance {
|
||||
unsafe { self.vmctx_plus_offset(self.offsets.vmctx_interrupts()) }
|
||||
}
|
||||
|
||||
/// Return a pointer to the `VMExternRefActivationsTable`.
|
||||
pub fn externref_activations_table(&self) -> *mut *mut VMExternRefActivationsTable {
|
||||
unsafe { self.vmctx_plus_offset(self.offsets.vmctx_externref_activations_table()) }
|
||||
}
|
||||
|
||||
/// Return a pointer to the `StackMapRegistry`.
|
||||
pub fn stack_map_registry(&self) -> *mut *mut StackMapRegistry {
|
||||
unsafe { self.vmctx_plus_offset(self.offsets.vmctx_stack_map_registry()) }
|
||||
}
|
||||
|
||||
/// Return a reference to the vmctx used by compiled wasm code.
|
||||
pub fn vmctx(&self) -> &VMContext {
|
||||
&self.vmctx
|
||||
@@ -784,6 +809,8 @@ impl InstanceHandle {
|
||||
vmshared_signatures: BoxedSlice<SignatureIndex, VMSharedSignatureIndex>,
|
||||
host_state: Box<dyn Any>,
|
||||
interrupts: Arc<VMInterrupts>,
|
||||
externref_activations_table: Rc<VMExternRefActivationsTable>,
|
||||
stack_map_registry: Arc<StackMapRegistry>,
|
||||
) -> Result<Self, InstantiationError> {
|
||||
let tables = create_tables(&module);
|
||||
let memories = create_memories(&module, mem_creator.unwrap_or(&DefaultMemoryCreator {}))?;
|
||||
@@ -819,6 +846,8 @@ impl InstanceHandle {
|
||||
trampolines,
|
||||
host_state,
|
||||
interrupts,
|
||||
externref_activations_table,
|
||||
stack_map_registry,
|
||||
vmctx: VMContext {},
|
||||
};
|
||||
let layout = instance.alloc_layout();
|
||||
@@ -878,6 +907,9 @@ impl InstanceHandle {
|
||||
VMBuiltinFunctionsArray::initialized(),
|
||||
);
|
||||
*instance.interrupts() = &*instance.interrupts;
|
||||
*instance.externref_activations_table() =
|
||||
&*instance.externref_activations_table as *const _ as *mut _;
|
||||
*instance.stack_map_registry() = &*instance.stack_map_registry as *const _ as *mut _;
|
||||
|
||||
// Perform infallible initialization in this constructor, while fallible
|
||||
// initialization is deferred to the `initialize` method.
|
||||
|
||||
Reference in New Issue
Block a user