Summary
Perry lowers a method call with a spread tail through js_native_call_method_apply_by_id / js_native_call_method_apply even when the callee identity is stable and the spread source is an exact short packed Array. In perform-ecs, every component reset uses:
this._getComponentInstance(component.component).reset(entity, ...component.args);
The common args arrays are empty, but the hot path still performs generic apply dispatch and vtable/ABI trampoline work. Add a guarded direct-call specialization that unpacks a proven exact short packed spread source and invokes the selected method clone directly, with generic iterator/apply fallback for all proof misses.
Self-contained reproduction
class Position {
constructor() {
this.x = 0;
}
reset(entity, delta = 1) {
this.x = entity.id + delta;
}
}
class Velocity {
constructor() {
this.dx = 0;
}
reset(entity, delta = 2) {
this.dx = entity.id + delta;
}
}
const position = new Position();
const velocity = new Velocity();
const empty = [];
const one = [3];
function invoke(instance, entity, args) {
instance.reset(entity, ...args);
}
let checksum = 0;
const iterations = 300_000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
const entity = { id: i };
invoke(position, entity, empty);
invoke(velocity, entity, one);
checksum += position.x + velocity.dx;
}
console.log(JSON.stringify({
elapsedMs: performance.now() - start,
checksum,
}));
Expected checksum: 90000900000.
The public integration reproduction is ddmills/js-ecs-benchmarks, perform-ecs@0.7.8, suite Destroy. Build with release artifacts and retain --trace llvm --opt-report=json --explain-lowering --no-cache output.
Current evidence
On Perry 20a388974d7333ccaec9da9175fbcee54ad49902, the real benchmark measures 13.609234 us/op versus Node 1.025681 us/op (13.268x slower) on the quiet Apple M1 protocol.
In a 701-sample symbolized timed profile, ECS.addComponentsToEntity$pshape reaches js_native_call_method_apply_by_id / js_native_call_method_apply and generic method dispatch for each reset(entity, ...component.args). The spread/reset branch accounts for 80 inclusive samples in entity creation and 36 + 34 in the two component additions: 150/701 samples (21.4%) before subsidiary reset field stores are counted separately.
The lowering report also identifies rest_param as a fixable specialized-ABI denial in this module, but this ticket is about spread-call lowering and callee routing; it does not require changing arbitrary rest-parameter semantics.
Proposed direction
- Recognize direct/guarded method candidates with a fixed prefix plus one spread operand.
- Guard the spread operand as an exact ordinary packed Array with builtin
%ArrayIteratorPrototype%/iterator identity, stable prototype/descriptor generations, supported length, and no holes/accessors/proxy behavior.
- For an initial empty/short length cap (for example 0-4), load the elements under the same version/bounds proof, append them to the fixed argument list, and call the already-selected method clone directly.
- Preserve evaluation order: resolve/evaluate receiver and callee, evaluate fixed arguments, obtain and consume spread iteration in JavaScript order, then call. If any guard fails before observable fast-path effects, enter the existing generic apply path.
- Explain the spread guard, arity case, direct target, and fallback in lowering artifacts.
Semantic constraints
- Array spread uses iteration, not raw indexing in the general case. Overridden
Symbol.iterator, iterator getters, holes, accessors, proxies, subclasses/species behavior, and mutations during iteration must use the generic iterator/apply path.
- Preserve exceptions and evaluation order for receiver/callee, arguments, iterator acquisition/steps/close, and invocation.
- Preserve method identity,
this, default/rest arguments, arity, bound functions, accessors, and call/apply semantics.
- Keep the spread array, receiver, elements, and arguments rooted/reloaded correctly across collecting calls.
Acceptance criteria
- Register the reproduction; Node and Perry print checksum
90000900000, normally and under forced-moving GC.
- Empty and one-element packed fast arms contain neither
js_native_call_method_apply_by_id nor js_native_call_method_apply and call the guarded reset clone directly.
- Explicit generic fallback remains and is exercised by iterator override, accessor/hole, proxy, subclass, length/element mutation, wrong receiver/method, throwing iterator, and oversized spread tests.
--explain-lowering records the selected packed-spread arities, method target, guards, and fallback.
- Re-run
perform-ecs/destroy with exact semantic state. Require at least a 10% median improvement and 9/11 wins on the quiet M1 protocol; report apply/dispatch profile samples, RSS, and executable-size delta.
Related work
Summary
Perry lowers a method call with a spread tail through
js_native_call_method_apply_by_id/js_native_call_method_applyeven when the callee identity is stable and the spread source is an exact short packed Array. Inperform-ecs, every component reset uses:The common
argsarrays are empty, but the hot path still performs generic apply dispatch and vtable/ABI trampoline work. Add a guarded direct-call specialization that unpacks a proven exact short packed spread source and invokes the selected method clone directly, with generic iterator/apply fallback for all proof misses.Self-contained reproduction
Expected checksum:
90000900000.The public integration reproduction is
ddmills/js-ecs-benchmarks,perform-ecs@0.7.8, suiteDestroy. Build with release artifacts and retain--trace llvm --opt-report=json --explain-lowering --no-cacheoutput.Current evidence
On Perry
20a388974d7333ccaec9da9175fbcee54ad49902, the real benchmark measures 13.609234 us/op versus Node 1.025681 us/op (13.268x slower) on the quiet Apple M1 protocol.In a 701-sample symbolized timed profile,
ECS.addComponentsToEntity$pshapereachesjs_native_call_method_apply_by_id/js_native_call_method_applyand generic method dispatch for eachreset(entity, ...component.args). The spread/reset branch accounts for 80 inclusive samples in entity creation and 36 + 34 in the two component additions: 150/701 samples (21.4%) before subsidiary reset field stores are counted separately.The lowering report also identifies
rest_paramas a fixable specialized-ABI denial in this module, but this ticket is about spread-call lowering and callee routing; it does not require changing arbitrary rest-parameter semantics.Proposed direction
%ArrayIteratorPrototype%/iterator identity, stable prototype/descriptor generations, supported length, and no holes/accessors/proxy behavior.Semantic constraints
Symbol.iterator, iterator getters, holes, accessors, proxies, subclasses/species behavior, and mutations during iteration must use the generic iterator/apply path.this, default/rest arguments, arity, bound functions, accessors, andcall/applysemantics.Acceptance criteria
90000900000, normally and under forced-moving GC.js_native_call_method_apply_by_idnorjs_native_call_method_applyand call the guardedresetclone directly.--explain-loweringrecords the selected packed-spread arities, method target, guards, and fallback.perform-ecs/destroywith exact semantic state. Require at least a 10% median improvement and 9/11 wins on the quiet M1 protocol; report apply/dispatch profile samples, RSS, and executable-size delta.Related work
this-capturing class methods, but the spread call still routes through generic apply.