Skip to content

Stack-allocate trivial value_object/value_array argument temporaries - #27610

Draft
dimokol wants to merge 1 commit into
emscripten-core:mainfrom
dimokol:embind-trivial-value-stack
Draft

Stack-allocate trivial value_object/value_array argument temporaries#27610
dimokol wants to merge 1 commit into
emscripten-core:mainfrom
dimokol:embind-trivial-value-stack

Conversation

@dimokol

@dimokol dimokol commented Aug 26, 2026

Copy link
Copy Markdown

Addresses the argument-marshaling side of #27553.

When a value type is trivially constructible and destructible and does not require over-alignment (alignof(T) <= STACK_ALIGN), its argument temporaries no longer round-trip through new T() plus destructor bookkeeping. The registration passes sizeof(T) and a triviality flag; toWireType places the temporary on the wasm stack when the invoker brackets the call in stackSave/stackRestore (a null destructors argument is that contract), zero-filled so unregistered fields and padding match the heap path's value-initialization. The bracket restores the frame in a finally, so a throwing argument conversion or callee cannot leak stack. Over-aligned types keep the heap path, as do callers that defer destruction (emval returns, property setters), Asyncify builds, and JSPI-async invokers, which outlive the frame. Field/element writes skip their per-write destructors array when the element type registers no destructor, which turns out to be the dominant source of per-call garbage in large modules (V8 sinks those arrays in small benchmarks but not at scale). The AOT generator mirrors the type shape so invoker signatures stay in sync, and libsigs.js is regenerated for the new registration parameters.

Measured on box3d.js (a real embind physics binding) rebuilt with this branch, forced-GC heapUsed deltas, median over 9 rounds of 20k calls:

call before after
getter taking one 3-field value_object id 96 B/call 0
getter with id + value_array vec3 argument 208 16
raycast (id, two vec3, filter in; result object out) 179 37
world step (id + two scalars) 125 61

A stock rebuild with an unpatched toolchain reproduces the before column exactly. The remaining nonzeros are value-type returns, which still materialize fresh objects by design and are out of scope here.

other.test_embind, other.test_embind_aot_js, and other.test_embind_no_dynamic pass locally.

Draft because two things are still missing and I'd like direction before writing them: dedicated tests for the new behavior (a trivial POD value type exercising the stack path, plus an allocation regression check if that fits the suite), and the ChangeLog entry. Note the registration arity change means objects built against an older bind.h need a rebuild.

When a value type is trivially constructible and destructible (and
alignof(T) <= STACK_ALIGN), its argument temporaries no longer
round-trip through new T() plus destructor bookkeeping. The
registration passes sizeof(T) and a triviality flag; toWireType places
the temporary on the wasm stack when the invoker brackets the call in
stackSave/stackRestore (a null destructors argument is that contract),
zero-filled so unregistered fields and padding match the heap path's
value-initialization. The bracket is a try/finally, so a throwing
argument conversion or callee cannot leak stack. Callers that defer
destruction (emval returns, property setters) keep the heap path, as
do Asyncify builds and JSPI-async invokers, which outlive the frame.
Field and element writes skip their per-write destructors array when
the element type registers no destructor, the dominant source of
per-call garbage in large modules. The AOT generator mirrors the type
shape so invoker signatures stay in sync ('s' kind), and libsigs.js is
regenerated for the new registration parameters.

Note the registration arity change means objects built against an
older bind.h need a rebuild.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant