Add shared memories (#4187)

* Add shared memories

This change adds the ability to use shared memories in Wasmtime when the
[threads proposal] is enabled. Shared memories are annotated as `shared`
in the WebAssembly syntax, e.g., `(memory 1 1 shared)`, and are
protected from concurrent access during `memory.size` and `memory.grow`.

[threads proposal]: https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md

In order to implement this in Wasmtime, there are two main cases to
cover:
    - a program may simply create a shared memory and possibly export it;
    this means that Wasmtime itself must be able to create shared
    memories
    - a user may create a shared memory externally and pass it in as an
    import during instantiation; this is the case when the program
    contains code like `(import "env" "memory" (memory 1 1
    shared))`--this case is handled by a new Wasmtime API
    type--`SharedMemory`

Because of the first case, this change allows any of the current
memory-creation mechanisms to work as-is. Wasmtime can still create
either static or dynamic memories in either on-demand or pooling modes,
and any of these memories can be considered shared. When shared, the
`Memory` runtime container will lock appropriately during `memory.size`
and `memory.grow` operations; since all memories use this container, it
is an ideal place for implementing the locking once and once only.

The second case is covered by the new `SharedMemory` structure. It uses
the same `Mmap` allocation under the hood as non-shared memories, but
allows the user to perform the allocation externally to Wasmtime and
share the memory across threads (via an `Arc`). The pointer address to
the actual memory is carefully wired through and owned by the
`SharedMemory` structure itself. This means that there are differing
views of where to access the pointer (i.e., `VMMemoryDefinition`): for
owned memories (the default), the `VMMemoryDefinition` is stored
directly by the `VMContext`; in the `SharedMemory` case, however, this
`VMContext` must point to this separate structure.

To ensure that the `VMContext` can always point to the correct
`VMMemoryDefinition`, this change alters the `VMContext` structure.
Since a `SharedMemory` owns its own `VMMemoryDefinition`, the
`defined_memories` table in the `VMContext` becomes a sequence of
pointers--in the shared memory case, they point to the
`VMMemoryDefinition` owned by the `SharedMemory` and in the owned memory
case (i.e., not shared) they point to `VMMemoryDefinition`s stored in a
new table, `owned_memories`.

This change adds an additional indirection (through the `*mut
VMMemoryDefinition` pointer) that could add overhead. Using an imported
memory as a proxy, we measured a 1-3% overhead of this approach on the
`pulldown-cmark` benchmark. To avoid this, Cranelift-generated code will
special-case the owned memory access (i.e., load a pointer directly to
the `owned_memories` entry) for `memory.size` so that only
shared memories (and imported memories, as before) incur the indirection
cost.

* review: remove thread feature check

* review: swap wasmtime-types dependency for existing wasmtime-environ use

* review: remove unused VMMemoryUnion

* review: reword cross-engine error message

* review: improve tests

* review: refactor to separate prevent Memory <-> SharedMemory conversion

* review: into_shared_memory -> as_shared_memory

* review: remove commented out code

* review: limit shared min/max to 32 bits

* review: skip imported memories

* review: imported memories are not owned

* review: remove TODO

* review: document unsafe send + sync

* review: add limiter assertion

* review: remove TODO

* review: improve tests

* review: fix doc test

* fix: fixes based on discussion with Alex

This changes several key parts:
 - adds memory indexes to imports and exports
 - makes `VMMemoryDefinition::current_length` an atomic usize

* review: add `Extern::SharedMemory`

* review: remove TODO

* review: atomically load from VMMemoryDescription in JIT-generated code

* review: add test probing the last available memory slot across threads

* fix: move assertion to new location due to rebase

* fix: doc link

* fix: add TODOs to c-api

* fix: broken doc link

* fix: modify pooling allocator messages in tests

* review: make owned_memory_index panic instead of returning an option

* review: clarify calculation of num_owned_memories

* review: move 'use' to top of file

* review: change '*const [u8]' to '*mut [u8]'

* review: remove TODO

* review: avoid hard-coding memory index

* review: remove 'preallocation' parameter from 'Memory::_new'

* fix: component model memory length

* review: check that shared memory plans are static

* review: ignore growth limits for shared memory

* review: improve atomic store comment

* review: add FIXME for memory growth failure

* review: add comment about absence of bounds-checked 'memory.size'

* review: make 'current_length()' doc comment more precise

* review: more comments related to memory.size non-determinism

* review: make 'vmmemory' unreachable for shared memory

* review: move code around

* review: thread plan through to 'wrap()'

* review: disallow shared memory allocation with the pooling allocator
This commit is contained in:
Andrew Brown
2022-06-08 10:13:40 -07:00
committed by GitHub
parent ed9db962de
commit 2b52f47b83
27 changed files with 1211 additions and 226 deletions

View File

@@ -10,7 +10,7 @@ use std::mem;
use std::ops::Range;
use wasmtime_types::*;
/// Implemenation styles for WebAssembly linear memory.
/// Implementation styles for WebAssembly linear memory.
#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
pub enum MemoryStyle {
/// The actual memory can be resized and moved.
@@ -18,7 +18,7 @@ pub enum MemoryStyle {
/// Extra space to reserve when a memory must be moved due to growth.
reserve: u64,
},
/// Addresss space is allocated up front.
/// Address space is allocated up front.
Static {
/// The number of mapped and unmapped pages.
bound: u64,
@@ -160,7 +160,7 @@ pub enum MemoryInitialization {
/// which might reside in a compiled module on disk, available immediately
/// in a linear memory's address space.
///
/// To facilitate the latter fo these techniques the `try_static_init`
/// To facilitate the latter of these techniques the `try_static_init`
/// function below, which creates this variant, takes a host page size
/// argument which can page-align everything to make mmap-ing possible.
Static {
@@ -919,6 +919,28 @@ impl Module {
}
}
/// Convert a `DefinedMemoryIndex` into an `OwnedMemoryIndex`. Returns None
/// if the index is an imported memory.
#[inline]
pub fn owned_memory_index(&self, memory: DefinedMemoryIndex) -> OwnedMemoryIndex {
assert!(
memory.index() < self.memory_plans.len(),
"non-shared memory must have an owned index"
);
// Once we know that the memory index is not greater than the number of
// plans, we can iterate through the plans up to the memory index and
// count how many are not shared (i.e., owned).
let owned_memory_index = self
.memory_plans
.iter()
.skip(self.num_imported_memories)
.take(memory.index())
.filter(|(_, mp)| !mp.memory.shared)
.count();
OwnedMemoryIndex::new(owned_memory_index)
}
/// Test whether the given memory index is for an imported memory.
#[inline]
pub fn is_imported_memory(&self, index: MemoryIndex) -> bool {

View File

@@ -240,9 +240,6 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> {
EntityType::Function(sig_index)
}
TypeRef::Memory(ty) => {
if ty.shared {
return Err(WasmError::Unsupported("shared memories".to_owned()));
}
self.result.module.num_imported_memories += 1;
EntityType::Memory(ty.into())
}
@@ -296,9 +293,6 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> {
for entry in memories {
let memory = entry?;
if memory.shared {
return Err(WasmError::Unsupported("shared memories".to_owned()));
}
let plan = MemoryPlan::for_memory(memory.into(), &self.tunables);
self.result.module.memory_plans.push(plan);
}

View File

@@ -15,7 +15,8 @@
// imported_memories: [VMMemoryImport; module.num_imported_memories],
// imported_globals: [VMGlobalImport; module.num_imported_globals],
// tables: [VMTableDefinition; module.num_defined_tables],
// memories: [VMMemoryDefinition; module.num_defined_memories],
// memories: [*mut VMMemoryDefinition; module.num_defined_memories],
// owned_memories: [VMMemoryDefinition; module.num_owned_memories],
// globals: [VMGlobalDefinition; module.num_defined_globals],
// anyfuncs: [VMCallerCheckedAnyfunc; module.num_escaped_funcs],
// }
@@ -27,6 +28,7 @@ use crate::{
use cranelift_entity::packed_option::ReservedValue;
use more_asserts::assert_lt;
use std::convert::TryFrom;
use wasmtime_types::OwnedMemoryIndex;
/// Sentinel value indicating that wasm has been interrupted.
// Note that this has a bit of an odd definition. See the `insert_stack_check`
@@ -66,6 +68,8 @@ pub struct VMOffsets<P> {
pub num_defined_tables: u32,
/// The number of defined memories in the module.
pub num_defined_memories: u32,
/// The number of memories owned by the module instance.
pub num_owned_memories: u32,
/// The number of defined globals in the module.
pub num_defined_globals: u32,
/// The number of escaped functions in the module, the size of the anyfuncs
@@ -86,6 +90,7 @@ pub struct VMOffsets<P> {
imported_globals: u32,
defined_tables: u32,
defined_memories: u32,
owned_memories: u32,
defined_globals: u32,
defined_anyfuncs: u32,
size: u32,
@@ -157,9 +162,11 @@ pub struct VMOffsetsFields<P> {
pub num_defined_tables: u32,
/// The number of defined memories in the module.
pub num_defined_memories: u32,
/// The number of memories owned by the module instance.
pub num_owned_memories: u32,
/// The number of defined globals in the module.
pub num_defined_globals: u32,
/// The numbe of escaped functions in the module, the size of the anyfunc
/// The number of escaped functions in the module, the size of the anyfunc
/// array.
pub num_escaped_funcs: u32,
}
@@ -167,6 +174,14 @@ pub struct VMOffsetsFields<P> {
impl<P: PtrSize> VMOffsets<P> {
/// Return a new `VMOffsets` instance, for a given pointer size.
pub fn new(ptr: P, module: &Module) -> Self {
let num_owned_memories = module
.memory_plans
.iter()
.skip(module.num_imported_memories)
.filter(|p| !p.1.memory.shared)
.count()
.try_into()
.unwrap();
VMOffsets::from(VMOffsetsFields {
ptr,
num_imported_functions: cast_to_u32(module.num_imported_funcs),
@@ -177,6 +192,7 @@ impl<P: PtrSize> VMOffsets<P> {
num_defined_memories: cast_to_u32(
module.memory_plans.len() - module.num_imported_memories,
),
num_owned_memories,
num_defined_globals: cast_to_u32(module.globals.len() - module.num_imported_globals),
num_escaped_funcs: cast_to_u32(module.num_escaped_funcs),
})
@@ -206,12 +222,13 @@ impl<P: PtrSize> VMOffsets<P> {
num_defined_tables: _,
num_defined_globals: _,
num_defined_memories: _,
num_owned_memories: _,
num_escaped_funcs: _,
// used as the initial size below
size,
// exhaustively match teh rest of the fields with input from
// exhaustively match the rest of the fields with input from
// the macro
$($name,)*
} = *self;
@@ -235,6 +252,7 @@ impl<P: PtrSize> VMOffsets<P> {
defined_anyfuncs: "module functions",
defined_globals: "defined globals",
defined_memories: "defined memories",
owned_memories: "owned memories",
defined_tables: "defined tables",
imported_globals: "imported globals",
imported_memories: "imported memories",
@@ -261,6 +279,7 @@ impl<P: PtrSize> From<VMOffsetsFields<P>> for VMOffsets<P> {
num_imported_globals: fields.num_imported_globals,
num_defined_tables: fields.num_defined_tables,
num_defined_memories: fields.num_defined_memories,
num_owned_memories: fields.num_owned_memories,
num_defined_globals: fields.num_defined_globals,
num_escaped_funcs: fields.num_escaped_funcs,
magic: 0,
@@ -276,6 +295,7 @@ impl<P: PtrSize> From<VMOffsetsFields<P>> for VMOffsets<P> {
imported_globals: 0,
defined_tables: 0,
defined_memories: 0,
owned_memories: 0,
defined_globals: 0,
defined_anyfuncs: 0,
size: 0,
@@ -330,7 +350,9 @@ impl<P: PtrSize> From<VMOffsetsFields<P>> for VMOffsets<P> {
size(defined_tables)
= cmul(ret.num_defined_tables, ret.size_of_vmtable_definition()),
size(defined_memories)
= cmul(ret.num_defined_memories, ret.size_of_vmmemory_definition()),
= cmul(ret.num_defined_memories, ret.size_of_vmmemory_pointer()),
size(owned_memories)
= cmul(ret.num_owned_memories, ret.size_of_vmmemory_definition()),
align(16),
size(defined_globals)
= cmul(ret.num_defined_globals, ret.size_of_vmglobal_definition()),
@@ -452,7 +474,7 @@ impl<P: PtrSize> VMOffsets<P> {
/// Return the size of `VMMemoryImport`.
#[inline]
pub fn size_of_vmmemory_import(&self) -> u8 {
2 * self.pointer_size()
3 * self.pointer_size()
}
}
@@ -477,6 +499,12 @@ impl<P: PtrSize> VMOffsets<P> {
pub fn size_of_vmmemory_definition(&self) -> u8 {
2 * self.pointer_size()
}
/// Return the size of `*mut VMMemoryDefinition`.
#[inline]
pub fn size_of_vmmemory_pointer(&self) -> u8 {
self.pointer_size()
}
}
/// Offsets for `VMGlobalImport`.
@@ -613,6 +641,12 @@ impl<P: PtrSize> VMOffsets<P> {
self.defined_memories
}
/// The offset of the `owned_memories` array.
#[inline]
pub fn vmctx_owned_memories_begin(&self) -> u32 {
self.owned_memories
}
/// The offset of the `globals` array.
#[inline]
pub fn vmctx_globals_begin(&self) -> u32 {
@@ -676,11 +710,19 @@ impl<P: PtrSize> VMOffsets<P> {
self.vmctx_tables_begin() + index.as_u32() * u32::from(self.size_of_vmtable_definition())
}
/// Return the offset to `VMMemoryDefinition` index `index`.
/// Return the offset to the `*mut VMMemoryDefinition` at index `index`.
#[inline]
pub fn vmctx_vmmemory_definition(&self, index: DefinedMemoryIndex) -> u32 {
pub fn vmctx_vmmemory_pointer(&self, index: DefinedMemoryIndex) -> u32 {
assert_lt!(index.as_u32(), self.num_defined_memories);
self.vmctx_memories_begin() + index.as_u32() * u32::from(self.size_of_vmmemory_definition())
self.vmctx_memories_begin() + index.as_u32() * u32::from(self.size_of_vmmemory_pointer())
}
/// Return the offset to the owned `VMMemoryDefinition` at index `index`.
#[inline]
pub fn vmctx_vmmemory_definition(&self, index: OwnedMemoryIndex) -> u32 {
assert_lt!(index.as_u32(), self.num_owned_memories);
self.vmctx_owned_memories_begin()
+ index.as_u32() * u32::from(self.size_of_vmmemory_definition())
}
/// Return the offset to the `VMGlobalDefinition` index `index`.
@@ -744,13 +786,13 @@ impl<P: PtrSize> VMOffsets<P> {
/// Return the offset to the `base` field in `VMMemoryDefinition` index `index`.
#[inline]
pub fn vmctx_vmmemory_definition_base(&self, index: DefinedMemoryIndex) -> u32 {
pub fn vmctx_vmmemory_definition_base(&self, index: OwnedMemoryIndex) -> u32 {
self.vmctx_vmmemory_definition(index) + u32::from(self.vmmemory_definition_base())
}
/// Return the offset to the `current_length` field in `VMMemoryDefinition` index `index`.
#[inline]
pub fn vmctx_vmmemory_definition_current_length(&self, index: DefinedMemoryIndex) -> u32 {
pub fn vmctx_vmmemory_definition_current_length(&self, index: OwnedMemoryIndex) -> u32 {
self.vmctx_vmmemory_definition(index) + u32::from(self.vmmemory_definition_current_length())
}