Successor to #7151 item 2, split out with a measurement so it is one live
quantified item instead of one of five unranked ones. #7151 is being closed;
this is the only one of its five forms with a material number.
The gap
A.map(cb) / A.forEach(cb) / A.reduce(cb, …) callback parameters carry
no element-shape fact, from either mechanism:
So every r.field in a callback body re-enters the guard diamond that both
mechanisms delete for the equivalent for loop.
Measured on main @ 514ff7c9a
macOS arm64, --release compiler and --release
libperry_{runtime,stdlib}.a, PERRY_RUNTIME_DIR pinned, /usr/bin/time -l
instructions retired, medians of 3 interleaved reps. Both programs build the
same 20,000-element C[], read two fields off every element 200 times (4M
element reads), and print the identical t: 80000000000.
// baseline — the covered form
for (let i = 0; i < arr.length; i++) { const r = arr[i]; s = s + r.a + r.b; }
// probe — the same work through a callback
arr.forEach((r: C) => { s = s + r.a + r.b; });
|
Ptr<Shape> consumed |
instructions retired |
vs baseline |
for + const r = A[i] |
2 |
15,800,121,583 |
— |
A.forEach(cb) |
0 |
17,391,614,644 |
+10.1% |
--opt-report on the forEach probe: Ptr<Shape> 0 selected / 1 denied,
*** Ptr<Shape> promoted 0 of 1 candidates in this build. ***
For scale, the other four forms in #7151 measure −0.3% (direct A[i].field,
already served by the versioned clone), +1.5% (let r = A[i]), ≲1% (sort
then read), and ~0 for the element-rule half of non-empty literals — full table
and method in #7151 (comment).
Why it is worth its own issue
#7170's profile of benchmarks/app-patterns/kernels/batch.ts put the reducer's
acc[idx] field traffic (js_object_set_field_by_name 3.57% +
js_put_value_set_dyn_ic_miss 3.14% inclusive) in exactly this position, and
#7151 item 2 already named it: "it is where batch.ts's hot field traffic
actually lives".
Two shapes, and they are not the same problem
forEach/map element parameter — one array, one class, the parameter
is the element. This is the narrow case and the 10.1% above.
reduce accumulator parameter — acc[idx] inside the reducer, i.e. a
second array reached through a parameter. Needs the array-level fact to
cross the call boundary, not just the element fact.
(1) alone would be worth scheduling if it can be done as clone-and-route on the
callback with an all-call-sites-agree obligation, which is #6925's precedent.
Not measured
x86-64; --profile dist; map/reduce separately from forEach; any real
dependency-JS population (#7152 measured the outbound call argument escape at
5 of 746 there, which is a different direction from this inbound one); and I did
not attempt an implementation, so there is no cost estimate for the
clone-and-route side.
Successor to #7151 item 2, split out with a measurement so it is one live
quantified item instead of one of five unranked ones. #7151 is being closed;
this is the only one of its five forms with a material number.
The gap
A.map(cb)/A.forEach(cb)/A.reduce(cb, …)callback parameters carryno element-shape fact, from either mechanism:
collectors/ptr_shape_elements.rs(repsel row 1: scoping Ptr<Shape> beyond locals — measured, promotion is 0 on the motivating workload; build --opt-report (#6952) first #7034 §3 / perf(repsel): open the array-element escape forPtr<Shape>(#7034 §3) #7149) licenses onlyconst r = A[i]inside the region that ownsA— a callback parameter is thecall argumentposition, which needs clone-and-route (repsel row 1: scoping Ptr<Shape> beyond locals — measured, promotion is 0 on the motivating workload; build --opt-report (#6952) first #7034 §1), not acontainment relaxation.
stmt/element_shape_loop.rs(repsel: element-shape proofs through arrays — measured 6.2× vs node; route = invariant bit → versioned-loop consumer → element Ptr<Shape> #7480/perf(repsel): per-array homogeneous element-shape invariant (#7480) #7496 runtime invariant + versioned loopclone) matches a
forloop overA, and a callback body is not one.So every
r.fieldin a callback body re-enters the guard diamond that bothmechanisms delete for the equivalent
forloop.Measured on
main@514ff7c9amacOS arm64,
--releasecompiler and--releaselibperry_{runtime,stdlib}.a,PERRY_RUNTIME_DIRpinned,/usr/bin/time -linstructions retired, medians of 3 interleaved reps. Both programs build the
same 20,000-element
C[], read two fields off every element 200 times (4Melement reads), and print the identical
t: 80000000000.Ptr<Shape>consumedfor+const r = A[i]A.forEach(cb)--opt-reporton theforEachprobe:Ptr<Shape> 0 selected / 1 denied,*** Ptr<Shape> promoted 0 of 1 candidates in this build. ***For scale, the other four forms in #7151 measure −0.3% (direct
A[i].field,already served by the versioned clone), +1.5% (
let r = A[i]), ≲1% (sortthen read), and ~0 for the element-rule half of non-empty literals — full table
and method in #7151 (comment).
Why it is worth its own issue
#7170's profile of
benchmarks/app-patterns/kernels/batch.tsput the reducer'sacc[idx]field traffic (js_object_set_field_by_name3.57% +js_put_value_set_dyn_ic_miss3.14% inclusive) in exactly this position, and#7151 item 2 already named it: "it is where
batch.ts's hot field trafficactually lives".
Two shapes, and they are not the same problem
forEach/mapelement parameter — one array, one class, the parameteris the element. This is the narrow case and the 10.1% above.
reduceaccumulator parameter —acc[idx]inside the reducer, i.e. asecond array reached through a parameter. Needs the array-level fact to
cross the call boundary, not just the element fact.
(1) alone would be worth scheduling if it can be done as clone-and-route on the
callback with an all-call-sites-agree obligation, which is #6925's precedent.
Not measured
x86-64;
--profile dist;map/reduceseparately fromforEach; any realdependency-JS population (#7152 measured the outbound
call argumentescape at5 of 746 there, which is a different direction from this inbound one); and I did
not attempt an implementation, so there is no cost estimate for the
clone-and-route side.