From 62304805254b069c1d05faa3a8783ada71e632ea Mon Sep 17 00:00:00 2001 From: perf-bundle Date: Thu, 27 Aug 2026 10:58:33 +0200 Subject: [PATCH] Avoid per-call closure allocation in type-hierarchy traversal 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 --- src/Compiler/Checking/TypeHierarchy.fs | 14 ++++++------ src/Compiler/Utilities/illib.fs | 30 ++++++++++++++++++++++++++ src/Compiler/Utilities/illib.fsi | 7 ++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/Compiler/Checking/TypeHierarchy.fs b/src/Compiler/Checking/TypeHierarchy.fs index 5ea78ba2ee9..8a172fefb71 100644 --- a/src/Compiler/Checking/TypeHierarchy.fs +++ b/src/Compiler/Checking/TypeHierarchy.fs @@ -2,6 +2,7 @@ module internal FSharp.Compiler.TypeHierarchy +open Internal.Utilities.Library open Internal.Utilities.Library.Extras open FSharp.Compiler.Text open FSharp.Compiler.AbstractIL.IL @@ -239,7 +240,7 @@ let FoldHierarchyOfTypeAux followInterfaces allowMultiIntfInst skipUnref visitor | _ -> false // Do not visit the same type twice. Could only be doing this if we've seen this tycon - if seenThisTycon && List.exists (typeEquiv g ty) (visited.Find (tcrefOfAppTy g ty)) then state else + if seenThisTycon && ListInline.exists (typeEquiv g ty) (visited.Find (tcrefOfAppTy g ty)) then state else // Do not visit the same tycon twice, e.g. I and I, collect I only, unless directed to allow this if seenThisTycon && allowMultiIntfInst = AllowMultiIntfInstantiations.No then state else @@ -253,10 +254,11 @@ let FoldHierarchyOfTypeAux followInterfaces allowMultiIntfInst skipUnref visitor state if ndeep > 100 then (errorR(Error((FSComp.SR.recursiveClassHierarchy (RichText.mkText (showType ty))), m)); (visitedTycon, visited, acc)) else + // Local 'loop' is passed as a lambda, not 'loop (ndeep+1)': InlineIfLambda then inlines it instead of allocating it as a closure. let visitedTycon, visited, acc = if isInterfaceTy g ty then - List.foldBack - (loop (ndeep+1)) + ListInline.foldBack + (fun ity st -> loop (ndeep+1) ity st) (GetImmediateInterfacesOfType skipUnref g amap m ty) (loop ndeep g.obj_ty_noNulls state) else @@ -287,15 +289,15 @@ let FoldHierarchyOfTypeAux followInterfaces allowMultiIntfInst skipUnref visitor | _ -> let state = if followInterfaces then - List.foldBack - (loop (ndeep+1)) + ListInline.foldBack + (fun ity st -> loop (ndeep+1) ity st) (GetImmediateInterfacesOfType skipUnref g amap m ty) state else state let state = Option.foldBack - (loop (ndeep+1)) + (fun sty st -> loop (ndeep+1) sty st) (GetSuperTypeOfType g amap m ty) state state diff --git a/src/Compiler/Utilities/illib.fs b/src/Compiler/Utilities/illib.fs index a5d44c12bdd..5842492b31d 100644 --- a/src/Compiler/Utilities/illib.fs +++ b/src/Compiler/Utilities/illib.fs @@ -436,6 +436,36 @@ module Option = module internal ValueTuple = let inline map1Of2 ([] f) struct (a1, a2) = struct (f a1, a2) +module ListInline = + /// List.exists, but inline so the predicate is inlined (InlineIfLambda) rather than allocated as a closure. + let inline exists ([] predicate: 'T -> bool) (list: 'T list) = + let mutable rest = list + let mutable result = false + + while not result && not rest.IsEmpty do + result <- predicate rest.Head + rest <- rest.Tail + + result + + /// List.foldBack, but inline so the folder is inlined (InlineIfLambda). Folds lengths up to 5 directly; longer lists use an array, staying stack-safe like List.foldBack. + let inline foldBack ([] folder: 'T -> 'State -> 'State) (list: 'T list) (state: 'State) = + match list with + | [] -> state + | [ h ] -> folder h state + | [ h1; h2 ] -> folder h1 (folder h2 state) + | [ h1; h2; h3 ] -> folder h1 (folder h2 (folder h3 state)) + | [ h1; h2; h3; h4 ] -> folder h1 (folder h2 (folder h3 (folder h4 state))) + | [ h1; h2; h3; h4; h5 ] -> folder h1 (folder h2 (folder h3 (folder h4 (folder h5 state)))) + | _ -> + let array = List.toArray list + let mutable state = state + + for i = array.Length - 1 downto 0 do + state <- folder array[i] state + + state + module List = let sortWithOrder (c: IComparer<'T>) elements = diff --git a/src/Compiler/Utilities/illib.fsi b/src/Compiler/Utilities/illib.fsi index a4bba551042..7c598ffa7a2 100644 --- a/src/Compiler/Utilities/illib.fsi +++ b/src/Compiler/Utilities/illib.fsi @@ -152,6 +152,13 @@ module internal Option = val attempt: f: (unit -> 'T) -> 'T option +module internal ListInline = + /// List.exists, but inline so the predicate is inlined (InlineIfLambda) rather than allocated as a closure. + val inline exists: [] predicate: ('T -> bool) -> list: 'T list -> bool + + /// List.foldBack, but inline so the folder is inlined (InlineIfLambda). Folds lengths up to 5 directly; longer lists use an array, staying stack-safe like List.foldBack. + val inline foldBack: [] folder: ('T -> 'State -> 'State) -> list: 'T list -> state: 'State -> 'State + module internal List = val sortWithOrder: c: IComparer<'T> -> elements: 'T list -> 'T list