Summary
Perry still lowers a statically specialized Uint32Array element update through three generic runtime helpers per iteration:
js_typed_array_index_get_dynamic
js_dynamic_string_or_number_add
js_typed_array_index_set_dynamic
In the reduced wolf-ecs loop below, that is approximately 6,000,000 helper calls for 2,000,000 component updates. On an M1 Mac mini, current Perry is 31.76x slower than Node even after #8655's Array-subclass indexing improvement.
This issue is specifically about preserving the typed-array representation through a numeric indexed read-modify-write. Guard hoisting and direct iteration over the source Array/Array-subclass are tracked separately in #8690.
Self-contained reproduction
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 \
--trace llvm --opt-report=json --explain-lowering --no-cache
Current evidence
Measured on Perry 7ad718ab4287641cb2b29dce3a056edc45d4c7f8 (0.5.1519) and Node 26.5.1. Current main differs by #8688, a class-semantics/TLS merge that does not implement this optimization.
Machine: Apple M1 Mac mini, 8 GiB, macOS 26.5.1, AC power. Three process warmups followed by 11 alternating Node/Perry processes. Every process reported checksum 2000.
| Runtime |
median |
CV |
relative MAD |
| Node 26.5.1 |
2.219416 ms |
2.23% |
0.29% |
| Perry |
70.495958 ms |
0.078% |
0.041% |
Perry/Node: 31.763x.
The compiler emits a specialized system$spec_ta5x1000 symbol, so the Uint32Array kind and length are known at the function boundary. Nevertheless, the inner loop calls all three helpers above. --explain-lowering records typed-array fallbacks with mutable_alias, unknown_call_escape, and typed_array_fallback=untracked_or_unproven; the indexed set is boxed at a polymorphic helper edge.
Proposed direction
- Recognize fixed-width typed-array read-modify-write expressions such as
values[index] += numericValue after specialization.
- Preserve the typed-array kind, length/backing-store facts, and numeric-index proof through the get/add/set sequence.
- Emit a guarded direct load, JavaScript-correct numeric addition/conversion, and direct store.
- Hoist loop-invariant kind, detachment, backing-store/version, and length guards when legal.
- Retain an explicit semantics-preserving fallback or side exit when any proof fails.
- Record the selection, guards, and fallback in native-region artifacts and
--explain-lowering.
An initial slice may target Uint32Array plus a numeric constant RHS, provided the mechanism and tests are representation-driven rather than source-name- or benchmark-specific.
Semantic constraints
- Preserve evaluation order and abrupt completion behavior for base, key, get, RHS, numeric coercion/addition, and set.
- Preserve
Uint32 wrapping and behavior for negative, fractional, NaN, infinite, and out-of-bounds indices.
- Handle detached/resized backing stores according to Perry's supported TypedArray semantics.
- Do not use the direct path for proxies, accessors, unknown calls that can invalidate the proof, unsupported key representations, or an aliased value that may change kind.
- Remain correct under moving/forced GC; reload any relocatable roots at required safepoints.
Acceptance criteria
- Add the reproduction as a semantic and compiler-output ratchet; Node and Perry must both print checksum
2000.
- The optimized inner-loop arm contains none of
js_typed_array_index_get_dynamic, js_dynamic_string_or_number_add, or js_typed_array_index_set_dynamic.
- Emitted artifacts prove the typed-array guard and an explicit fallback/side exit remain present.
- Tests cover parameter and captured/module-global typed arrays, guard failure, aliasing, bounds, wrapping, detachment/resizing where supported, exceptions/evaluation order, and forced-moving GC.
--explain-lowering reports why the direct RMW was selected or rejected.
- On the same quiet M1 protocol, improve the reduced reproduction by at least 2x versus
7ad718a, with at least 9/11 paired wins and no ordinary packed-array or typed-array regression.
- Re-run
noctjs/ecs-benchmark wolf-ecs/simple_iter, becsy/simple_iter, javelin-ecs/simple_iter, and piecs/simple_iter, reporting semantic parity, medians, RSS, and executable-size deltas.
Related work
Summary
Perry still lowers a statically specialized
Uint32Arrayelement update through three generic runtime helpers per iteration:js_typed_array_index_get_dynamicjs_dynamic_string_or_number_addjs_typed_array_index_set_dynamicIn the reduced
wolf-ecsloop below, that is approximately 6,000,000 helper calls for 2,000,000 component updates. On an M1 Mac mini, current Perry is 31.76x slower than Node even after #8655's Array-subclass indexing improvement.This issue is specifically about preserving the typed-array representation through a numeric indexed read-modify-write. Guard hoisting and direct iteration over the source Array/Array-subclass are tracked separately in #8690.
Self-contained reproduction
Build from a Perry checkout:
Current evidence
Measured on Perry
7ad718ab4287641cb2b29dce3a056edc45d4c7f8(0.5.1519) and Node 26.5.1. Currentmaindiffers by #8688, a class-semantics/TLS merge that does not implement this optimization.Machine: Apple M1 Mac mini, 8 GiB, macOS 26.5.1, AC power. Three process warmups followed by 11 alternating Node/Perry processes. Every process reported checksum
2000.Perry/Node: 31.763x.
The compiler emits a specialized
system$spec_ta5x1000symbol, so theUint32Arraykind and length are known at the function boundary. Nevertheless, the inner loop calls all three helpers above.--explain-loweringrecords typed-array fallbacks withmutable_alias,unknown_call_escape, andtyped_array_fallback=untracked_or_unproven; the indexed set is boxed at a polymorphic helper edge.Proposed direction
values[index] += numericValueafter specialization.--explain-lowering.An initial slice may target
Uint32Arrayplus a numeric constant RHS, provided the mechanism and tests are representation-driven rather than source-name- or benchmark-specific.Semantic constraints
Uint32wrapping and behavior for negative, fractional, NaN, infinite, and out-of-bounds indices.Acceptance criteria
2000.js_typed_array_index_get_dynamic,js_dynamic_string_or_number_add, orjs_typed_array_index_set_dynamic.--explain-loweringreports why the direct RMW was selected or rejected.7ad718a, with at least 9/11 paired wins and no ordinary packed-array or typed-array regression.noctjs/ecs-benchmarkwolf-ecs/simple_iter,becsy/simple_iter,javelin-ecs/simple_iter, andpiecs/simple_iter, reporting semantic parity, medians, RSS, and executable-size deltas.Related work