Skip to content

perf(codegen): propagate Ptr<Shape> facts into guarded argument clones #8774

Description

@proggeramlug

Summary

Perry's Ptr<Shape> proof stops when a known-shape object is passed as a non-receiver call argument. The callee therefore performs guarded field ICs and repeated GC-header/shape-descriptor reads even when a monomorphic call site proves the argument's class.

Generalize the existing clone-and-route mechanism for proven this receivers to selected object argument positions: a guarded call should route a known-shape argument into a callee clone that can directly access its declared fields while retaining the tagged ABI, shadow rooting, and generic fallback.

Self-contained reproduction

class Entity {
  constructor(id) {
    this.id = id;
    this.components = [];
  }
}

class Registry {
  add(entity, component) {
    entity.components.push(component);
  }

  hash(entity) {
    let value = entity.id;
    for (let i = 0; i < entity.components.length; i++) {
      value += entity.components[i];
    }
    return value;
  }

  clear(entity) {
    entity.components.length = 0;
  }
}

const registry = new Registry();
let checksum = 0;
const iterations = 200_000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
  const entity = new Entity(i);
  registry.add(entity, 1);
  registry.add(entity, 2);
  checksum += registry.hash(entity);
  registry.clear(entity);
}
console.log(JSON.stringify({
  elapsedMs: performance.now() - start,
  checksum,
}));

Expected checksum: 20000500000.

Build with release artifacts and retain --trace llvm --opt-report=json --explain-lowering --no-cache output. The public integration case is ddmills/js-ecs-benchmarks, perform-ecs@0.7.8, suite Destroy.

Current evidence

The fresh lowering report for the real workload on Perry 20a388974d7333ccaec9da9175fbcee54ad49902 diagnoses the exact loss in perform-ecs/src/ECS.ts, closure#7:

local: entity
candidate class: Entity
outcome: denied
rule: rule 2 (containment)
reason: passed as a call argument. There is no mechanism yet by which a
shape fact at a call site becomes a fact about the callee's parameter.

The timed M1 profile collected 701 main-thread samples. shape_descriptor_by_id was the top leaf in 68 samples (9.7%) and try_read_tracked_gc_header in 62 (8.8%). Within ECS.addComponentsToEntity$pshape, field-get IC misses accounted for 47 samples in the create path plus 14 and 22 in the two later add paths (83 inclusive samples, 11.8% of the profile). The full case remains 13.268x slower than Node.

The closed design/scoping issue #7034 explicitly identified argument-position clone-and-route as the next Ptr<Shape> phase; it did not implement it. #8693/#8737 applies the same general idea to the receiver (this) position only.

Proposed direction

  • For a direct/guarded call whose argument has a proven class/shape, build or select a callee clone annotated with a Ptr<Shape> fact for that parameter.
  • Start with non-exported or producer-known callees and monomorphic argument positions; require all routed call sites to agree, or guard each call site and retain the boxed generic entry.
  • Keep the public machine ABI tagged. On clone entry, shadow-bind the tagged parameter slot exactly as proven-this clones do, and re-derive the raw pointer from the rewritten slot after every safepoint.
  • Consume the parameter proof in direct declared-field loads/stores, then allow downstream inlining and scalar replacement to use the same fact.
  • Record argument index, producer provenance, selected shape/class, guard, clone, and fallback in --explain-lowering.

Semantic and GC constraints

  • A runtime TypeScript annotation is not proof; wrong-class, subclass, proxy, cross-realm, or mutated-shape values must take the generic fallback.
  • Preserve aliasing, identity, accessors, private brands, prototype mutation, exceptions, and method replacement semantics.
  • A shadow root must provide liveness, relocation, and the value actually observed after GC. Do not copy the non-movable typed-array-pointer shortcut: ordinary objects move.
  • Do not keep raw object pointers at rest or across calls/safepoints; write barriers remain required for pointer stores.

Acceptance criteria

  • Register the reproduction as semantic/compiler-output coverage; Node and Perry print checksum 20000500000, normally and under forced-moving GC.
  • The stable Registry.add/hash/clear clones carry a proven Entity argument and access id / components directly without field-get IC diamonds or shape_descriptor_by_id on the fast arm.
  • The tagged generic entry and explicit guard-failure fallback remain present and are exercised.
  • Negative tests cover multiple caller shapes, subclassing, shape mutation, accessors, proxies, alias/reassignment, exceptions, imports/re-exports, and forced-moving GC.
  • --explain-lowering no longer reports call-argument containment as the denial for the valid fixture and names every selected argument clone.
  • Re-run perform-ecs/destroy with exact component-ID/view-count parity. Require at least a 10% median Perry improvement and 9/11 wins on the quiet M1 protocol; report shape/header/IC profile samples, RSS, and executable size.

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