Skip to content

Perf #861: typed List<double> map→filter→reduce pipeline (array-methods beats Node)#872

Merged
nickna merged 4 commits into
mainfrom
wrk/array-methods-typed-pipeline
Jun 21, 2026
Merged

Perf #861: typed List<double> map→filter→reduce pipeline (array-methods beats Node)#872
nickna merged 4 commits into
mainfrom
wrk/array-methods-typed-pipeline

Conversation

@nickna

@nickna nickna commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Part of #856 (perf epic). Closes the last benchmark gap: array-methods. Rebased onto main (the #871 per-scope analyzer prerequisite is already merged); this is the 4 typed-HOF commits standalone.

Result

array-methods @100k (warm) Before After Node
compiled 11.2 ms (2.79×) 1.95 ms (0.64× — beats Node) 3.03 ms

The map→filter→reduce chain runs entirely on List<double> with a direct Func<double,…> per stage and zero per-element boxing / no isinst ladder: build→ArrayPushDouble, map→ArrayMapDouble, filter→ArrayFilterDouble, reduce→ArrayReduceDouble (IL-verified). With #856's other workloads already at/above Node, every benchmark in the suite now meets or exceeds Node — the epic goal.

How (3 typed-HOF increments)

  1. typed reduceArrayReduceDouble. The annotated arrow's typed double(double,double) method binds directly to the delegate (no boxed adapter — simpler than Perf: array HOFs (map/filter/reduce) cache callback MethodInfo + delegate-specialize #861's object-adapter, which only exists to fit the List<object> helpers).
  2. typed mapArrayMapDouble(List<double>, Func<double,double>) -> List<double>, with result-local typing: const d = src.map(cb) becomes a List<double> slot.
  3. typed filterArrayFilterDouble(List<double>, Func<double,bool>) -> List<double>.

Design notes

  • No analyzer fixpoint. The result-local slot type is decided at emit time from the source's already-declared slot (declarations emit in source order), keeping everything slot-type-keyed and scope-correct.
  • Typed results never escape. The typed map/filter is emitted inline from EmitVarDeclaration, and only when the result lands in a promoted (non-escaping) local — so a List<double> result is never produced into $Array context. Otherwise it falls back to the boxed $Array HOF (still correct).
  • Analyzer/emitter agreement. Both gate on the same criteria (List<double> source slot + a non-capturing inline number→number/number→bool arrow; GetCaptures(af).Count == 0 ≡ the emitter's DisplayClasses check). A typed number→number map result is recorded as number[] directly (the checker often can't infer a chained .map() result type).
  • Capturing callbacks stay on the boxed path. Pure-BCL helpers — no standalone-DLL dependency.

Tests / validation

  • ArrayLocalPromotionTests: 38/38 (both modes) — typed reduce/map/filter, the full map→filter→reduce pipeline, chains, and the capturing-reducer / map-result-escapes / filter-result-escapes fallbacks.
  • Full dotnet test: 0 real regressions (only tests that pass in isolation under parallel load + the documented stale Test262 baselines — Array.isArray/proxy, present in interpreter mode too → unrelated to this compile-only change).
  • --verify passes on the full typed pipeline.

Plan: docs/plans/issue-861-array-methods-typed-pipeline.md.

nickna added 4 commits June 20, 2026 19:53
First increment of the typed-HOF pipeline. A `number[]` local used only via push +
`reduce((a,x)=>…, init)` with a non-capturing, annotated `(number,number)=>number`
reducer now stays a promoted `List<double>` and drives a new emitted runtime helper
`ArrayReduceDouble(List<double>, Func<double,double,double>, double)` — zero per-element
boxing. The arrow's typed `double(double,double)` method binds DIRECTLY to
`Func<double,double,double>` (ldnull+ldftn), no boxed adapter (simpler than #861's
object-adapter, which only exists to fit the List<object> helpers).

- ArrayLocalPromotionAnalyzer: permit `arr.reduce(reducer, init)` as a receiver use when
  the array is number[] and the reducer is an inline, non-capturing, 2-arg numeric arrow
  (`GetCaptures(af).Count == 0`, consistent with the emitter's DisplayClasses gate). Any
  other reduce shape disqualifies → stays on the $Array path.
- EmitMethodCall: typed-reduce hook gated on a List<double> slot + the same arrow criteria;
  emits the direct delegate + ArrayReduceDouble. Promotion and typed-emit share criteria, so
  a promoted List<double> reduced here is always typeable (no broken fallback).

reduce is already ~Node parity, so this is the foundational vertical (proves the runtime
helper + direct typed-delegate binding + analyzer/emitter agreement); map/filter (the
benchmark win, which add result-local typing) reuse this machinery next.

Green on dotnet test (28 ArrayLocalPromotionTests incl. typed-reduce + capturing-reducer
fallback, both modes; 13971 total) except pre-existing stale/flaky Test262 baselines.
IL-verified.
Second increment of the typed-HOF pipeline — and the first benchmark win. A
`const d = src.map(cb)` where `src` is a promoted List<double> and `cb` is a
non-capturing `number→number` arrow now makes `d` itself a promoted List<double>
slot, built by a new emitted helper `ArrayMapDouble(List<double>, Func<double,double>)
-> List<double>` — zero per-element boxing, no isinst ladder. Chains: arr → map →
d → reduce all stay typed (verified: ArrayMapDouble → ArrayReduceDouble, no boxing).

Result-local typing without an analyzer fixpoint: the result slot type is decided at
EMIT time from the source's already-declared slot (declarations emit in source order),
and the typed map is emitted inline from EmitVarDeclaration ONLY when the result lands
in a promoted (non-escaping) local — so a List<double> result never escapes into
$Array context. If the source isn't promoted, or the result escapes, it falls back to
the boxed $Array map (still correct).

- ArrayLocalPromotionAnalyzer: permit `src.map(typed non-capturing number→number)` as a
  receiver; treat `const d = src.map(cb)` as a promotion candidate and record its element
  kind as number directly (a typed number→number map is number[] by construction — the
  checker often can't infer the chained .map() result type). IsNumberArrayReceiver accepts
  either a checker-resolved number[] or a recorded number map-result, so chains work.
- ILEmitter: TryResolveTypedDoubleMapInit + EmitVarDeclaration emit ArrayMapDouble into the
  List<double> slot when the source slot is List<double> and the mapper is a direct
  double(double) static method (bound to Func<double,double>, no boxed adapter).

Green on dotnet test (34 ArrayLocalPromotionTests incl. typed map→index/length,
map→reduce chain, and map-result-escapes fallback, both modes; 13976 total) except the
pre-existing stale/flaky Test262 baselines. IL-verified.
… pipeline

Final increment. A `const e = src.filter(pred)` where `src` is a promoted List<double>
and `pred` is a non-capturing `number→boolean` arrow makes `e` a promoted List<double>,
built by `ArrayFilterDouble(List<double>, Func<double,bool>) -> List<double>` — no
per-element boxing. Mirrors the typed-map result-local machinery (emit-time slot decision
from the source slot; typed result only into a non-escaping promoted local).

With map+filter+reduce all typed, the full array-methods pipeline runs end-to-end on
List<double> with a direct Func<double,…> per stage and zero boxing/isinst:
build→ArrayPushDouble, map→ArrayMapDouble, filter→ArrayFilterDouble, reduce→ArrayReduceDouble.

**array-methods @100k: 11.2ms → 1.95ms — now BEATS Node (3.03ms, 0.64×), was 2.79× slower.**
This closes the last benchmark gap in #856; every workload now meets or exceeds Node.

- TryResolveTypedDoubleMapInit generalized to TryResolveTypedDoubleHofInit (map: double(double)
  → Func<double,double>+ArrayMapDouble; filter: bool(double) → Func<double,bool>+ArrayFilterDouble).
- Analyzer: permit `src.filter(typed non-capturing number→bool)` receiver + filter-result candidate.

Green on dotnet test (38 ArrayLocalPromotionTests incl. typed filter, the full
map→filter→reduce pipeline, and the filter/map escape fallbacks, both modes; 13981 total)
except the pre-existing stale/flaky Test262 baselines. IL-verified.
@nickna nickna force-pushed the wrk/array-methods-typed-pipeline branch from d157a91 to 642a2a0 Compare June 21, 2026 02:55
@nickna nickna changed the base branch from wrk/array-promotion-per-scope to main June 21, 2026 02:55
@nickna nickna changed the title Perf #861: typed List<double> map→filter→reduce pipeline (array-methods now beats Node) Perf #861: typed List<double> map→filter→reduce pipeline (array-methods beats Node) Jun 21, 2026
@nickna nickna merged commit 55def21 into main Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant