Skip to content

perf: numeric-array specialization is lost when the array is captured from an enclosing scope (27x slower than the same array passed as a parameter) #6369

Description

@proggeramlug

A number[] gets the specialized numeric-array index path when it arrives as a parameter, but falls all the way back to the fully generic js_dyn_index_get when it is reached through a closure capture (module-scope const read inside a function). Same declared type, same loop body, 27x difference.

This matters a lot for bundled code: a bundle is overwhelmingly module-scope consts captured by closures, so in practice almost nothing gets the fast path.

Repro

Parameter (fast):

function hot(a: number[]): number {
  let s = 0;
  for (let r = 0; r < 2000; r++) for (let i = 0; i < 1000; i++) s += a[i];
  return s;
}
const arr: number[] = [];
for (let i = 0; i < 1000; i++) arr.push(i);
// ... time hot(arr)

Captured (slow) — the only change is that arr is read from the enclosing scope instead of being passed in:

const arr: number[] = [];
for (let i = 0; i < 1000; i++) arr.push(i);
const hot = (): number => {
  let s = 0;
  for (let r = 0; r < 2000; r++) for (let i = 0; i < 1000; i++) s += arr[i];
  return s;
};
// ... time hot()

Numbers (best of 5, 2M iterations)

shape time
node 1.1 – 1.3 ms
perry, number[] as a parameter 2.0 ms
perry, number[] captured 53.6 ms
perry, untyped array captured (const arr = []) 53.6 ms

Note the last row: the captured typed array is no faster than an untyped one — the annotation buys nothing once the value is captured.

Cause (from the emitted IR)

PERRY_LLVM_KEEP_IR=1, grepping the array helpers in each:

parameter version:
   2 @js_typed_feedback_numeric_array_index_get_guard   <-- specialized path

captured version:
   2 @js_dyn_index_get                                  <-- fully generic path
   2 @js_array_length

So the specialization decision is made from the local type of the accessed base, and a capture/upvalue read is typed as unknown. The declared element type is available at the declaration site; it just isn't propagated to the capture.

Suggested direction

Propagate the declared/inferred type of a captured binding to its read sites (upvalue reads and module-scope reads), so js_typed_feedback_numeric_array_index_get_guard is selected there too. A guarded fast path is already the mechanism used for parameters — this is about making the guard reachable, not about proving anything new.

Same question likely applies to captured object bindings (class-field get/set guards) — worth checking whether those degrade identically.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions