Skip to content

perf(codegen): scalar-replace non-escaping aggregate literals #8691

Description

@proggeramlug

Summary

Perry does not scalar-replace short-lived aggregate literals passed through known ECS helper calls. The public perform-ecs/destroy loop allocates fresh arrays and descriptor objects several times per entity:

this.ecs.createEntity([
  { component: Position },
  { component: Velocity },
]);

this.ecs.addComponentsToEntity(entity, [{ component: Position }]);
this.ecs.addComponentsToEntity(entity, [{ component: Velocity }]);

Once the consumers are known/inlined, these aggregates do not need material object/array identity on the common path. Current lowering performs zero scalar replacements, leaving allocation, GC-layout tracking, write barriers, array pushes/gets, and boxing in the hottest 82% of the benchmark.

Add escape analysis and scalar replacement for non-escaping object/array literals across specialized/inlined calls, with explicit materialization when identity or a generic call makes the allocation observable.

Self-contained reproduction

class Position {}
class Velocity {}

let checksum = 0;

function consume(initializers) {
  for (let i = 0; i < initializers.length; i++) {
    const initializer = initializers[i];
    if (initializer.component === Position) checksum += 1;
    if (initializer.component === Velocity) checksum += 2;
  }
}

const iterations = 500_000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
  consume([{ component: Position }, { component: Velocity }]);
  consume([{ component: Position }]);
  consume([{ component: Velocity }]);
}
console.log(JSON.stringify({
  elapsedMs: performance.now() - start,
  checksum,
}));

Expected checksum: 3000000.

Build and retain artifacts:

cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
PERRY_NO_AUTO_OPTIMIZE=1 \
PERRY_RUNTIME_DIR=target/release \
target/release/perry compile repro.js -o repro-perry \
  --trace llvm --opt-report=json --explain-lowering --no-cache

The public integration reproduction is ddmills/js-ecs-benchmarks, perform-ecs@0.7.8, suite Destroy.

Current evidence

For the corrected public workload on Perry 7ad718ab4287641cb2b29dce3a056edc45d4c7f8, the lowering report contains:

  • 494 records;
  • 0 scalar replacements;
  • 61 inserted boxes;
  • 11 emitted barriers and 0 barrier eliminations;
  • 9 dynamic fallbacks;
  • 332 js_value representations versus only 34 i32 and 16 i1 representations.

Materialization reasons are dominated by runtime API edges (46), function ABI (8), and return ABI (7). A symbolized profile shows repeated allocation/GC-layout bookkeeping, generic array operations, GC-header validation, and barriers. The workload remains 33.091x slower than Node; entity creation and component addition account for approximately 82% of its time.

This optimization should follow or consume #8693's guarded direct-call/inlining work for imported methods that capture this; unknown calls must continue to force escape/materialization.

Proposed direction

  • Run escape analysis after specialization/inlining, when actual consumers and field/index uses are visible.
  • Track object and fixed-length array literals field-by-field when they do not escape to a heap store, return, closure, unknown call, reflective operation, proxy, weak reference, or identity comparison.
  • Replace their property/index reads with SSA/native values and eliminate their allocations, layout registration, roots, and barriers.
  • Support path-sensitive materialization at an explicit bailout/escape edge instead of rejecting the entire function when only a cold path observes identity.
  • Start with plain data object literals and fixed-length packed array literals; do not include arbitrary constructors or observable custom prototypes in the first slice.
  • Record escape, scalar-replacement, and forced-materialization decisions and reasons in --explain-lowering and native-region verification.

Semantic constraints

  • Preserve object/array identity when compared, returned, captured, stored, reflected upon, passed to an unknown call, proxied, or used with WeakRef/WeakMap/WeakSet/FinalizationRegistry behavior.
  • Preserve property creation/evaluation order, computed-key side effects, spreads, getters/setters, prototype semantics, exceptions, and array holes/length behavior.
  • Preserve GC root visibility when a value materializes at a side exit or collecting call.
  • Never scalar-replace a value merely because the benchmark consumer happens not to inspect it; the non-escape proof must be general and verifier-backed.

Acceptance criteria

  • Register the reproduction as semantic and compiler-output coverage; Node and Perry must print checksum 3000000.
  • --explain-lowering records scalar replacement of the two descriptor objects and their fixed-length carrier arrays in the optimized fixture.
  • The hot arm contains no heap allocation, array push/get helper, layout-slot registration, or write barrier for those replaced aggregates.
  • Native-region verification rejects a claimed scalar replacement without a complete escape proof and materialization records for all escape edges.
  • Negative tests cover return/store/capture, identity comparison, unknown and collecting calls, reflection, spreads/computed keys, getters/setters, prototypes, proxies, WeakMap/WeakSet/WeakRef where supported, exceptions, and forced-moving GC.
  • Re-run ddmills/js-ecs-benchmarks perform-ecs/destroy with exact component-ID/view-count parity and the complete adapter suite.
  • On the quiet M1 alternating protocol, require at least a 5% median improvement with at least 9/11 wins; report allocations/op or allocation samples, barrier counts, peak RSS, executable size, and scalar-replacement/materialization counts.

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