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

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