Perf #861: typed List<double> map→filter→reduce pipeline (array-methods beats Node)#872
Merged
Merged
Conversation
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.
d157a91 to
642a2a0
Compare
This was referenced Jun 21, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
The map→filter→reduce chain runs entirely on
List<double>with a directFunc<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)
ArrayReduceDouble. The annotated arrow's typeddouble(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 theList<object>helpers).ArrayMapDouble(List<double>, Func<double,double>) -> List<double>, with result-local typing:const d = src.map(cb)becomes aList<double>slot.ArrayFilterDouble(List<double>, Func<double,bool>) -> List<double>.Design notes
EmitVarDeclaration, and only when the result lands in a promoted (non-escaping) local — so aList<double>result is never produced into$Arraycontext. Otherwise it falls back to the boxed$ArrayHOF (still correct).List<double>source slot + a non-capturing inlinenumber→number/number→boolarrow;GetCaptures(af).Count == 0≡ the emitter'sDisplayClassescheck). A typednumber→numbermap result is recorded asnumber[]directly (the checker often can't infer a chained.map()result type).Tests / validation
ArrayLocalPromotionTests: 38/38 (both modes) — typed reduce/map/filter, the fullmap→filter→reducepipeline, chains, and the capturing-reducer / map-result-escapes / filter-result-escapes fallbacks.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).--verifypasses on the full typed pipeline.Plan:
docs/plans/issue-861-array-methods-typed-pipeline.md.