Skip to content

perf: Array-subclass numeric indexing leaves Wolf ECS hot loop 191x slower than Node #8655

Description

@proggeramlug

Summary

Numeric iteration over stable Array subclasses stays on Perry's fully generic dynamic-index/property machinery. A self-contained current-main repro is 191.1x slower than Node on an M1 Mac mini even though both produce the same checksum.

This is the dominant shape behind the public wolf-ecs iteration results. The newest class PRs do not address the hot loop: #8653 accelerates safe class-field initialization, #8649 removes some derived-constructor scope setup, and #8645 restores Array-subclass correctness. The repeated numeric reads and stable .length accesses remain generic after setup.

Tested with current main c2da03439e6e848cfed73163c131acb601ae96dc (Perry 0.5.1519) and Node 26.5.1.

Self-contained repro

class Query extends Array {
  archetypes = this;
}

class Archetype extends Array {
  entities = this;
}

const entityCount = 1_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);

const components = new Uint32Array(entityCount);

function system(values) {
  for (let i = 0, length = query.length; i < length; i++) {
    const current = query[i];
    for (let j = 0, length = current.length; j < length; j++) {
      values[current[j]] += 1;
    }
  }
}

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

Build from a Perry checkout:

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

Measurement

Machine: M1 Mac mini, 8 GiB, macOS 26.5.1. Three process warmups, then 11 alternating Node/Perry samples. The timed region is inside each process; both runtimes report checksum: 2000.

Runtime median CV relative MAD
Node 26.5.1 2.329 ms 0.96% 0.57%
Perry 0.5.1519 445.038 ms 0.40% 0.10%

Perry slowdown: 191.06x.

The upstream noctjs/ecs-benchmark wolf-ecs/simple_iter case is worse: the full auto-optimized sweep measured Perry 1,372.02x slower than Node (95% bootstrap interval 1,369.44-1,381.39x).

Native profile from the upstream case

A symbolized 5-second sample of wolf-ecs/simple_iter captured 3,772 main-thread samples:

  • four shape/property functions: 30.1% of all leaf samples;
  • js_dyn_index_get: 8.9%;
  • the three compiled workload closures combined: about 3%.

The remaining leading frames repeatedly classify typed arrays/arguments/class objects, create and convert key strings, hash/compare keys, validate GC headers, create runtime handle scopes, and allocate. The component arithmetic itself is not the bottleneck.

The relevant Wolf shape is the same as the repro: a captured Query extends Array, query[i] returning Archetype extends Array, current[j] returning an entity index, and a typed component-array access at that index.

Suggested direction

  • Preserve a guarded Array/Array-subclass element representation for captured and imported bindings.
  • Lower stable packed numeric reads without entering the typed-array, arguments-object, class-object, and dynamic-string-key dispatch chain.
  • Hoist invariant shape/prototype/version guards and .length resolution outside the entity loop, with a semantics-preserving side exit for indexed accessors, proxies, prototype mutation, holes, or element-kind changes.
  • Keep the typed component-array access specialized when its numeric index comes from a guarded packed array.

This is narrower than #5497: the repro is untyped JavaScript and needs runtime-guarded specialization. It also still reproduces after the captured-array fix in #6377/#6369.

Acceptance criteria

  • Add the repro as a native semantic/performance ratchet; checksum must remain 2000 in Node and Perry.
  • Prove in emitted IR/native-region artifacts that the inner loop does not call js_dyn_index_get or redo constant .length key construction/shape lookup per entity.
  • Cover ordinary arrays and Array subclasses, captured and parameter forms, plus guard-failure tests for holes, indexed prototype accessors, prototype mutation, and proxies.
  • Improve the reduced repro by at least 5x on the same host/configuration without regressing ordinary packed-array iteration.
  • Re-run wolf-ecs/simple_iter, becsy/simple_iter, javelin-ecs/simple_iter, and piecs/simple_iter with semantic parity, RSS, and executable-size deltas reported.

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