From 963aa387696073e630704de9b6b5350a0f84b8be Mon Sep 17 00:00:00 2001 From: Tomas Grosup Date: Tue, 25 Aug 2026 17:39:30 +0200 Subject: [PATCH] Cache IL method parameter attributes during overload resolution MethInfo.GetParamAttribs rebuilt the full per-parameter attribute list (custom-attr decoding, well-known-attr probes, OptionalArgInfo) from IL metadata on every call and was never cached. On an overload-heavy compile it was called 2.17M times against only 137 distinct underlying methods. Memoize the decode with the existing MemoizationTable, held per compilation via WeakMap.getOrCreate keyed on the ImportMap. The key is the method's physical ILMethodDef plus extension-member use (the C#-style extension view drops the object argument from ParamMetadata, so the two views must not share an entry); canMemoize restricts caching to monomorphic declaring types (an optional arg's default can otherwise depend on the instantiation), matching InfoReader's monomorphic-only caches. Add a regression test covering an IL extension method used with both instance and static call syntax, which shares one ILMethodDef across both views. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Compiler/Checking/infos.fs | 87 ++++++++++++------- .../Language/ExtensionMethodTests.fs | 13 +++ 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/src/Compiler/Checking/infos.fs b/src/Compiler/Checking/infos.fs index cc527e37b48..7973fdc8170 100644 --- a/src/Compiler/Checking/infos.fs +++ b/src/Compiler/Checking/infos.fs @@ -3,6 +3,8 @@ module internal FSharp.Compiler.Infos open System +open System.Collections.Generic +open System.Runtime.CompilerServices open Internal.Utilities.Library open Internal.Utilities.Library.Extras open FSharp.Compiler @@ -658,6 +660,56 @@ type ILMethInfo = |> GetFSharpViewOfReturnType amap.g +// Module-level, not a local closure, so cache hits in GetParamAttribs allocate nothing. +let private ComputeILMethodParamAttribs g (ilMethInfo: ILMethInfo) amap m = + [ [ for p in ilMethInfo.ParamMetadata do + let attrs = p.CustomAttrs + let isParamArrayArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.ParamArrayAttribute) + let reflArgInfo = + match attrs with + | ILAttribDecoded WellKnownILAttributes.ReflectedDefinitionAttribute ([ILAttribElem.Bool b ], _) -> ReflectedArgInfo.Quote b + | ILAttribDecoded WellKnownILAttributes.ReflectedDefinitionAttribute _ -> ReflectedArgInfo.Quote false + | _ -> ReflectedArgInfo.None + let isOutArg = (p.IsOut && not p.IsIn) + let isInArg = (p.IsIn && not p.IsOut) + // Note: we get default argument values from VB and other .NET language metadata + let optArgInfo = OptionalArgInfo.FromILParameter g amap m ilMethInfo.MetadataScope ilMethInfo.DeclaringTypeInst p + + let isCallerLineNumberArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.CallerLineNumberAttribute) + let isCallerFilePathArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.CallerFilePathAttribute) + let isCallerMemberNameArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.CallerMemberNameAttribute) + + let callerInfo = + match isCallerLineNumberArg, isCallerFilePathArg, isCallerMemberNameArg with + | false, false, false -> NoCallerInfo + | true, false, false -> CallerLineNumber + | false, true, false -> CallerFilePath + | false, false, true -> CallerMemberName + | _, _, _ -> + // if multiple caller info attributes are specified, pick the "wrong" one here + // so that we get an error later + if p.Type.TypeRef.FullName = "System.Int32" then CallerFilePath + else CallerLineNumber + + ParamAttribs(isParamArrayArg, isInArg, isOutArg, optArgInfo, callerInfo, reflArgInfo) ] ] + +// Extension view drops the object arg from ParamMetadata, so IsILExtensionMethod is part of the key. +let private getILMethodParamAttribsTable = + WeakMap.getOrCreate (fun (amap: ImportMap) -> + MemoizationTable( + "ilMethodParamAttribs", + (fun (struct (ilMethInfo: ILMethInfo, m)) -> ComputeILMethodParamAttribs amap.g ilMethInfo amap m), + keyComparer = + { new IEqualityComparer with + member _.GetHashCode(struct (mi, _)) = + RuntimeHelpers.GetHashCode mi.RawMetadata ^^^ (if mi.IsILExtensionMethod then 1 else 0) + + member _.Equals(struct (mi1, _), struct (mi2, _)) = + mi1.IsILExtensionMethod = mi2.IsILExtensionMethod + && mi1.RawMetadata === mi2.RawMetadata }, + canMemoize = fun (struct (mi, _)) -> mi.DeclaringTypeInst.IsEmpty)) + + /// Describes an F# use of a method [] [] @@ -1335,39 +1387,10 @@ type MethInfo = #endif /// Get the parameter attributes of a method info, which get combined with the parameter names and types - member x.GetParamAttribs(amap, m) = + member x.GetParamAttribs(amap: ImportMap, m) = match x with - | ILMeth(g, ilMethInfo, _) -> - [ [ for p in ilMethInfo.ParamMetadata do - let attrs = p.CustomAttrs - let isParamArrayArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.ParamArrayAttribute) - let reflArgInfo = - match attrs with - | ILAttribDecoded WellKnownILAttributes.ReflectedDefinitionAttribute ([ILAttribElem.Bool b ], _) -> ReflectedArgInfo.Quote b - | ILAttribDecoded WellKnownILAttributes.ReflectedDefinitionAttribute _ -> ReflectedArgInfo.Quote false - | _ -> ReflectedArgInfo.None - let isOutArg = (p.IsOut && not p.IsIn) - let isInArg = (p.IsIn && not p.IsOut) - // Note: we get default argument values from VB and other .NET language metadata - let optArgInfo = OptionalArgInfo.FromILParameter g amap m ilMethInfo.MetadataScope ilMethInfo.DeclaringTypeInst p - - let isCallerLineNumberArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.CallerLineNumberAttribute) - let isCallerFilePathArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.CallerFilePathAttribute) - let isCallerMemberNameArg = p.CustomAttrsStored.HasWellKnownAttribute(g, WellKnownILAttributes.CallerMemberNameAttribute) - - let callerInfo = - match isCallerLineNumberArg, isCallerFilePathArg, isCallerMemberNameArg with - | false, false, false -> NoCallerInfo - | true, false, false -> CallerLineNumber - | false, true, false -> CallerFilePath - | false, false, true -> CallerMemberName - | _, _, _ -> - // if multiple caller info attributes are specified, pick the "wrong" one here - // so that we get an error later - if p.Type.TypeRef.FullName = "System.Int32" then CallerFilePath - else CallerLineNumber - - ParamAttribs(isParamArrayArg, isInArg, isOutArg, optArgInfo, callerInfo, reflArgInfo) ] ] + | ILMeth(_, ilMethInfo, _) -> + (getILMethodParamAttribsTable amap).Apply(struct (ilMethInfo, m)) | FSMeth(g, _, vref, _) -> GetArgInfosOfMember x.IsCSharpStyleExtensionMember g vref diff --git a/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs index 771b4d17bb2..8060b2ee119 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/ExtensionMethodTests.fs @@ -844,3 +844,16 @@ module CompiledExtensions = """ |> compile |> shouldSucceed + + [] + let ``IL extension method used with both instance and static call syntax`` () = + Fsx + """ +open System.Linq +let xs : System.Collections.Generic.IEnumerable = Seq.ofList [ 1; 2; 3 ] +let a = xs.Select(fun x -> x + 1) |> Seq.length +let b = System.Linq.Enumerable.Select(xs, (fun x -> x + 1)) |> Seq.length +if a <> b then failwith "unexpected" + """ + |> compile + |> shouldSucceed