Skip to content

Commit d74a422

Browse files
committed
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.
1 parent 140b835 commit d74a422

File tree

23 files changed

+858
-144
lines changed

23 files changed

+858
-144
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cranelift/object/tests/basic.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ fn libcall_function() {
200200

201201
#[test]
202202
#[should_panic(
203-
expected = "Result::unwrap()` on an `Err` value: Backend(Symbol \"function\\0with\\0nul\\0bytes\" has a null byte, which is disallowed"
203+
expected = "Result::unwrap()` on an `Err` value: Backend(Symbol \"function\\u{0}with\\u{0}nul\\u{0}bytes\" has a null byte, which is disallowed"
204204
)]
205205
fn reject_nul_byte_symbol_for_func() {
206206
let flag_builder = settings::builder();
@@ -224,7 +224,7 @@ fn reject_nul_byte_symbol_for_func() {
224224

225225
#[test]
226226
#[should_panic(
227-
expected = "Result::unwrap()` on an `Err` value: Backend(Symbol \"data\\0with\\0nul\\0bytes\" has a null byte, which is disallowed"
227+
expected = "Result::unwrap()` on an `Err` value: Backend(Symbol \"data\\u{0}with\\u{0}nul\\u{0}bytes\" has a null byte, which is disallowed"
228228
)]
229229
fn reject_nul_byte_symbol_for_data() {
230230
let flag_builder = settings::builder();

crates/cranelift/src/compiler.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use cranelift_codegen::{MachSrcLoc, MachStackMap};
1616
use cranelift_entity::{EntityRef, PrimaryMap};
1717
use cranelift_frontend::FunctionBuilder;
1818
use cranelift_wasm::{
19-
DefinedFuncIndex, DefinedMemoryIndex, FuncIndex, FuncTranslator, MemoryIndex, SignatureIndex,
19+
DefinedFuncIndex, FuncIndex, FuncTranslator, MemoryIndex, OwnedMemoryIndex, SignatureIndex,
2020
WasmFuncType,
2121
};
2222
use object::write::Object;
@@ -332,8 +332,9 @@ impl wasmtime_environ::Compiler for Compiler {
332332
let memory_offset = if ofs.num_imported_memories > 0 {
333333
ModuleMemoryOffset::Imported(ofs.vmctx_vmmemory_import(MemoryIndex::new(0)))
334334
} else if ofs.num_defined_memories > 0 {
335+
// TODO shared?
335336
ModuleMemoryOffset::Defined(
336-
ofs.vmctx_vmmemory_definition_base(DefinedMemoryIndex::new(0)),
337+
ofs.vmctx_vmmemory_definition_base(OwnedMemoryIndex::new(0)),
337338
)
338339
} else {
339340
ModuleMemoryOffset::None

crates/cranelift/src/func_environ.rs

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,18 +1368,37 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
13681368

13691369
fn make_heap(&mut self, func: &mut ir::Function, index: MemoryIndex) -> WasmResult<ir::Heap> {
13701370
let pointer_type = self.pointer_type();
1371-
1371+
let is_shared = self.module.memory_plans[index].memory.shared;
13721372
let (ptr, base_offset, current_length_offset) = {
13731373
let vmctx = self.vmctx(func);
13741374
if let Some(def_index) = self.module.defined_memory_index(index) {
1375-
let base_offset =
1376-
i32::try_from(self.offsets.vmctx_vmmemory_definition_base(def_index)).unwrap();
1377-
let current_length_offset = i32::try_from(
1378-
self.offsets
1379-
.vmctx_vmmemory_definition_current_length(def_index),
1380-
)
1381-
.unwrap();
1382-
(vmctx, base_offset, current_length_offset)
1375+
if is_shared {
1376+
// As with imported memory, the `VMMemoryDefinition` for a
1377+
// shared memory is stored elsewhere. We store a `*mut
1378+
// VMMemoryDefinition` to it and dereference that when
1379+
// atomically growing it.
1380+
let from_offset = self.offsets.vmctx_vmmemory_pointer(def_index);
1381+
let memory = func.create_global_value(ir::GlobalValueData::Load {
1382+
base: vmctx,
1383+
offset: Offset32::new(i32::try_from(from_offset).unwrap()),
1384+
global_type: pointer_type,
1385+
readonly: true,
1386+
});
1387+
let base_offset = i32::from(self.offsets.vmmemory_definition_base());
1388+
let current_length_offset =
1389+
i32::from(self.offsets.vmmemory_definition_current_length());
1390+
(memory, base_offset, current_length_offset)
1391+
} else {
1392+
let owned_index = self.module.owned_memory_index(def_index).expect("TODO");
1393+
let owned_base_offset =
1394+
self.offsets.vmctx_vmmemory_definition_base(owned_index);
1395+
let owned_length_offset = self
1396+
.offsets
1397+
.vmctx_vmmemory_definition_current_length(owned_index);
1398+
let current_base_offset = i32::try_from(owned_base_offset).unwrap();
1399+
let current_length_offset = i32::try_from(owned_length_offset).unwrap();
1400+
(vmctx, current_base_offset, current_length_offset)
1401+
}
13831402
} else {
13841403
let from_offset = self.offsets.vmctx_vmmemory_import_from(index);
13851404
let memory = func.create_global_value(ir::GlobalValueData::Load {
@@ -1693,16 +1712,33 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m
16931712
) -> WasmResult<ir::Value> {
16941713
let pointer_type = self.pointer_type();
16951714
let vmctx = self.vmctx(&mut pos.func);
1715+
let is_shared = self.module.memory_plans[index].memory.shared;
16961716
let base = pos.ins().global_value(pointer_type, vmctx);
16971717
let current_length_in_bytes = match self.module.defined_memory_index(index) {
16981718
Some(def_index) => {
1699-
let offset = i32::try_from(
1700-
self.offsets
1701-
.vmctx_vmmemory_definition_current_length(def_index),
1702-
)
1703-
.unwrap();
1704-
pos.ins()
1705-
.load(pointer_type, ir::MemFlags::trusted(), base, offset)
1719+
if is_shared {
1720+
let offset =
1721+
i32::try_from(self.offsets.vmctx_vmmemory_pointer(def_index)).unwrap();
1722+
let vmmemory_ptr =
1723+
pos.ins()
1724+
.load(pointer_type, ir::MemFlags::trusted(), base, offset);
1725+
// TODO should be an atomic_load (need a way to to do atomic_load + offset).
1726+
pos.ins().load(
1727+
pointer_type,
1728+
ir::MemFlags::trusted(),
1729+
vmmemory_ptr,
1730+
i32::from(self.offsets.vmmemory_definition_current_length()),
1731+
)
1732+
} else {
1733+
let owned_index = self.module.owned_memory_index(def_index).expect("TODO");
1734+
let offset = i32::try_from(
1735+
self.offsets
1736+
.vmctx_vmmemory_definition_current_length(owned_index),
1737+
)
1738+
.unwrap();
1739+
pos.ins()
1740+
.load(pointer_type, ir::MemFlags::trusted(), base, offset)
1741+
}
17061742
}
17071743
None => {
17081744
let offset = i32::try_from(self.offsets.vmctx_vmmemory_import_from(index)).unwrap();

crates/environ/src/module.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ use std::mem;
1010
use std::ops::Range;
1111
use wasmtime_types::*;
1212

13-
/// Implemenation styles for WebAssembly linear memory.
13+
/// Implementation styles for WebAssembly linear memory.
1414
#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
1515
pub enum MemoryStyle {
1616
/// The actual memory can be resized and moved.
1717
Dynamic {
1818
/// Extra space to reserve when a memory must be moved due to growth.
1919
reserve: u64,
2020
},
21-
/// Addresss space is allocated up front.
21+
/// Address space is allocated up front.
2222
Static {
2323
/// The number of mapped and unmapped pages.
2424
bound: u64,
@@ -160,7 +160,7 @@ pub enum MemoryInitialization {
160160
/// which might reside in a compiled module on disk, available immediately
161161
/// in a linear memory's address space.
162162
///
163-
/// To facilitate the latter fo these techniques the `try_static_init`
163+
/// To facilitate the latter of these techniques the `try_static_init`
164164
/// function below, which creates this variant, takes a host page size
165165
/// argument which can page-align everything to make mmap-ing possible.
166166
Static {
@@ -919,6 +919,25 @@ impl Module {
919919
}
920920
}
921921

922+
/// Convert a `DefinedMemoryIndex` into an `OwnedMemoryIndex`. Returns None
923+
/// if the index is an imported memory.
924+
#[inline]
925+
pub fn owned_memory_index(&self, memory: DefinedMemoryIndex) -> Option<OwnedMemoryIndex> {
926+
if memory.index() >= self.memory_plans.len() {
927+
return None;
928+
}
929+
// Once we know that the memory index is not greater than the number of
930+
// plans, we can iterate through the plans up to the memory index and
931+
// count how many are not shared (i.e., owned).
932+
let owned_memory_index = self
933+
.memory_plans
934+
.iter()
935+
.take(memory.index())
936+
.filter(|(_, mp)| !mp.memory.shared)
937+
.count();
938+
Some(OwnedMemoryIndex::new(owned_memory_index))
939+
}
940+
922941
/// Test whether the given memory index is for an imported memory.
923942
#[inline]
924943
pub fn is_imported_memory(&self, index: MemoryIndex) -> bool {

crates/environ/src/module_environ.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> {
240240
EntityType::Function(sig_index)
241241
}
242242
TypeRef::Memory(ty) => {
243-
if ty.shared {
243+
if ty.shared && !self.validator.features().threads {
244244
return Err(WasmError::Unsupported("shared memories".to_owned()));
245245
}
246246
self.result.module.num_imported_memories += 1;
@@ -296,7 +296,7 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> {
296296

297297
for entry in memories {
298298
let memory = entry?;
299-
if memory.shared {
299+
if memory.shared && !self.validator.features().threads {
300300
return Err(WasmError::Unsupported("shared memories".to_owned()));
301301
}
302302
let plan = MemoryPlan::for_memory(memory.into(), &self.tunables);

crates/environ/src/vmoffsets.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
// imported_memories: [VMMemoryImport; module.num_imported_memories],
1515
// imported_globals: [VMGlobalImport; module.num_imported_globals],
1616
// tables: [VMTableDefinition; module.num_defined_tables],
17-
// memories: [VMMemoryDefinition; module.num_defined_memories],
17+
// memories: [*mut VMMemoryDefinition; module.num_defined_memories],
18+
// owned_memories: [VMMemoryDefinition; module.num_owned_memories],
1819
// globals: [VMGlobalDefinition; module.num_defined_globals],
1920
// anyfuncs: [VMCallerCheckedAnyfunc; module.num_escaped_funcs],
2021
// }
@@ -26,6 +27,7 @@ use crate::{
2627
use cranelift_entity::packed_option::ReservedValue;
2728
use more_asserts::assert_lt;
2829
use std::convert::TryFrom;
30+
use wasmtime_types::OwnedMemoryIndex;
2931

3032
/// Sentinel value indicating that wasm has been interrupted.
3133
// Note that this has a bit of an odd definition. See the `insert_stack_check`
@@ -67,6 +69,8 @@ pub struct VMOffsets<P> {
6769
pub num_defined_tables: u32,
6870
/// The number of defined memories in the module.
6971
pub num_defined_memories: u32,
72+
/// The number of memories owned by the module instance.
73+
pub num_owned_memories: u32,
7074
/// The number of defined globals in the module.
7175
pub num_defined_globals: u32,
7276
/// The number of escaped functions in the module, the size of the anyfuncs
@@ -86,6 +90,7 @@ pub struct VMOffsets<P> {
8690
imported_globals: u32,
8791
defined_tables: u32,
8892
defined_memories: u32,
93+
owned_memories: u32,
8994
defined_globals: u32,
9095
defined_anyfuncs: u32,
9196
size: u32,
@@ -133,16 +138,23 @@ pub struct VMOffsetsFields<P> {
133138
pub num_defined_tables: u32,
134139
/// The number of defined memories in the module.
135140
pub num_defined_memories: u32,
141+
/// The number of memories owned by the module instance.
142+
pub num_owned_memories: u32,
136143
/// The number of defined globals in the module.
137144
pub num_defined_globals: u32,
138-
/// The numbe of escaped functions in the module, the size of the anyfunc
145+
/// The number of escaped functions in the module, the size of the anyfunc
139146
/// array.
140147
pub num_escaped_funcs: u32,
141148
}
142149

143150
impl<P: PtrSize> VMOffsets<P> {
144151
/// Return a new `VMOffsets` instance, for a given pointer size.
145152
pub fn new(ptr: P, module: &Module) -> Self {
153+
let num_shared_memories = module
154+
.memory_plans
155+
.iter()
156+
.filter(|p| p.1.memory.shared)
157+
.count();
146158
VMOffsets::from(VMOffsetsFields {
147159
ptr,
148160
num_imported_functions: cast_to_u32(module.num_imported_funcs),
@@ -152,6 +164,7 @@ impl<P: PtrSize> VMOffsets<P> {
152164
num_defined_functions: cast_to_u32(module.functions.len()),
153165
num_defined_tables: cast_to_u32(module.table_plans.len()),
154166
num_defined_memories: cast_to_u32(module.memory_plans.len()),
167+
num_owned_memories: cast_to_u32(module.memory_plans.len() - num_shared_memories),
155168
num_defined_globals: cast_to_u32(module.globals.len()),
156169
num_escaped_funcs: cast_to_u32(module.num_escaped_funcs),
157170
})
@@ -181,13 +194,14 @@ impl<P: PtrSize> VMOffsets<P> {
181194
num_defined_tables: _,
182195
num_defined_globals: _,
183196
num_defined_memories: _,
197+
num_owned_memories: _,
184198
num_defined_functions: _,
185199
num_escaped_funcs: _,
186200

187201
// used as the initial size below
188202
size,
189203

190-
// exhaustively match teh rest of the fields with input from
204+
// exhaustively match the rest of the fields with input from
191205
// the macro
192206
$($name,)*
193207
} = *self;
@@ -211,6 +225,7 @@ impl<P: PtrSize> VMOffsets<P> {
211225
defined_anyfuncs: "module functions",
212226
defined_globals: "defined globals",
213227
defined_memories: "defined memories",
228+
owned_memories: "owned memories",
214229
defined_tables: "defined tables",
215230
imported_globals: "imported globals",
216231
imported_memories: "imported memories",
@@ -237,6 +252,7 @@ impl<P: PtrSize> From<VMOffsetsFields<P>> for VMOffsets<P> {
237252
num_defined_functions: fields.num_defined_functions,
238253
num_defined_tables: fields.num_defined_tables,
239254
num_defined_memories: fields.num_defined_memories,
255+
num_owned_memories: fields.num_owned_memories,
240256
num_defined_globals: fields.num_defined_globals,
241257
num_escaped_funcs: fields.num_escaped_funcs,
242258
runtime_limits: 0,
@@ -251,6 +267,7 @@ impl<P: PtrSize> From<VMOffsetsFields<P>> for VMOffsets<P> {
251267
imported_globals: 0,
252268
defined_tables: 0,
253269
defined_memories: 0,
270+
owned_memories: 0,
254271
defined_globals: 0,
255272
defined_anyfuncs: 0,
256273
size: 0,
@@ -303,7 +320,9 @@ impl<P: PtrSize> From<VMOffsetsFields<P>> for VMOffsets<P> {
303320
size(defined_tables)
304321
= cmul(ret.num_defined_tables, ret.size_of_vmtable_definition()),
305322
size(defined_memories)
306-
= cmul(ret.num_defined_memories, ret.size_of_vmmemory_definition()),
323+
= cmul(ret.num_defined_memories, ret.size_of_vmmemory_pointer()),
324+
size(owned_memories)
325+
= cmul(ret.num_owned_memories, ret.size_of_vmmemory_definition()),
307326
align(16),
308327
size(defined_globals)
309328
= cmul(ret.num_defined_globals, ret.size_of_vmglobal_definition()),
@@ -445,6 +464,12 @@ impl<P: PtrSize> VMOffsets<P> {
445464
pub fn size_of_vmmemory_definition(&self) -> u8 {
446465
2 * self.pointer_size()
447466
}
467+
468+
/// Return the size of `*mut VMMemoryDefinition`.
469+
#[inline]
470+
pub fn size_of_vmmemory_pointer(&self) -> u8 {
471+
self.pointer_size()
472+
}
448473
}
449474

450475
/// Offsets for `VMGlobalImport`.
@@ -604,6 +629,12 @@ impl<P: PtrSize> VMOffsets<P> {
604629
self.defined_memories
605630
}
606631

632+
/// The offset of the `owned_memories` array.
633+
#[inline]
634+
pub fn vmctx_owned_memories_begin(&self) -> u32 {
635+
self.owned_memories
636+
}
637+
607638
/// The offset of the `globals` array.
608639
#[inline]
609640
pub fn vmctx_globals_begin(&self) -> u32 {
@@ -667,11 +698,19 @@ impl<P: PtrSize> VMOffsets<P> {
667698
self.vmctx_tables_begin() + index.as_u32() * u32::from(self.size_of_vmtable_definition())
668699
}
669700

670-
/// Return the offset to `VMMemoryDefinition` index `index`.
701+
/// Return the offset to the `*mut VMMemoryDefinition` at index `index`.
671702
#[inline]
672-
pub fn vmctx_vmmemory_definition(&self, index: DefinedMemoryIndex) -> u32 {
703+
pub fn vmctx_vmmemory_pointer(&self, index: DefinedMemoryIndex) -> u32 {
673704
assert_lt!(index.as_u32(), self.num_defined_memories);
674-
self.vmctx_memories_begin() + index.as_u32() * u32::from(self.size_of_vmmemory_definition())
705+
self.vmctx_memories_begin() + index.as_u32() * u32::from(self.size_of_vmmemory_pointer())
706+
}
707+
708+
/// Return the offset to the owned `VMMemoryDefinition` at index `index`.
709+
#[inline]
710+
pub fn vmctx_vmmemory_definition(&self, index: OwnedMemoryIndex) -> u32 {
711+
assert_lt!(index.as_u32(), self.num_owned_memories);
712+
self.vmctx_owned_memories_begin()
713+
+ index.as_u32() * u32::from(self.size_of_vmmemory_definition())
675714
}
676715

677716
/// Return the offset to the `VMGlobalDefinition` index `index`.
@@ -735,13 +774,13 @@ impl<P: PtrSize> VMOffsets<P> {
735774

736775
/// Return the offset to the `base` field in `VMMemoryDefinition` index `index`.
737776
#[inline]
738-
pub fn vmctx_vmmemory_definition_base(&self, index: DefinedMemoryIndex) -> u32 {
777+
pub fn vmctx_vmmemory_definition_base(&self, index: OwnedMemoryIndex) -> u32 {
739778
self.vmctx_vmmemory_definition(index) + u32::from(self.vmmemory_definition_base())
740779
}
741780

742781
/// Return the offset to the `current_length` field in `VMMemoryDefinition` index `index`.
743782
#[inline]
744-
pub fn vmctx_vmmemory_definition_current_length(&self, index: DefinedMemoryIndex) -> u32 {
783+
pub fn vmctx_vmmemory_definition_current_length(&self, index: OwnedMemoryIndex) -> u32 {
745784
self.vmctx_vmmemory_definition(index) + u32::from(self.vmmemory_definition_current_length())
746785
}
747786

crates/runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ edition = "2021"
1414
wasmtime-environ = { path = "../environ", version = "=0.38.0" }
1515
wasmtime-fiber = { path = "../fiber", version = "=0.38.0", optional = true }
1616
wasmtime-jit-debug = { path = "../jit-debug", version = "=0.38.0", features = ["gdb_jit_int"] }
17+
wasmtime-types = { path = "../types", version = "=0.38.0" }
1718
region = "2.1.0"
1819
libc = { version = "0.2.112", default-features = false }
1920
log = "0.4.8"

0 commit comments

Comments
 (0)