Skip to content

repsel: a proven element fetch is still a runtime call — specialize a[i] inside the element-shape guarded clone #7771

Description

@proggeramlug

The measurement

On main @ v0.5.1448, fetching an element from an array whose element shape is already proven is still a runtime call.

class P { constructor(public x: number, public y: number) {} }
function run(n: number): number {
  const a: P[] = [];
  for (let i = 0; i < n; i++) a.push(new P(i, i + 1));
  let s = 0;
  for (let i = 0; i < a.length; i++) { const r = a[i]; s += r.x + r.y; }   // <- a[i]
  return s;
}

--opt-report: local r -> Ptr<Shape> (in function run). The promotion succeeds, the by-name field machinery is gone, and the loop runs under the element-shape guard.

Isolating the read loop (subtracting a build-only variant, because construction is ~12 calls and swamps it):

js_array_get_f64   x1     <- THIS ISSUE
js_number_coerce   x2     <- separate issue, see "Related"
js_gc_loop_safepoint x1   <- by design (#7721)

a[i] where a's element shape is proven, i is an in-bounds i32 loop counter, and the result is immediately bound to a Ptr<Shape> local — and it is still a call into the runtime.

Where this sits

The guarded-clone machinery already exists and is already entered here. expr/element_shape_guard.rs provides:

  • emit_element_shape_loop_preheader_check — run once, before the fast clone
  • emit_element_shape_field_load — run per access inside the fast clone

So there is already a per-access fast path for the field load inside the clone. The element fetch is what has no equivalent: it still goes through the generic index-get lowering (expr/index_get.rs / expr/index_get/) to js_array_get_f64.

The engine plan names this as the third leg of the same route — invariant bit → versioned-loop consumer → element Ptr<Shape>. The first two shipped (#7480, #7669, #7701). This is the one left.

First thing to establish, because it changes the shape of the fix: confirm whether the fast clone is genuinely entered for this loop, or whether it is admitted and then degrades. #7701 added assert_fast_clone_is_entered for exactly this — a cond_br INTO the clone, not an unconditional branch to the slow path. If the clone is not entered, the fix is upstream of this issue and this ticket is mis-scoped; say so rather than building on it.

Acceptance criteria

  1. The read loop above emits zero js_array_get_f64 for a[i].
  2. --opt-report still shows local r -> Ptr<Shape>.
  3. The bounds and shape guards still hold. The generic path checks holes, accessors, growth-forwarding stubs, proxies and subclass receivers; a specialized fetch must be guarded such that each of those still reaches correct behaviour. Cover, against Node: an array with a hole (a[5] = undefined; delete a[3]), one grown past inline capacity mid-loop, a.length reduced mid-loop, an Array subclass, a proxy-wrapped array, and an element replaced with a different shape mid-loop.
  4. The clone must still be entered. Assert cond_br INTO the fast clone (perf(repsel): the element-shape loop clone fires on arr.length and aliased element types (#7480) #7701's assert_fast_clone_is_entered). fix(gc): restore evacuation at precise safepoints — the pacing half of #7682 #7690's lesson applies directly: a call added inside a clone whose admission rests on being call-free-by-construction deletes the clone rather than slowing it — the loop silently reverts to the slow path and the benchmark still passes.
  5. GC: PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800 clean over the reproducer, with the [gc-zeal] verdict showing non-zero copying_minors and moved_objects. A specialized fetch holds a raw element address across the loop body; if a collection moves the array, that address must be re-read. js_array_refresh_local_head exists for this and fix(deforest): the caller's binding kept a growth-forwarding stub after a deforested call (#7661) #7751 has just made the producer side sound — check whether the specialized path needs the same treatment.
  6. Protected floors hold on the pinned quiet mini, interleaved, best-of-N, output byte-verified against Node before timing.

Traps

Related

#7151 covers element read forms (direct A[i].field vs bound const r = A[i], and map/forEach/reduce callback params). It is not this issue: measured, both forms behave identically here, and both still call js_array_get_f64. Do not fold them together, but check #7151 before widening the read forms this fix admits.

#7766 (rule-1 provenance, the hop before), and the numeric-field-slot issue filed alongside this one (the js_number_coerce in the same measurement). Closed context: #7480 / #7669 / #7701 (the guarded clone), #7149 / #7034 §3 (the element rule).

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