-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement wasmtime GC tracing without libunwind #2459
Comments
One advantage of both of these schemes that I missed above is:
Thus this will likely be an additional performance improvement (on top of faster stack-walking) in code that only sparsely uses reference types. |
Agreed. We can even use a guard page to avoid explicit capacity checks for option 2, as long as we always push in the right order. |
The other purpose of dwarf-based unwinding is that we can get a full stack trace on traps, but I think most developers generally expect traps to be slow-ish and not used in performance-sensitive code. In that sense if the main concern here is gc of references and correctness we can focus on just solving that one problem rather than a more general problem of reconstructing the call stack. This is what both proposed solutions basically do (assuming in the first the push is elided if the frame has no stack map), but figured I'd point it out! One possible way to optimize your first thought would be to first assign a constant number to all stack maps (perhaps function-local too, so when compiling a function we know what number a stack map gets). The linked list entry would then contain a previous pointer, the function's global number, the stack pointer (which presumably doesn't change during function execution), and a slot for the local number within a function. Then when a safepoint happens we'd update the local number slot but that's it, ideally keeping traffic pretty low? (and this would also avoid baking in pointers and it'd be up to the stack walker to translate indices to actual stack maps). This could also presumably all be skipped for functions having no reference types. I would personally lean towards as little complexity as possible, though, so option 2 sounds pretty good to me as well. One extra possible cost though is that the base pointer of the shadow stack to push to would need to be recalculated after each function call in case the function call relocated it. That may be unconditional new memory traffic we can't get away from? |
Indeed, for maximal clarity I should point out that I wouldn't expect
Combining these two thoughts led me to: wouldn't it be nice to just build this on the regular stack, where these problems are already solved ... which I think could actually be possible (let's call this "Option 2.5"): push refs on the real stack at any safepoint, in a contiguous block; push the block size; link this chunk into a list of chunks by reading I'm indifferent on Option 2.0 vs. Option 2.5 (adds complexity in one place, removes it from another); just thought I would add this to the mix. |
That's actually a good point that having a switch to turn off dwarf unwinding info would be quite nice! Anyway back to this isssue, I like the idea of using an alloca-equivalent rather than having to manage two stacks, it should help make the prologue a bit cheaper ideally and is one less stack to manage. |
(the other part of this issue is I guess discussing ways to avoid the need for stackmaps entirely, but the toplevel problem description is basically just "make do without libunwind") |
Yes, this issue should be resolved, but I'll leave it open until I can verify whether we can disable dynamically registering unwind info without breaking |
I'm actually going to close this as complete, and handle the unwind info and all that in #4554 |
In wasmtime, GC tracing of reftype pointers currently works by using
libunwind
to iterate over stack frames, fetching a stackmap for each relevant PC and finding the stack slots with live pointers.This works perfectly fine, but is potentially slower than we would like, because
libunwind
relies on DWARF info to understand stack frames. It is also more complex -- it relies on DWARF generation and interpretation to be correct -- which increases risk a little because GC-tracing bugs can lead to various security issues.In contrast, many other high-performance JITs use explicit data structures of some sort on the stack so that tracing stack roots boils down to walking a linked list of some sort. For example, SpiderMonkey has a strict JIT-frame discipline allowing fast iteration (different from the system ABI), and V8 indirects object references through
InstanceHandle
s that link themselves into a list on the thread context.We should look into designing a mechanism that maintains a stack of frames reachable from the
vmctx
and walkable without any metadata (aside from the stackmaps). Two options that come to mind are:Option 1: Shadow Stack of (SP, Stackmap) Tuples
Maintain a shadow stack (with top and limit pointers in
vmctx
) of(stackmap, SP)
tuples. On function entry, allocate a tuple. At every safepoint, ensure that the stack pointer and stackmap for that safepoint are up-to-date in the tuple. Walking the stack for GC roots then simply requires (i) looping over these tuples, and (ii) tracing references at offsets indicated by the stackmap.Some advantages of this scheme are:
Some disadvantages of this scheme are:
Option 2: Shadow Stack of References
Maintain a shadow stack that actually stores spilled references. On function entry, bounds-check that there is enough shadow-stack space for the maximal live-set of reftyped values at any safepoint in the function (this is statically known). At any safepoint, push all live reftyped values to the shadow stack; after the safepoint, restore them (if we implement a moving GC that may edit pointers) or bulk-pop them by bumping the top pointer.
Some advantages of this scheme are:
regalloc.rs
; in other words, it is very simple and easy to verify. While we are pretty confident in the reftypes implementation at least in the backtracking allocator at this point, less complexity is always good; and it lowers the bar for adopting other register allocators in the future, if we choose to do that.&[PointerT]
(slice of contiguous live pointers). This is even better than walking a potentially sparse stackmap looking for set bits.A disadvantage of this scheme is:
I tentatively prefer Option 2, but I can see both options as viable. Thoughts?
cc @fitzgen
The text was updated successfully, but these errors were encountered: