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

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