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).

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