You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
--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
The read loop above emits zerojs_array_get_f64 for a[i].
--opt-report still shows local r -> Ptr<Shape>.
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.
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.
Protected floors hold on the pinned quiet mini, interleaved, best-of-N, output byte-verified against Node before timing.
Traps
Isolate the read loop by subtraction. Build a build-only variant and diff the call multisets, as above; construction dominates otherwise.
A benchmark win with the clone deleted is a regression that looks like a win — see criterion 4.
--opt-report is the instrument for whether the promotion still holds.
PERRY_NO_AUTO_OPTIMIZE=1 changes nothing here (verified identical IR), so it is safe for iteration.
Build -p perry -p perry-runtime-static -p perry-stdlib-static and pin PERRY_RUNTIME_DIR, or both arms of an A/B link the same stale .a.
#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).
The measurement
On main @ v0.5.1448, fetching an element from an array whose element shape is already proven is still a runtime call.
--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):
a[i]wherea's element shape is proven,iis an in-bounds i32 loop counter, and the result is immediately bound to aPtr<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.rsprovides:emit_element_shape_loop_preheader_check— run once, before the fast cloneemit_element_shape_field_load— run per access inside the fast cloneSo 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/) tojs_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_enteredfor exactly this — acond_brINTO 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
js_array_get_f64fora[i].--opt-reportstill showslocal r -> Ptr<Shape>.a[5] = undefined; delete a[3]), one grown past inline capacity mid-loop,a.lengthreduced mid-loop, anArraysubclass, a proxy-wrapped array, and an element replaced with a different shape mid-loop.cond_brINTO the fast clone (perf(repsel): the element-shape loop clone fires onarr.lengthand aliased element types (#7480) #7701'sassert_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.PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800clean over the reproducer, with the[gc-zeal]verdict showing non-zerocopying_minorsandmoved_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_headexists 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.Traps
--opt-reportis the instrument for whether the promotion still holds.PERRY_NO_AUTO_OPTIMIZE=1changes nothing here (verified identical IR), so it is safe for iteration.-p perry -p perry-runtime-static -p perry-stdlib-staticand pinPERRY_RUNTIME_DIR, or both arms of an A/B link the same stale.a.P[]parameter still reads fields by name (first-party half of #7152/#7170) #7766 — the symptom reproduces on the already-promoted local above — but repsel: rule-1 provenance does not survive a function boundary — a typedP[]parameter still reads fields by name (first-party half of #7152/#7170) #7766 is what makes it fire when the array crosses a function boundary.Related
#7151 covers element read forms (direct
A[i].fieldvs boundconst r = A[i], andmap/forEach/reducecallback params). It is not this issue: measured, both forms behave identically here, and both still calljs_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_coercein the same measurement). Closed context: #7480 / #7669 / #7701 (the guarded clone), #7149 / #7034 §3 (the element rule).