Repository navigation
perf: Array-subclass numeric indexing leaves Wolf ECS hot loop 191x slower than Node #8655
Description
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-perryMeasurement
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
.lengthresolution 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
2000in Node and Perry. - Prove in emitted IR/native-region artifacts that the inner loop does not call
js_dyn_index_getor redo constant.lengthkey 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, andpiecs/simple_iterwith semantic parity, RSS, and executable-size deltas reported.
Activity