tschneidereit requested fitzgen for a review on PR #13822.
tschneidereit requested wasmtime-core-reviewers for a review on PR #13822.
tschneidereit opened PR #13822 from tschneidereit:gc-perf-improvements to bytecodealliance:main:
This is a set of commits which, combined, substantially improve GC performance, at least as measured for one mid-sized Kotlin component I've been testing. (Which I unfortunately can't share.)
While single-threaded performance improves substantially with the first commit, the other two only make a real difference when running multiple instances in parallel. In total, performance for 20 concurrent requests improves from 168 RPS to 17680 RPS:
rev concurrency=1 concurrency=20 main239 168 initial-heap-size=2MB 1549 1809 batched-trace-info 1565 4050 cached-subtype-checks 1610 17681
tschneidereit commented on PR #13822:
One thing I'm wondering is if we should choose a different default for the initial GC heap size allocation. Perhaps we should set that to at least one wasm page?
tschneidereit updated PR #13822.
tschneidereit commented on PR #13822:
One thing I'm wondering is if we should choose a different default for the initial GC heap size allocation. Perhaps we should set that to at least one wasm page?
Testing this just now, it doesn't seem to really make much of a difference. Which I guess makes some sense: setting the initial size to 64kb will only ever remove the first, cheapest growth cycle.
tschneidereit updated PR #13822.
:memo: alexcrichton submitted PR review:
Overall this looks pretty reasonable to me, and are definitely levers that were expected to be slow points about the GC implementation (primarily all the locking).
Personally I'd like to ideally consider completely redesigning how
TraceInfois managed (the second commit here). To me this seems like something we should precompute at module-creation-time and not require any locks at all during instantiation/runtime. As-is that's not going to be an easy refactor, however.I also think that this is something where we're going to need to bust out
unsafeand raw pointers. For example what I'm thinking is thatModules (andRegisteredTypes) should haveTraceInfoprecomputed along with a map fromVMSharedTypeIndexto this information. When instantiating within a store this shouldn't require cloningTraceInfobecause that'll just slow down instantiation, especially when there are a lot of types. The module/RegisteredTypeis already strong-held by the store as well, so using moreArcs is also extra overhead (e.g. cloning a contendedArcis not the cheapest).Basically the second commit here I'd like to ideally separate out and take some time to figure out what we "really want to do" w.r.t. performance there.
The third commit I think is another in this category, but the true fix is something we know about and is also pretty gnarly. As a temporary fix for performance the hash-map-cache seems reasonable.
So, tl;dr; I think it's fine to land the 1st/3rd commits here with light modifications, but I'd prefer to take more time to figure out what to do about the 2nd commit.
:speech_balloon: alexcrichton created PR review comment:
Would it be possible to model
ensure_trace_infoas a wrapper around this function instead of the other way around? That way implementations are guaranteed to, by default, implement the bulk registration.
:speech_balloon: alexcrichton created PR review comment:
I think this'd be best stored in
Tunablessogc_heap_memory_type()could also be updated as-is to return a memory with the appropriate minimum set. Implementation/feature-wise this all looks fine though, so just shuffling around where this is defined.
:speech_balloon: alexcrichton created PR review comment:
Could this use
NopHasherwe have elsewhere perhaps?
:memo: fitzgen submitted PR review:
Thanks! A few requested tweaks.
:speech_balloon: fitzgen created PR review comment:
Can we store this in the tunables, where we store the other similar knobs for memories and the GC heap?
Also, what happens if the
gc_heap_initial_sizeis larger than thegc_heap_reservation? It seems like maybe we actually just want to always make this beround_down(gc_heap_reservation, DEFAULT_WASM_PAGE_SIZE)and adjust theTunables::gc_heap_memory_typemethod to have its initial/min size reflect that reservation size. This would, I think, give us the wins this commit is aiming for without adding a new / partially redundant config knob.
:speech_balloon: fitzgen created PR review comment:
As an additional potential follow up: we could probably make this register even fewer trace infos by making it so that if the collector keeps small trace infos inline in the GC header (which the copying collector does) then we don't actually register that type in the
TraceInfosregistry. Would need a little API design work since different collectors do/don't store tracing data inline the GC header, and theoretically they could also have different capacity for the size of trace info data that can be stored inline in the header. But it is worth looking into.
:speech_balloon: fitzgen created PR review comment:
Could we factor this cache out into its own type to try and encapsulate some of the details from the rest of the
StoreOpaque(which is massive, and I don't want to keep making it more massive), something like this:// is_subtype_cache.rs pub(crate) struct IsSubtypeCache { cache: crate::hash_map::HashMap<u64, bool, GcTypeCacheHasher>, } impl IsSubtypeCache { pub(crate) fn is_subtype(&mut self, engine: &Engine, sub: VMSharedTypeIndex, sup: VMSharedTypeIndex) -> bool { // ... } } // store.rs impl StoreOpaque { pub fn is_subtype(&mut self, sub: VMSharedTypeIndex, sup: VMSharedTypeIndex) -> bool { self.is_subtype_cache.is_subtype(&self.engine, sub, sup) } }Also, do we want this to cache's size to grow unbounded? Maybe we want to use an LRU cache like https://docs.rs/associative-cache/latest/associative_cache/ instead?
:speech_balloon: fitzgen created PR review comment:
Also, if we do end up keeping this new config knob, we want to also hook it up to our fuzz config in
crates/fuzzing/src/generators/config.rs
github-actions[bot] added the label wasmtime:api on PR #13822.
github-actions[bot] added the label wasmtime:config on PR #13822.
github-actions[bot] added the label wasmtime:ref-types on PR #13822.
github-actions[bot] commented on PR #13822:
Subscribe to Label Action
cc @fitzgen
<details>
This issue or pull request has been labeled: "wasmtime:api", "wasmtime:config", "wasmtime:ref-types"Thus the following users have been cc'd because of the following labels:
- fitzgen: wasmtime:ref-types
To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.
Learn more.
</details>
github-actions[bot] commented on PR #13822:
Label Messager: wasmtime:config
It looks like you are changing Wasmtime's configuration options. Make sure to
complete this check list:
[ ] If you added a new
Configmethod, you wrote extensive documentation for
it.<details>
Our documentation should be of the following form:
```text
Short, simple summary sentence.More details. These details can be multiple paragraphs. There should be
information about not just the method, but its parameters and results as
well.Is this method fallible? If so, when can it return an error?
Can this method panic? If so, when does it panic?
Example
Optional example here.
```</details>
[ ] If you added a new
Configmethod, or modified an existing one, you
ensured that this configuration is exercised by the fuzz targets.<details>
For example, if you expose a new strategy for allocating the next instance
slot inside the pooling allocator, you should ensure that at least one of our
fuzz targets exercises that new strategy.Often, all that is required of you is to ensure that there is a knob for this
configuration option in [wasmtime_fuzzing::Config][fuzzing-config] (or one
of its nestedstructs).Rarely, this may require authoring a new fuzz target to specifically test this
configuration. See [our docs on fuzzing][fuzzing-docs] for more details.</details>
[ ] If you are enabling a configuration option by default, make sure that it
has been fuzzed for at least two weeks before turning it on by default.[fuzzing-config]: https://github.com/bytecodealliance/wasmtime/blob/ca0e8d0a1d8cefc0496dba2f77a670571d8fdcab/crates/fuzzing/src/generators.rs#L182-L194
[fuzzing-docs]: https://docs.wasmtime.dev/contributing-fuzzing.html
<details>
To modify this label's message, edit the <code>.github/label-messager/wasmtime-config.md</code> file.
To add new label messages or remove existing label messages, edit the
<code>.github/label-messager.json</code> configuration file.</details>
fitzgen commented on PR #13822:
Also I'd be curious whether you saw/see any perf improvements on Sightglass's GC benchmarks:
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
The reason I didn't make it a tunable is that it doesn't affect compilation, and can be tweaked at runtime if so desired. I agree that it's in a bit of a gnarly location as-is, though.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
but I do like the idea of
round_down(gc_heap_reservation, DEFAULT_WASM_PAGE_SIZE), so maybe that's just the answer and the option can go away entirely.
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
See the conversation over here
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
err, wrong thread
:memo: tschneidereit submitted PR review.
:speech_balloon: tschneidereit created PR review comment:
See the conversation over here
tschneidereit commented on PR #13822:
Personally I'd like to ideally consider completely redesigning how
TraceInfois managed (the second commit here). To me this seems like something we should precompute at module-creation-time and not require any locks at all during instantiation/runtime. As-is that's not going to be an easy refactor, however.I should've mentioned that I looked at that and then decided that there's no way we'd backport it, so focused on something much smaller ... and then forgot to note that the real fix should happen as follow-up work.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
I've moved this part of this PR over to https://github.com/bytecodealliance/wasmtime/pull/13841 where I've hooked up fuzzing and configured a tunable. FWIW though this setting can affect codegen because a static offset known to be beneath the threshold of the minimum size of a linear memory can avoid the bounds check entirely for example.
Also, for the initial value, VM-wise there's no consequence to setting this equal to
gc_heap_reservation, but store-resource wise there's still consequence. For example the resource limiter will think that an allocation of 4GiB is being made ifgc_heap_initial_sizematchesgc_heap_reservation. Effectively by increasing the initial size the embedder loses hooks into growth to limit resource usage.
alexcrichton commented on PR #13822:
Basically the second commit here I'd like to ideally separate out and take some time to figure out what we "really want to do" w.r.t. performance there.
I've created https://github.com/bytecodealliance/wasmtime/pull/13843 for what I was thinking here. Notably instead of batching lock acquisitions I want to eliminate the lock acquisition entirely as I think it's tractable to do so.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
@fitzgen I'd personally prefer to not pick up a whole new dependency for this, and given that the set of types is probably fixed within a store and the max size of this cache is O(n^2) of the size of types, WDYT about leaving it as-is with just a hash map?
:memo: fitzgen submitted PR review.
:speech_balloon: fitzgen created PR review comment:
FWIW, that crate is my own, not just a random crate.
$O(n^2)$ is maybe fine, but maybe not great? I don't know. Have you looked at how many types Kotlin programs are generating? It can be quite a few. Sightglass's
kotlin-richardshas 87 and its a pretty small program. The benchmark program that the Kotlin folks have been filing issues about recently has 7908 types. That's up to 62.5M entries in this cache...The limit on types is 1M, so up to a trillion cache entries, and we don't have any backpressure/limits on the cache's growth -- given a malicious guest, that sounds like a guest-controlled resource growth CVE waiting to happen, no?
The more I think this through, the more I think we should probably not have an unbounded cache here.
We could fix this by simply capping the cache's size at some nearly-always-large-enough max capacity, and refusing to add new entries after that, but at that point why not just use an actual LRU cache? We can probably make the LRU cache's capacity much smaller while still getting 99% of the caching benefits.
:memo: alexcrichton submitted PR review.
:speech_balloon: alexcrichton created PR review comment:
Ok yeah makes sense to me. I've extracted this functionality to https://github.com/bytecodealliance/wasmtime/pull/13860 although I've stayed on
HashMapfor now because we'd need some work to get theassociative-cachecrate integrated, notably getting it working for#![no_std]. I'm also a bit hesitant to invest too much time into this as we know the "real solution", https://github.com/bytecodealliance/wasmtime/issues/13484, has nothing to do with a store-local map/cache. I think that a fixed-size map is probably good enough for most reasonable cases for now for performance and I'm not too worried about the maintenance overhead (just an extra field in a GC-specificfile already)
:cross_mark: alexcrichton closed without merge PR #13822.
alexcrichton commented on PR #13822:
This is now extracted as a combination of:
- https://github.com/bytecodealliance/wasmtime/pull/13841
- https://github.com/bytecodealliance/wasmtime/pull/13843
- https://github.com/bytecodealliance/wasmtime/pull/13860
I'm going to close this as a result, and once the final one lands I'll work on backports.
Last updated: Jul 29 2026 at 05:03 UTC