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

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