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
Browse filesBrowse the repository at this point in the historyBrowse files
Ralph Küpper
committed
perf(closure): hoist per-element closure dispatch out of the array-callback loops (#8180)
`js_closure_callN` re-derived, on EVERY element of every fused array-callback
loop, three answers that cannot change while one closure is being called:
1. `get_valid_func_ptr` — two address-band checks, a volatile `CLOSURE_MAGIC`
probe through `*(closure + 12)` and a volatile `func_ptr` load;
2. `resolve_strategy` — a `perry_thread_local!` single-slot cache, which on
Darwin is a `tlv_get_addr` CALL plus a load and a compare even on a hit;
3. the `DispatchStrategy` match before the indirect jump.
The tree already contained the answer, applied to exactly one call site:
`array/sort.rs`'s `ComparatorCall`, introduced to "skip ~50M HashMap lookups
over a 1.25M-element sort". New `closure/dispatch/direct.rs` generalises it to
arities 1–4 as `DirectCall{1,2,3,4}` — resolve once, call directly, fall back to
`js_closure_callN` for a bound method/function, a rest parameter, a declared
arity above the call arity, or an invalid closure pointer, so the
proxy-callee/throw path, the rest bundling and the undefined-padding stay in one
place. `resolve_call2_direct` is DELETED rather than left standing beside it;
`ComparatorCall` now holds a `DirectCall2`.
Hoisted at 31 call sites: `array/iter_methods.rs` (14), `array/reduce_right.rs`
(1), `typedarray/iterate.rs` (9), `typedarray/transform.rs` (5 — including the
BigInt lane comparator, which resolved once per COMPARISON) and the uint8
`%TypedArray%.prototype` dispatcher's `RootedCallback{2,3,4}`.
Measured on a quiet M1 mini, instructions retired, best of 5, arms interleaved,
per-arm `PERRY_RUNTIME_DIR` + `PERRY_CACHE_DIR`, `PERRY_NO_AUTO_OPTIMIZE=1`:
bench main(A) +8179(B) +8179+8180(C) C vs A
arr 5,027,207,909 5,027,015,176 3,970,592,563 -21.0 %
u8 1,979,043,224 2,535,814,615 1,978,697,176 -0.02 %
`arr` is 21M plain-`Array` callback invocations (forEach/map/filter/reduce/
findIndex/some/every); `u8` is 7.9M Buffer-backed `Uint8Array` ones. Peak RSS is
flat: `arr` 46,628,864 B on both A and C, `u8` 14,794,752 -> 14,876,672 B
(+0.55 %, 20 pages, the handle stack and the resolved sites). #8179's rooting
costs +28 % on the `u8` path on its own; this change pays all of it back and the
plain-`Array` path is 21 % cheaper than main.
SOUNDNESS. `closure->func_ptr` is written once at `js_closure_alloc` and never
mutated; `lookup_closure_rest` / `lookup_closure_arity` are keyed by it and are
insert-only per key, registered at closure creation; the two sentinels are
process constants. The only way to observe a different strategy mid-loop is to
call a DIFFERENT closure, and an array method calls one. Callers that root their
callback pass the CURRENT address to `call`; the resolved target is a static
CODE address, which relocation does not change — the argument
`ComparatorCall::compare_at` already documents. The unit tests in `direct.rs`
assert the fast path is LIVE (`is_direct()`), not merely that nothing threw, and
assert each decline (higher declared arity, rest parameter, invalid pointer).
`array/generic.rs`'s `js_arraylike_*` engine is deliberately NOT converted. Its
per-element cost is dominated by generic array-like property access (`al_has` +
`al_get`, full prototype-chain lookups) rather than by dispatch, and the spec
order it implements reads `LengthOfArrayLike` BEFORE `IsCallable` — so a hoisted
resolve would have to be sequenced after the existing `callable()` call rather
than inserted at the top of the function, which is not the same mechanical edit
and risks moving an observable throw.
0 commit comments