[Request For Feedback] Inline List/Array higher-order functions so lambda arguments allocate no closure - #20422
Draft
T-Gro wants to merge 1 commit into
Draft
[Request For Feedback] Inline List/Array higher-order functions so lambda arguments allocate no closure#20422T-Gro wants to merge 1 commit into
T-Gro wants to merge 1 commit into
Conversation
… no closure Add `inline` + `[<InlineIfLambda>]` to 32 allocation-free List/Array traversal combinators, so a captured lambda is spliced into the loop and its FSharpFunc closure (24 B) is never created. Recursion is moved behind inner `let rec loop` bindings (a binding cannot be both `rec` and `inline`, FS3890). Error paths are routed through hidden non-inline helpers so inline bodies can raise the localized SR exception messages without exposing internal resources. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f15ee4ba-0309-440a-b0d2-c2bd7d1bffa1
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
Warning No PR link found in some release notes, please consider adding it.
|
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.
This makes 32 allocation-free
List/Arrayhigher-order functionsinline+[<InlineIfLambda>]. For the common case — a lambda passed directly — it is a clear win: the per-callFSharpFuncclosure disappears (24 B → 0), small-collection calls get faster, large ones are at parity.The catch, and the reason this is a Request For Feedback: when the callback is not inlinable — an opaque function value such as a curried/partially-applied function held in a field, returned from a non-inlined function, or a delegate — the closure can't be removed, and dropping
OptimizedClosures.Adaptregresses that path 1.6×–3×. The non-inline library methods useAdaptto hoist the per-element arity type-tests out of the loop; the inline path can't keep it, because applyingAdaptto an inlined lambda would re-materialise the very closure we set out to remove. So the allocation win andAdaptare mutually exclusive.Recursion is moved behind an inner
let rec loop(a binding cannot be bothrecandinline, FS3890); error paths go through hidden non-inline helpers so inline bodies can still raise the localizedSRmessages. Design: RFC FS-1115.List(14):exists exists2 find findIndex fold fold2 forall forall2 iter2 iteri2 pick reduce skipWhile tryPickArray(18):exists2 find findBack findIndex findIndexBack fold fold2 foldBack foldBack2 forall forall2 iter2 iteri iteri2 pick reduce reduceBack tryPickMeasured on .NET 11 / Arm64: exact allocation via
GC.GetAllocatedBytesForCurrentThread, wall-clock via BenchmarkDotNet out-of-process, shipped FSharp.Core 10.0.101 vs this branch (same netstandard2.1 target).✅ Clearly better
Allocation per call (exact probe; identical across the 25 probed functions):
(fun a x -> a+x+k)(addBy k)Wall-clock, small collections, capturing lambda:
List.fold (fun a x -> a+x+k)Array.fold (fun a x -> a+x+k)Inlined lambda reaches the hand-written-loop floor and stays alloc-free (N=1M):
Array.findArray.iteri2⚪ Neutral
Wall-clock at large N (per-element work dominates), capturing lambda:
List.foldArray.foldArray.fold2lambdaArgument forms that were already free stay free:
(+)🔴 Risk of regression
Non-inlinable function values (N=1M):
Array.find(1-arg)Array.iteri2(3-arg)Array.fold2(3-arg)Shipped's non-inline
fold2runsOptimizedClosures.Adaptonce, then a direct multi-arg invoke — as fast as a hand loop. The inline path does per-element arity dispatch instead.find(1-arg, noAdapt) still regresses 1.57× from the inlined-at-call-site loop vs the library's single loop; the benchmark shows the slowdown but does not isolate its cause.Cost (not runtime):
inlinecopies the loop to every recompiled call site — 10 representative sites: 308 → 707 IL bytes (~40 B/site;fold2+91,iteri2+116). Sixpublicthrow-helpers added to the SurfaceArea baseline (inline bodies can't reference internalSR).Open question for feedback: keep all 32 and accept the opaque-value regression, or restrict to the shapes that don't shed
Adapt? These benchmarks don't measure how common opaque-value call sites are in the wild.