cfallin requested fitzgen for a review on PR #12942.
cfallin opened PR #12942 from cfallin:gc-growth-heuristic to bytecodealliance:main:
This implements the heuristic discussed in #12860: it replaces the existing behavior where Wasmtime's GC, when allocating, will continue growing the GC heap up to its size limit before initiating a collection. That behavior optimizes for allocation performance but at the cost of resident memory size -- it is at one extreme end of that tradeoff spectrum.
There are a number of use-cases where there may be heavy allocation traffic but a relatively small live-heap size compared to the total volume of allocations. For example, lots of temporary "garbage" may be allocated by many workloads. Or, more pertinently to #12860, a C/C++ workload that uses the underlying GC heap only for exceptions, and uses those exceptions in a way that only one
exnrefis live at a time (noexnobjects are stashed away and used later), will also generate a lot of "garbage" during normal execution. These kinds of workloads benefit significantly from more frequent collection to keep the resident-set size small. This also may benefit performance, even accounting for the cost of the collection itself, because it keeps the footprint of touched memory within higher cache-hierarchy levels.In order to accommodate that kind of workload while also presenting reasonable behavior to large-working-set-size benchmarks, it is desirable to implement an adaptive policy. To that end, this PR implements a scheme similar to our OwnedRooted allocation/collection algorithm (and specified explicitly [here] by fitzgen): we use the last live-heap size (post-collection) compared to current capacity to decide whether to grow or collect. When the current capacity is more than twice the last live-heap size, we collect first; if we still can't allocate, then we grow. Otherwise, we just grow.
The idea is that (when combined with an exponential heap-growth rule) the continuous-allocation case will collect once at each power-of-two, then grow; this is "amortized constant time" overhead. A case with a stable working-set size but with some ups and downs will never hit a "threshold-thrashing" problem: the heap capacity will tend toward twice the live-heap size, in the steady state (see proof here for the analogous algorithm for
OwnedRooted). Thus we have a nice, deterministic bound no matter what, with no bad (quadratic or worse) cases.This PR adds a test that creates a bunch of almost-immediately-dead garbage (allocates a GC struct that is only live for one iteration of a loop) and checks the heap size at each iteration. To allow this check, it also adds a method to
Storeto get the current GC heap capacity, which seems like a generally useful kind of observability as well.[here]: https://github.com/bytecodealliance/wasmtime/pull/12860#issuecomment-4158147842
[proof]: https://github.com/bytecodealliance/wasmtime/blob/e5b127ccd71dbd7d447a32722b2c699abc46fe61/crates/wasmtime/src/runtime/gc/enabled/rooting.rs#L617-L678<!--
Please make sure you include the following information:
If this work has been discussed elsewhere, please include a link to that
conversation. If it was discussed in an issue, just mention "issue #...".Explain why this change is needed. If the details are in an issue already,
this can be brief.Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.htmlPlease ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->
cfallin requested wasmtime-core-reviewers for a review on PR #12942.
fitzgen created PR review comment:
Especially since
layout.size()is being used here on increments.
fitzgen created PR review comment:
Nitpick: this isn't quite the same as "live" since there may be unreachable objects we just haven't collected yet.
fitzgen submitted PR review.
fitzgen created PR review comment:
This function's behavior seems weird to me now. In particular, it is strange that we would always grow if
bytes_neededis provided, even when collection freed up more than enough space in the GC heap.I guess what I would expect is something like this:
if caller is Store::gc || post_gc_allocated_bytes < gc_heap_capacity / 2 { collect(); if bytes_needed > gc_heap_capacity - post_gc_allocated_bytes { grow(); } } else { grow(); }And I'm not even 100% sold on always-collect-if-the-caller-is-
Store::gcbut if we do want to have that behavior, then we probably want to pull this apart into multiple functions (collect-or-grow vs collect-and-then-grow-if-necessary) and just haveStore::gccall the latter.
fitzgen created PR review comment:
The free list can round sizes up to its min size and min alignment, so it is more accurate to do the following:
let size = self.index(drc_ref).object_size(); let layout = FreeList::layout(size); self.allocated_bytes -= layout.size();
github-actions[bot] added the label wasmtime:api on PR #12942.
github-actions[bot] added the label wasmtime:ref-types on PR #12942.
github-actions[bot] commented on PR #12942:
Subscribe to Label Action
cc @fitzgen
<details>
This issue or pull request has been labeled: "wasmtime:api", "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>
cfallin updated PR #12942.
cfallin updated PR #12942.
cfallin submitted PR review.
cfallin submitted PR review.
cfallin created PR review comment:
Clarified.
cfallin submitted PR review.
cfallin created PR review comment:
So this one is a little confusing to me too wrt intent. Note that this function's only caller is
Store::gc, so we can safely const-prop your OR-condition; then the intent is I guess "always collect, then grow if request is greater than likely available size". I've updated to this and renamed tocollect_and_maybe_grow_gc_heapto hopefully make that clearer.
cfallin created PR review comment:
Done!
cfallin updated PR #12942.
cfallin commented on PR #12942:
Updated; thanks!
fitzgen submitted PR review:
Thanks!
fitzgen created PR review comment:
Indentation looks like it got messed up here
cfallin created PR review comment:
Hmm, actually, there's something weird in the current test failure that removing the
if bytes_needed > ...conditional resolves -- will investigate more next week...
cfallin submitted PR review.
cfallin commented on PR #12942:
@fitzgen I looked into the test failure here and it seems to be kind of a fundamental mismatch between the
nullcollector's inline path andStore::gc's contract (here) that allocation may not succeed even after GC.The DRC collector's allocation path goes through
retry_after_gc_async(via thegc_alloc_rawlibcall), which implements the new two-level retry behavior in this PR, which (given the algorithm) is guaranteed to grow if more space is needed. However, the null collector's inline path (NullCompiler::emit_inline_alloc) only calls thegrow_gc_heaplibcall (which callsStore::gcwithbytes_needed) and then retries allocation once. That's not sufficient to cause a growth to occur with the new algorithm, as long as your requested heuristic) is present.I think the actual bug here appears to be the mismatch in contracts between the inline alloc path and
Store::gc's contract, but it also seems like we shouldn't be duplicating the growth heuristic or retry logic multiple times. I kind of prefer restoring the oldStore::gcbehavior to keep the null allocator working but could either (i) refactor the null allocator to have a double-retry inline or (ii) remove the inline allocation path altogether, and make the same libcall as the DRC collector, if you prefer.
fitzgen commented on PR #12942:
However, the null collector's inline path (
NullCompiler::emit_inline_alloc) only calls thegrow_gc_heaplibcall (which callsStore::gcwithbytes_needed) and then retries allocation once. That's not sufficient to cause a growth to occur with the new algorithm, as long as your requested heuristic) is presentI think the
grow_gc_heaplibcall should just grow the GC heap directly, not try to do it indirectly throughstore.gc(Some(bytes_needed))orstore.grow_or_collect_gc_heap. We should just makeStoreOpaque::grow_gc_heapincrates/wasmtime/src/runtime/store/gc.rsbepub(crate)and call it from thegrow_gc_heaplibcall.How does that sound?
cfallin updated PR #12942.
cfallin updated PR #12942.
cfallin commented on PR #12942:
Ah, right, that's way simpler -- done!
cfallin updated PR #12942.
cfallin updated PR #12942.
cfallin has enabled auto merge for PR #12942.
fitzgen submitted PR review:
Thanks!
cfallin updated PR #12942.
cfallin has disabled auto merge for PR #12942.
cfallin commented on PR #12942:
There's a weird failure with the GC oom test that wasn't there before -- will try to bottom this out later when I have some more time.
cfallin updated PR #12942.
cfallin has enabled auto merge for PR #12942.
cfallin added PR #12942 GC: implement a grow-vs-collect heuristic. to the merge queue
cfallin removed PR #12942 GC: implement a grow-vs-collect heuristic. from the merge queue
cfallin merged PR #12942.
Last updated: Apr 12 2026 at 23:10 UTC