Skip to content

[MicroPerf] Inline the remaining free-variable foldBacks - #20439

Open
T-Gro wants to merge 4 commits into
t-gro-freevars-inline-foldbackfrom
t-gro-freevars-expr-foldbacks
Open

[MicroPerf] Inline the remaining free-variable foldBacks#20439
T-Gro wants to merge 4 commits into
t-gro-freevars-inline-foldbackfrom
t-gro-freevars-expr-foldbacks

Conversation

@T-Gro

@T-Gro T-Gro commented Sep 3, 2026

Copy link
Copy Markdown
Member

Extends #20385: routes the remaining lambda / partial-application List.foldBack/List.fold sites in the free-variable walkers through the inline ListInline.foldBack/ListInline.fold twins, whose InlineIfLambda folder is inlined instead of allocated as a per-call closure. Fold order unchanged. Adds ListInline.fold next to the existing ListInline.foldBack.

Per-call closure classes in FSharp.Compiler.Service.dll (IL TypeDefinition enumeration): 22 → 6. The 6 survivors are CPS continuations, an Option.foldBack, a LetRec cache thunk and a List.foldBack2 lambda — none is a List fold this inlines.

Closure allocation removed, measured over a full FSharp.Compiler.Service self-compile (455 source files, GCAllocationTick sampling, 6 compiles; every class below reaches 0 samples after, matching the IL count):

eliminated closure class MB / self-compile before after
accFreeInFlatExprs@ 32.0 0
boundTypars@ 17.6 0
accFreeInTarget@ 14.6 0
accFreeInMethod@ 0.1 0
total ~64 MB 0

accFreeInMeasure@, boundTyparsLeftToRight@ and accFreeInTyparConstraintsLeftToRight@ are eliminated too (part of the 22→6 IL count) but sit on the units-of-measure / signature-only left-to-right paths, which barely execute while compiling the compiler's own sources, so they don't register on this workload.

perf-bundle and others added 4 commits August 27, 2026 10:58
FoldHierarchyOfTypeAux runs on a hot path (InfoReader member/property/field
lookup and type inference). Its inner 'let rec loop' was handed to
List.foldBack / Option.foldBack / List.exists as a partial application, so a
fresh closure was allocated on every traversal.

Add a ListInline module (illib.fs) with exists / foldBack whose function
argument is inlined at the call site via InlineIfLambda, so the closure never
materializes. foldBack folds lists up to length five directly (no allocation)
and only copies to an array for longer lists, so it stays stack-safe. Length
five is measured: over a real compile GetImmediateInterfacesOfType returns
<=5 elements ~96% of the time (81% are empty), so short interface lists
allocate nothing at all.

At the call sites the top-level 'typeEquiv g ty' stays partially applied (the
optimizer fuses it after inlining), but the local 'loop' is passed as a lambda
so InlineIfLambda inlines it rather than allocating it as a closure.

Fold order is unchanged, so visit order, dedup and the ndeep>100 error are
preserved; compiler output is byte-identical under --deterministic+.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c6610fae-a96e-4421-a920-e73de4fc94dc
accFreeInTyparConstraints and accFreeInTypars fold accFreeInTyparConstraint /
accFreeTyparRef over a typar's constraint and typar lists during
free-variable computation, one of the hottest traversals in the compiler.
Passing those top-level functions to List.foldBack allocated a partial-
application closure per call, over typically tiny (often empty) lists where
the closure is the entire cost.

Switch both folds to ListInline.foldBack (added in the parent change), whose
InlineIfLambda folder inlines the top-level partial application, so no closure
is allocated. Fold order is unchanged; compiler output is byte-identical under
--deterministic+.

Measured (dotnet-trace gc-verbose, 6 self-compiles of a 120-file corpus):
accFreeInTyparConstraints closure 147.8 MB -> 0, accFreeTyparRef closure
238.8 MB -> 0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c6610fae-a96e-4421-a920-e73de4fc94dc
The remaining List.foldBack / List.fold sites in the free-variable
walkers (accFreeInFlatExprs, boundTypars and its left-to-right twin,
accFreeInMeasure, the LetRec/Obj/target walkers, and the object-
expression method walker) passed lambdas or partial applications to the
non-inline List HOFs, allocating a closure per call over typically tiny
(often empty) lists.

Route each through the inline ListInline.foldBack / ListInline.fold
twins, whose InlineIfLambda folder is inlined rather than allocated.
ListInline.fold is added alongside the existing ListInline.foldBack
(inline, InlineIfLambda folder, left-to-right cursor loop). Fold order
is unchanged.

Extends #20385. IL TypeDefinition enumeration of FSharp.Compiler.Service.dll:
the matching per-call closure classes drop from 22 to 6, with the 6
survivors being continuation-passing closures, Option.foldBack partial
applications, a LetRec cache-compute thunk, and a List.foldBack2 lambda,
none of which is a List fold this technique can inline.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@T-Gro
T-Gro requested a review from a team as a code owner September 3, 2026 13:57
@T-Gro T-Gro added Theme-Performance NO_RELEASE_NOTES Label for pull requests which signals, that user opted-out of providing release notes labels Sep 3, 2026
@T-Gro
T-Gro requested a review from abonie September 3, 2026 13:57
@github-actions github-actions Bot added the AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed label Sep 3, 2026
@abonie
abonie force-pushed the t-gro-freevars-inline-foldback branch from 198fdf4 to e4f972f Compare September 4, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-Tooling-Check-Bypassed Tooling check: non-fork PR, not diff-analyzed NO_RELEASE_NOTES Label for pull requests which signals, that user opted-out of providing release notes Theme-Performance

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant