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

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