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.
Summary
Numeric iteration over stable
Arraysubclasses 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-ecsiteration 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.lengthaccesses remain generic after setup.Tested with current
mainc2da03439e6e848cfed73163c131acb601ae96dc(Perry 0.5.1519) and Node 26.5.1.Self-contained repro
Build from a Perry checkout:
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.Perry slowdown: 191.06x.
The upstream
noctjs/ecs-benchmarkwolf-ecs/simple_itercase 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_itercaptured 3,772 main-thread samples:js_dyn_index_get: 8.9%;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]returningArchetype extends Array,current[j]returning an entity index, and a typed component-array access at that index.Suggested direction
Array/Array-subclass element representation for captured and imported bindings..lengthresolution outside the entity loop, with a semantics-preserving side exit for indexed accessors, proxies, prototype mutation, holes, or element-kind changes.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
2000in Node and Perry.js_dyn_index_getor redo constant.lengthkey construction/shape lookup per entity.wolf-ecs/simple_iter,becsy/simple_iter,javelin-ecs/simple_iter, andpiecs/simple_iterwith semantic parity, RSS, and executable-size deltas reported.