Open Bug 2032554 Opened 4 months ago Updated 2 months ago

Optimize suspend to not need an allocation

Categories

(Core :: JavaScript: WebAssembly, task, P3)

task

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:

  1. Need a new value representation in our compilers
  2. 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?
  3. Need to update tracing to understand this

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

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.

You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: