Skip to content

perf(codegen): loop-version stable packed Array and Array-subclass iteration #8690

Description

@proggeramlug

Summary

After #8655, Perry can hit an inline packed-arraylike path for Array-subclass numeric reads, but it still repeats the full representation proof inside the entity loop. Stable ECS iteration therefore pays header, kind, forwarding, version, bounds, inline/spill, prototype, and fallback checks for every element instead of proving the loop once.

The remaining optimization is general loop versioning: enter a guarded private packed loop from a preheader, execute direct indexed loads in the fast arm, and transfer to the generic loop at the first semantically safe index if a proof changes.

Typed-array read-modify-write is tracked separately in #8692; this issue owns the source Array/Array-subclass iteration and guard placement.

Self-contained reproduction

This read-only variant isolates packed Array-subclass iteration from component TypedArrays:

class Query extends Array {}
class Archetype extends Array {}

const entityCount = 10_000;
const iterations = 2_000;
const query = new Query();
const archetype = new Archetype();
for (let i = 0; i < entityCount; i++) archetype.push(i);
query.push(archetype);

function scan() {
  let sum = 0;
  for (let i = 0, length = query.length; i < length; i++) {
    const current = query[i];
    for (let j = 0, length = current.length; j < length; j++) {
      sum = (sum + current[j]) | 0;
    }
  }
  return sum;
}

let checksum = 0;
const start = performance.now();
for (let i = 0; i < iterations; i++) checksum ^= scan();
console.log(JSON.stringify({ elapsedMs: performance.now() - start, checksum }));

Build and retain LLVM/lowering evidence:

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 original #8655 reproduction should remain a second integration fixture because it matches wolf-ecs and has a stable before/after cohort.

Current evidence

On Perry 7ad718ab4287641cb2b29dce3a056edc45d4c7f8, the original #8655 reproduction improved from 445.038 ms to 70.496 ms, but remains 31.76x slower than Node on an M1 Mac mini.

Fresh LLVM inspection shows:

  • query[i] still calls js_object_get_index_polymorphic after object coercibility and symbol/string/numeric dispatch.
  • current.length uses a generic PIC/property path with repeated class/string/null checks.
  • current[j] has an inline packed-arraylike arm, but repeats header validity, array classification, element kind, forwarding/version, bounds, and inline/spill checks for every entity, with js_packed_arraylike_index_get as fallback.
  • GC poll/root/barrier checks remain in the inner loop even when the fast arm cannot allocate or call arbitrary user code.

The unchanged codehz/ecs query profile shows the same architectural ceiling: after packed method-shape guard work, Perry remains 2.897x slower than Node for accumulation and 6.125x for read-only iteration. The dominant accumulation subtree is repeated method/array proof inside Archetype.forEachWithComponents, not one remaining header-load micro-optimization.

Proposed direction

  • Recognize general counted loops over exact packed Array or Array-subclass inputs with a stable element representation.
  • Emit a preheader that proves receiver/class/shape, descriptor state, prototype/indexed-accessor state, forwarding/version, element kind, holes policy, length, and backing storage once.
  • Enter a private fast loop containing direct length/indexed loads and only the minimal checks required by calls or safepoints inside the body.
  • If the body can mutate the receiver, prototype, or aliased arrays, validate sticky generations after the mutation point and side-exit to the generic loop at the first safe index.
  • Refresh relocated roots after possible collection without discarding unrelated stable proofs.
  • Keep the generic loop as an explicit fallback; do not bake in ECS class names, method names, source locations, or fixed loop sizes.

Semantic constraints

  • Preserve holes, inherited indexed accessors, proxies, species/subclass behavior where relevant, descriptors, prototype mutation, array growth/shrink, element-kind transitions, and exception order.
  • Preserve JavaScript for loop ordering if length, the indexed getter, or the body has side effects.
  • A callback capable of mutating the same array through an alias must either force fallback or trigger safe revalidation.
  • Remain correct when arrays move or grow under forced GC; forwarding compression alone is not a proof that a cached pointer stays valid.

Acceptance criteria

  • Register the read-only reproduction and the original perf: Array-subclass numeric indexing leaves Wolf ECS hot loop 191x slower than Node #8655 reproduction as semantic/compiler-output fixtures.
  • The read-only fast loop contains no js_object_get_index_polymorphic or js_packed_arraylike_index_get, and does not repeat full array classification/shape/prototype checks per element.
  • Native-region artifacts identify the preheader proof, minimal in-loop revalidations, and generic side exit.
  • Mutation tests cover holes, indexed prototype accessors, prototype replacement, descriptors, proxies, growth/shrink, element-kind changes, aliasing from the loop body/callback, thrown getters/callbacks, and forced-moving GC.
  • On the quiet M1 alternating protocol, improve the isolated read-only reproduction by at least 2x and the unchanged ECS accumulation row by at least 2%, each with at least 9/11 paired wins.
  • Re-run the unchanged codehz/ecs suite and noctjs/ecs-benchmark wolf-ecs/simple_iter, reporting Node/Perry medians, semantic checksums, RSS, and executable-size changes.

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