Skip to content

perf(codegen): direct guarded typed-array RMW through dynamic numeric indices #8692

Description

@proggeramlug

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

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