Optimize suspend to not need an allocation
Categories
(Core :: JavaScript: WebAssembly, task, P3)
Tracking
()
People
(Reporter: rhunt, Unassigned)
References
(Blocks 1 open bug)
Details
Attachments
(1 file)
A contref is represented as a ContObject (gc thing). It just holds the inner ContStack which represents the underlying execution resource. Continuations are single-shot, and so resuming a continuation destroys the ContObject by moving the ContStack out of it. A future resume will trap with null. Suspending a ContStack creates a fresh ContObject that holds it for the next resume.
This adds a GC allocation for every suspend/resume pair. An alternative implementation is to turn a contref into a fat pointer pair of (ContStack*, generation). Every time a resume happens, the generation field on the fat pointer is compared to a field on the ContStack. If they match, the resume succeeds and increments the generation field on the ContStack. If they don't match, they trap.
If the generation field is 64-bits, each suspend/resume happens in a single clock cycle, and you're running at 4Ghz, it would take 146 years to overflow the counter. A suspend/resume is likely to take way longer than that. We could just crash if there's ever overflow and not worry about it.
Three difficulties with this:
- Need a new value representation in our compilers
- How to handle 32-bit? Is a continuation a (32-bit, 64-bit) pair there? Do we run out of regs on x86 with that?
- Need to update tracing to understand this
Comment 1•2 months ago
|
||
Made simple experiment: added one cached ContObject on the Instance (GCPtr<JSObject*> recycledCont_, GC-traced). suspend's contNewEmpty returns the cached object instead of allocating, when it's empty (already consumed by the preceding resume); otherwise it allocates and caches. So a single continuation is allocated once and reused for every suspend. There was gain about 57% (~40 → ~17 ns/iter) for sum-bench
Comment 2•2 months ago
|
||
Avoid allocating a fresh ContObject on every suspend. A contref is now a
one-word AnyRef tagged as a String, carrying the ContObject* in the low 47 bits
and a non-zero generation in the high 17 bits (heap pointers use only 47 bits;
see JS::Value). The non-zero generation distinguishes a contref from a real
JSString and makes the reused ContObject single-shot safe: resume validates the
value's generation against the ContObject's and increments it, so a
stale/aliased contref traps. When the generation is exhausted the reused object
is discarded and a fresh one allocated.
suspend reuses the resumed ContObject (stashed on the matched Handlers) instead
of allocating; resume validation is the inlined MWasmResumeValidate MIR node
(untag, generation check + increment, trap on mismatch/null). Contrefs ride the
cold String GC path: TraceEdgeInternal traces the masked ContObject and
re-applies the tag. GC barriers go through AnyRef::toGCThing, which masks a
contref to its (tenured) ContObject cell - a no-op for every other ref - so the
pre-barrier marks the real cell and the post-barrier skips it (it is always
tenured); the inline pre-barrier fast path masks the generation bits likewise.
No new MIRType or StackMap::Kind.
~18ns/iter on the suspend/resume microbenchmark (was ~40ns), beating V8's 27ns.
All wasm/stack-switching and wasm/gc jit-tests pass on 64-bit and 32-bit.
The optimization is 64-bit only; 32-bit falls back to the original
allocate-on-suspend path.
Description
•