Skip to content

[regression] codegen: numeric array-index fast path lost when the array comes from a call — 6.8x slower (v0.5.1255 → main) #6299

Closed

Description

@proggeramlug

Summary

Between v0.5.1255 and current main (0960415ad), codegen stopped emitting the guarded numeric fast path for array element access when the array arrives as the return value of a function call. The access falls back to the fully generic js_array_get_index_or_string / js_typed_feedback_array_set_index_or_string path, costing ~6.8x.

This is a codegen-only regression: the lowered HIR is byte-identical between the good and bad compilers.

Repro

function build(): number[] { const arr: number[] = []; for (let i = 0; i < 250000; i++) arr.push(i); return arr; }
function run(): number {
  const arr = build();                 // <-- array arrives from a CALL
  let checksum = 0;
  for (let iter = 0; iter < 25; iter++) {
    for (let i = 0; i < arr.length; i++) { arr[i] = arr[i] + 1; }
    checksum = checksum + arr[0] + arr[arr.length - 1];
  }
  return checksum + arr.length;
}
const t0 = Date.now();
const r = run();
console.log("result", r, "ms", Date.now() - t0);

Measurement

Both binaries linked against the identical v0.5.1255 release runtime archive, so only codegen differs:

codegen time
v0.5.1255 190 ms
main @ 0960415 1300 ms
node 26 19 ms

Evidence

Lowered HIR for run() is identical between the two compilers (--trace hir --focus run), so this is not a lowering/type-inference change. The emitted LLVM differs:

helper (in run) v0.5.1255 main
js_typed_feedback_numeric_array_index_get_guard 3 2
js_typed_feedback_numeric_array_index_set_guard 2 1
js_array_get_index_or_string 2 3
js_typed_feedback_array_set_index_or_string 1 2

The trigger is narrow: the array must arrive as a call's return value, even with an explicit : number[] annotation on both the local and the callee's return type. An array built inline in the same function, or passed in as a number[] parameter, still gets the fast path.

Decision site: crates/perry-codegen/src/expr/index_get.rs — expr_has_numeric_pointer_free_array_layout (guarded path, :1541) vs the generic fallback (:1483).

Window

11 commits touch perry-codegen/perry-hir in 83f4fcba5..0960415ad. HIR being identical rules out the HIR-only commits (#6270, #6245). The only commit in the window that touches the pointer-locals analysis feeding the predicate is ac5263d (#6253), a +155-line change to crates/perry-codegen/src/collectors/pointer_locals.rs which added MAX_FIXPOINT_ITERS/MAX_FIXPOINT_LOCALS curtailment and broadened the statement walk. That is the prime suspect but has not been confirmed by bisect.

This currently contaminates every array-heavy benchmark, including the ones tracked under #5094.

Activity

  1. proggeramlug commented on Jul 12, 2026

    @proggeramlug
    ContributorAuthor

    Correction to this issue's "Window" section — the suspect and the base were both wrong

    Two premises in the original report do not hold. Recording them so nobody re-walks the dead end:

    1. #6253 / pointer_locals.rs is innocent. Reverting pointer_locals.rs to its pre-#6253 content changes nothing. The suspicion came from it being the only commit in the assumed window touching the pointer-locals analysis — but the window itself was wrong.

    2. The assumed-good base 83f4fcba5 is itself already bad. A bisect anchored on it is worthless: git trusts the good endpoint you hand it, so every rev tested was bad.

    Why the base was wrong (the trap worth knowing): the "known-good v0.5.1255" reference measured against was a prebuilt binary sitting in a worktree's target/ — an artifact built from that worktree's work-in-progress tree, not from the commit the worktree currently points at. Building that same commit from source reproduces the bug. A prebuilt binary or .a in a worktree is not evidence about the commit that worktree is checked out to.

    Real first bad commit (bisected on an IR signal, release builds): 95775b645 (#6258, "x++/x-- disqualifies the i32 fast path") — one commit before the base assumed good.

    And #6258 is not itself a defect. It correctly removed an unsound overflow path (that is the #6072 fix). It merely unmasked a much older blind spot: collect_index_used_locals has no arm for Expr::PutValueSet, so a call-returned array's arr[i] = arr[i] + 1 subtree went opaque, the loop counter never joined index_used_locals, and it lost its i32 shadow slot.

    The measured regression and everything in the Evidence section still stand. Fix in #6312.

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

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions