Skip to content

perf(codegen): direct-call stable methods with short packed spread tails #8772

Description

@proggeramlug

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions