From 3a2154c73b19e72e4d8842f3f4246e85935e8281 Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Fri, 14 Aug 2026 13:25:33 +0200 Subject: [PATCH 1/2] TAST: allocate an entity's ad-hoc member list and compiled-representation cache on first use Every Entity carried two objects that most entities never use. TyconAugmentation.Create allocated an empty ResizeArray for tcaug_adhoc_list, one per entity, and nothing is ever added for an imported type: NewILTycon closes the augmentation and adds no ad-hoc members. There is a single Add site, so the list is now allocated there, behind AddAdhocMember, with AdhocMembers for the readers. Unpickling an empty list stores nothing rather than an empty ResizeArray. entity_il_repr_cache held a cache cell used only by IlxGen, so an analysis-only host allocated one per entity and never read it. CompiledReprCache now creates it on first request, leaving the member's signature unchanged so no caller sees null. Retained memory after ParseAndCheckProject drops 0.05-1.38 MB per project across the measurement suite, and analysis time is unchanged within noise. Co-Authored-By: Claude Opus 5 (1M context) --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + .../Checking/Expressions/CheckExpressions.fs | 2 +- src/Compiler/TypedTree/TypedTree.fs | 48 +++++++++++++++---- src/Compiler/TypedTree/TypedTree.fsi | 10 +++- .../TypedTree/TypedTreeOps.Remapping.fs | 8 +++- src/Compiler/TypedTree/TypedTreePickle.fs | 8 ++-- 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 7dd68964cb3..2eb92045f84 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -166,6 +166,7 @@ * Add symbol and type highlighting to F# diagnostics ([PR #20097](https://github.com/dotnet/fsharp/pull/20097)) * IL: add `ILPreNamespace`, make `ILPreTypeDef` creation lazy ([PR #20092](https://github.com/dotnet/fsharp/pull/20092)) * IL: use empty tables for members when possible ([PR #20249](https://github.com/dotnet/fsharp/pull/20249)) +* TAST: allocate an entity's ad-hoc member list and compiled-representation cache on first use ### Improved diff --git a/src/Compiler/Checking/Expressions/CheckExpressions.fs b/src/Compiler/Checking/Expressions/CheckExpressions.fs index 03ea69f01c2..f8d265eea4c 100644 --- a/src/Compiler/Checking/Expressions/CheckExpressions.fs +++ b/src/Compiler/Checking/Expressions/CheckExpressions.fs @@ -1200,7 +1200,7 @@ let PublishValueDefnMaybeInclCompilerGenerated (cenv: cenv) env inclCompilerGene let tcaug = vspec.MemberApparentEntity.TypeContents let vref = mkLocalValRef vspec tcaug.tcaug_adhoc <- NameMultiMap.add vspec.LogicalName vref tcaug.tcaug_adhoc - tcaug.tcaug_adhoc_list.Add (ValRefIsExplicitImpl g vref, vref) + tcaug.AddAdhocMember(ValRefIsExplicitImpl g vref, vref) | _ -> () let PublishValueDefn cenv env declKind vspec = diff --git a/src/Compiler/TypedTree/TypedTree.fs b/src/Compiler/TypedTree/TypedTree.fs index 236a4a9b798..f3fdeece8be 100644 --- a/src/Compiler/TypedTree/TypedTree.fs +++ b/src/Compiler/TypedTree/TypedTree.fs @@ -693,7 +693,7 @@ type Entity = /// Used during codegen to hold the ILX representation indicating how to access the type // MUTABILITY: only for unpickle linkage and caching - mutable entity_il_repr_cache: CompiledTypeRepr cache + mutable entity_il_repr_cache: CompiledTypeRepr cache | null mutable entity_opt_data: EntityOptionalData option } @@ -938,7 +938,15 @@ type Entity = | _ -> TAccess [] /// Get the cache of the compiled ILTypeRef representation of this module or type. - member x.CompiledReprCache = x.entity_il_repr_cache + /// Allocated on first request rather than per entity: only codegen asks for it, so an analysis-only + /// host never pays for one. + member x.CompiledReprCache = + match x.entity_il_repr_cache with + | null -> + let c = newCache () + x.entity_il_repr_cache <- c + c + | c -> c /// Get a blob of data indicating how this type is nested in other namespaces, modules or types. member x.PublicPath = x.entity_pubpath @@ -1470,8 +1478,12 @@ type TyconAugmentation = /// Properties, methods etc. in declaration order. The boolean flag for each indicates if the /// member is known to be an explicit interface implementation. This must be computed and /// saved prior to remapping assembly information. - tcaug_adhoc_list: ResizeArray - + // + // Allocated on first member: there is one augmentation per entity, and nothing is ever added for + // an imported type. + mutable tcaug_adhoc_list: ResizeArray | null + + /// Properties, methods etc. as lookup table mutable tcaug_adhoc: NameMultiMap @@ -1488,6 +1500,24 @@ type TyconAugmentation = mutable tcaug_abstract: bool } + /// Record a member in declaration order, allocating the list on first use + member tcaug.AddAdhocMember(isExplicitImpl: bool, vref: ValRef) = + let list = + match tcaug.tcaug_adhoc_list with + | null -> + let l = ResizeArray() + tcaug.tcaug_adhoc_list <- l + l + | l -> l + + list.Add(isExplicitImpl, vref) + + /// Members in declaration order, empty when the type has none + member tcaug.AdhocMembers: (bool * ValRef) list = + match tcaug.tcaug_adhoc_list with + | null -> [] + | l -> List.ofSeq l + member tcaug.SetCompare x = tcaug.tcaug_compare <- Some x member tcaug.SetCompareWith x = tcaug.tcaug_compare_withc <- Some x @@ -1505,7 +1535,7 @@ type TyconAugmentation = tcaug_hash_and_equals_withc=None tcaug_hasObjectGetHashCode=false tcaug_adhoc=NameMultiMap.empty - tcaug_adhoc_list=ResizeArray<_>() + tcaug_adhoc_list=null tcaug_super=None tcaug_interfaces=[] tcaug_closed=false @@ -6302,7 +6332,7 @@ type Construct() = // Generated types get internal accessibility entity_pubpath = Some pubpath entity_cpath = Some cpath - entity_il_repr_cache = newCache() + entity_il_repr_cache = null entity_opt_data = match kind, access with | TyparKind.Type, TAccess [] -> None @@ -6327,7 +6357,7 @@ type Construct() = entity_pubpath=cpath |> Option.map (fun (cp: CompilationPath) -> cp.NestedPublicPath id) entity_cpath=cpath entity_attribs=WellKnownEntityAttribs.Create(attribs) - entity_il_repr_cache = newCache() + entity_il_repr_cache = null entity_opt_data = match xml, access with | doc, TAccess [] when doc.IsEmpty -> None @@ -6405,7 +6435,7 @@ type Construct() = entity_typars = LazyWithContext.NotLazy [] entity_tycon_repr = TNoRepr entity_flags = EntityFlags(usesPrefixDisplay=false, isModuleOrNamespace=false, preEstablishedHasDefaultCtor=false, hasSelfReferentialCtor=false, isStructRecordOrUnionType=false) - entity_il_repr_cache = newCache() + entity_il_repr_cache = null entity_opt_data = match doc, access, repr with | doc, TAccess [], TExnNone when doc.IsEmpty -> None @@ -6444,7 +6474,7 @@ type Construct() = entity_modul_type = mtyp entity_pubpath=cpath |> Option.map (fun (cp: CompilationPath) -> cp.NestedPublicPath (mkSynId m nm)) entity_cpath = cpath - entity_il_repr_cache = newCache() + entity_il_repr_cache = null entity_opt_data = match kind, doc, reprAccess, access with | TyparKind.Type, doc, TAccess [], TAccess [] when doc.IsEmpty -> None diff --git a/src/Compiler/TypedTree/TypedTree.fsi b/src/Compiler/TypedTree/TypedTree.fsi index 98c4ab0e840..3faba6563ec 100644 --- a/src/Compiler/TypedTree/TypedTree.fsi +++ b/src/Compiler/TypedTree/TypedTree.fsi @@ -450,7 +450,7 @@ type Entity = mutable entity_cpath: CompilationPath option /// Used during codegen to hold the ILX representation indicating how to access the type - mutable entity_il_repr_cache: cache + mutable entity_il_repr_cache: cache | null mutable entity_opt_data: EntityOptionalData option } @@ -886,7 +886,7 @@ type TyconAugmentation = /// Properties, methods etc. in declaration order. The boolean flag for each indicates if the /// member is known to be an explicit interface implementation. This must be computed and /// saved prior to remapping assembly information. - tcaug_adhoc_list: ResizeArray + mutable tcaug_adhoc_list: ResizeArray | null /// Properties, methods etc. as lookup table mutable tcaug_adhoc: NameMultiMap @@ -906,6 +906,12 @@ type TyconAugmentation = static member Create: unit -> TyconAugmentation + /// Record a member in declaration order, allocating the list on first use + member AddAdhocMember: isExplicitImpl: bool * vref: ValRef -> unit + + /// Members in declaration order, empty when the type has none + member AdhocMembers: (bool * ValRef) list + member SetCompare: x: (ValRef * ValRef) -> unit member SetCompareWith: x: ValRef -> unit diff --git a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs index 516c1010bec..9becdadaa54 100644 --- a/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs +++ b/src/Compiler/TypedTree/TypedTreeOps.Remapping.fs @@ -2182,8 +2182,12 @@ module internal ExprRemapping = |> Option.map (mapQuadruple (remapValRef tmenv, remapValRef tmenv, remapValRef tmenv, Option.map (remapValRef tmenv))) tcaug_adhoc = x.tcaug_adhoc |> NameMap.map (List.map (remapValRef tmenv)) tcaug_adhoc_list = - x.tcaug_adhoc_list - |> ResizeArray.map (fun (flag, vref) -> (flag, remapValRef tmenv vref)) + let remapped: ResizeArray | null = + match x.tcaug_adhoc_list with + | null -> null + | l -> l |> ResizeArray.map (fun (flag, vref) -> (flag, remapValRef tmenv vref)) + + remapped tcaug_super = x.tcaug_super |> Option.map (remapType tmenv) tcaug_interfaces = x.tcaug_interfaces |> List.map (map1Of3 (remapType tmenv)) } diff --git a/src/Compiler/TypedTree/TypedTreePickle.fs b/src/Compiler/TypedTree/TypedTreePickle.fs index 5b64b10f600..3fea2b73075 100644 --- a/src/Compiler/TypedTree/TypedTreePickle.fs +++ b/src/Compiler/TypedTree/TypedTreePickle.fs @@ -2839,8 +2839,7 @@ and p_tcaug p st = p.tcaug_hash_and_equals_withc |> Option.map (fun (v1, v2, v3, _) -> (v1, v2, v3)), p.tcaug_equals, - (p.tcaug_adhoc_list - |> ResizeArray.toList + (p.AdhocMembers // Explicit impls of interfaces only get kept in the adhoc list // in order to get check the well-formedness of an interface. // Keeping them across assembly boundaries is not valid, because relinking their ValRefs @@ -3201,7 +3200,10 @@ and u_tcaug st = tcaug_equals = b2 // only used for code generation and checking - hence don't care about the values when reading back in tcaug_hasObjectGetHashCode = false - tcaug_adhoc_list = ResizeArray<_>(c |> List.map (fun (_, vref) -> (false, vref))) + tcaug_adhoc_list = + match c with + | [] -> null + | _ -> ResizeArray<_>(c |> List.map (fun (_, vref) -> (false, vref))) tcaug_adhoc = NameMultiMap.ofList c tcaug_interfaces = d tcaug_super = e From ea30bfdb1bd29f68afafea1bb2bd127a1893510e Mon Sep 17 00:00:00 2001 From: Eugene Auduchinok Date: Tue, 18 Aug 2026 15:22:16 +0200 Subject: [PATCH 2/2] Release notes --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 2eb92045f84..6d9506c3889 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -166,7 +166,7 @@ * Add symbol and type highlighting to F# diagnostics ([PR #20097](https://github.com/dotnet/fsharp/pull/20097)) * IL: add `ILPreNamespace`, make `ILPreTypeDef` creation lazy ([PR #20092](https://github.com/dotnet/fsharp/pull/20092)) * IL: use empty tables for members when possible ([PR #20249](https://github.com/dotnet/fsharp/pull/20249)) -* TAST: allocate an entity's ad-hoc member list and compiled-representation cache on first use +* Make Entity's adhoc members list lazy ([PR #20286](https://github.com/dotnet/fsharp/pull/20286/changes)) ### Improved