Repository navigation
Tuning Wasmtime for linear memory + exceptions guests #11256
Description
This issue is a follow up to #11230 and the discussion it spawned around Wasm guests like Rust and C++ that may want to use exceptions but will not use GC, and how we can allow Wasmtime to be tuned for these use cases when embedders know that will not also be running Wasm guests that use GC.
I think it makes sense to open the discussion by enumerating our constraints and desired properties and then analyze potential options through that lens.
Constraints:
-
We must implement the full Wasm exceptions standard. Separately, we can potentially have a (compile-time and/or runtime) configuration where
exnrefs are disabled, for example, but Wasmtime must still provide an implementation of the full spec by default. -
We must be able to collect exception-and-gc-object cycles. When GC is enabled, an exception can contain a GC reference to a (for example) a
structand thatstructcan have a reference to the exception. If either edge is an owning/rooting reference, then the cycle can never be collected. (Note that exceptions by themselves cannot form cycles, since they are immutable.) -
We must only have one exceptions implementation. We do not have the engineering resources to maintain multiple, completely-disjoint implementations of the exceptions proposal. We can add knobs to turn certain subsets of functionality on or off, and tweak things here and there, but we can't realistically swap out the fundamental approach and representations.
Desired properties:
-
Small memory overheads. Fundamentally, exceptions must be stored somewhere and that will require memory. Additionally, they are dynamically sized and can have any number of payload fields, which further complicates things. But we shouldn't have to allocate the equivalent of a second full 4GiB linear memory for every LIME-style guest that uses exceptions and throws a couple of times during its execution, if it even throws at all.
-
Small code size. As always, all else being equal, smaller is better. Ideally we wouldn't have to include code in the binary for allocating
structs, for example, in builds for embedders that will only ever run LIME+exceptions Wasm programs. -
Fast. As always, all else being equal, faster runtime execution is better.
-
Simple. As always, all else being equal, simpler and easier to maintain is better.
-
Secure. Everything we add to Wasmtime must be absent known security vulnerabilities or fundamental flaws -- we would never accept an implementation with known use-after-free bugs or which fundamentally allows Wasm guests to escape the sandbox -- but, all else being equal, implementations with more security assurances are better than those with less assurances.
With that out of the way, I'll open up the issue for any ideas that people have!
Activity
fitzgen commentedon Jul 16, 2025
Existing WIP Exceptions Implementation
So here is an analysis of the current WIP exceptions implementation through the lens of the above constraints and desired properties:
exnref. We will be able to implement the full spec on top of it.structs andarrays, and all ofwasmtime::StructRefet al must be also be enabled when exceptions are enabled.fitzgen commentedon Jul 16, 2025
Reserve N bytes in the
vmctxfor a single exception objectThis is an idea that has been floated in the past. We reserve space in the
Store/vmctxfor an exception, only allow one exception to be live at any time in the wholeStore, and disallow exceptions that do not fit. This is very nice in terms of our desired properties, but because it is so limited, it doesn't actually satisfy our hard constraints.exnrefs live at the same time.vmctxallocation, rather than a new allocation.vmctxaccesses, something we must already do a lot of.fitzgen commentedon Jul 16, 2025
Some kind of simple, non-GC representation of exceptions
There are a couple approaches in this family, notably:
wasmtime_slab::Slab<VMException>orVec<VMException>) hanging off theStore/vmctx.malloced*mut VMExceptions.And these objects could either be refcounted or leaked until the
Storeis dropped.Again, these approaches can rate pretty well in terms of some of our desired properties, depending on the exact details, but they cannot satisfy constraints 2 and 3 at the same time.
mallocs. If the side table is fixed size, could even be inlined into theVMStoreContextallocation.Storeis dropped, then we would need only minimal additional runtime infrastructure. If they are reference counted, however, then we would need to do deferred reference counting, just like our existing DRC collector, or else objects in frames that we skip over when trapping would be leaked. That implies roughly as much runtime code as Wasm GC does.Vec::push.Storeis dropped, but either way this code is only additive and doesn't reuse existing infrastructure. Even worse if we are creating new variants of all of deferred reference counting.fitzgen commentedon Jul 16, 2025
Extension to current implementation: add config knobs for GC heap memories that are separate from linear memory config knobs
This alleviates on of the weaknesses of our current implementation by allowing the memories used for GC heaps to be configured separately from regular linear memories.
The pooling allocator would also need some small updates: right now it assumes that GC heap memories and linear memories are identical and uses the same pool for both. That would need to become an optimization or config option and when the two kinds of memories have different configurations the pooling allocator would need to have a separate pool for GC heap memories.
fitzgen commentedon Jul 16, 2025
Extension to current implementation:
#[cfg]-gatestructandarrayseparately from the core GC runtimeRight now,
#[cfg(feature = "gc")]controls whether a build includes bothStorecan have an associated GC heap; note that each collector already has its own feature flag), andstructandarrayobjects in particular and their reflections in host APIs likewasmtime::StructRefandwasmtime::ArrayRef.The current exceptions implementation only requires the former, and does not need the latter. We could tweak our
cfgfeature flags and our uses of them such that we could include the core GC runtime in a build but not any of thestructandarrayspecifics.fitzgen commentedon Jul 16, 2025
Finally, it is also worth noting that the previous two extensions of the current implementation are compatible with each other, so if we consider them together, then we get the following rating, which is pretty good along all of our dimensions:
cfallin commentedon Jul 16, 2025
Thanks very much for writing out all of these tradeoffs explicitly! This indeed captures all of the reasoning behind #11230 and much more, and more clearly than I laid out there.
I'll add a personal note that I went through many of these options in sequence as I was working out a design and I am sympathetic to the desire to find "something simpler" -- it doesn't feel at first like GC should be so tightly coupled. Unfortunately, in an engine that has both, it does seem to be that way.
I think the only thing I would add is that, with respect to the last option ("
#[cfg]-gatestructandarrayseparately") it is absolutely worth measuring, but I suspect the savings here might not be as much as one would hope: almost all of the machinery is in shared bits (GC algorithm but also e.g. the engine type registry); exceptions are "just another enum arm" in most places; and the host API layer will mostly fall away via link-time per-function GC if not used by the embedder. That said, I share your hunch that a small GC algorithm shouldn't add that much code-size. And to the extent that it does, that mechanism is what one would need to build anyway (just as you say: "That implies roughly as much runtime code as Wasm GC does."!).alexcrichton commentedon Jul 16, 2025
Agreed thanks for writing this all up!
My intuition matches what y'all are reaching as well I think, which is that the most viable path forward is combining knobs for GC memory with more aggressive #[cfg]. What I might add to the mix here though is the concept of a new GC entirely (e.g. implementation of
GcRuntime). Everything in Wasmtime is already factored for multipleGcRuntimeimplementations so I wouldn't consider this too too large of a maintenance burden, and I think we could codify some simple rules for a GC that makes it unsuitable for general-purpose usage but suitable for "low runtime cost and low runtime footprint":This would make GC allocation and GC operations practically trivial and my guess is that this would reduce a lot of the hypothetical overhead of the full-blown DRC collector for all exnref objects. Naturally though I'd also say that we should measure the different, for example, between the code footprint of the null collector and drc collector since that's sort of the upper and lower bounds of this hypothetical new collector.
In the near-term though I continue to feel that the order, in decreasing priority, things should happen are (1) implement the full proposal with everything integerated (aka the current trajectory), (2) work on separate GC knobs to make the runtime impact as low as we can, or basically don't require a second linear memory just for setjmp/longjmp in practice, and (3) follow-up with work to reduce the runtime footprint of the setjmp/longjmp use case.
creativitysurvey commentedon May 29, 2026