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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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