-
Notifications
You must be signed in to change notification settings - Fork 789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize metadata reading for extension methods #16168
Optimize metadata reading for extension methods #16168
Conversation
ca3f670
to
8e3c947
Compare
8e3c947
to
fc68762
Compare
❗ Release notes required
|
41d34e1
to
d7c77b3
Compare
3d691e3
to
029fd16
Compare
Still a WIP, or ready for review? I can see CI is passing all the gates now. |
I'm still doing some experiments, and it will be ready for review soon |
51cbd44
to
9255b94
Compare
@T-Gro, @psfinaki, I think it's ready for review. Lazy evaluation of attributes (our own implementation inside ReSharper.FSharp) and the presence of extension methods flag allowed us to eliminate unnecessary attribute calculations (from 30 seconds to 1 second), reduce the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs release notes and a rough estimation how does it affect startup time/runtime and memory consumtion.
b1bb412
to
860b97a
Compare
The main idea of PR is to spend a little more time searching for a possible Benchmarks were conducted for some scenarios
[<MemoryDiagnoser>]
type Bench() =
let mutable files = [||]
let mutable sources = [||]
let mutable options = Unchecked.defaultof<_>
let mutable checker = Unchecked.defaultof<_>
[<GlobalSetup>]
member this.Setup() =
let otherOptions = [| (* options with referenced dlls *) |]
files <- [| (* files *) |]
sources <- files |> Array.map (File.ReadAllText >> SourceText.ofString)
options <- {
ProjectFileName = // some .fsproj
ProjectId = None
SourceFiles = f
OtherOptions = otherOptions
ReferencedProjects = [||]
IsIncompleteTypeCheckEnvironment = false
UseScriptResolutionRules = false
LoadTime = DateTime.UtcNow
UnresolvedReferences = None
OriginalLoadReferences = []
Stamp = None
}
checker <- FSharpChecker.Create(projectCacheSize = 200)
[<IterationSetup>]
member this.Invalidate() =
checker.InvalidateAll()
checker.ClearLanguageServiceRootCachesAndCollectAndFinalizeAllTransients()
[<Benchmark>]
member _.TypeCheckFile() =
for file, source in Seq.zip files sources do
let _, checkResult =
checker.ParseAndCheckFileInProject(file, 0, source, options)
|> Async.RunSynchronously
match checkResult with
| FSharpCheckFileAnswer.Succeeded(checkResult) when checkResult.Diagnostics.Length <> 0 -> failwithf $"Errors: %A{checkResult.Diagnostics}"
| FSharpCheckFileAnswer.Aborted -> failwithf "Aborted"
| _ -> ()
Analyzing a single file in the Giraffe framework300 dll references required for project analysis were taken The averaged results of launches look as follows.
Sequential analysis of all files in
|
Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|---|
TypeCheckFiles - Giraffe - old | 1.185 s | 0.0136 s | 0.0121 s | 3000.0000 | 2000.0000 | 1000.0000 | 1.69 GB |
TypeCheckFile - Giraffe - new | 1.167 s | 0.0078 s | 0.0069 s | 3000.0000 | 2000.0000 | 1000.0000 | 1.63 GB |
Sequential analysis of all files in ReSharper.FSharp/FSharp.Psi.Services
48 files & 471 dll references required for project analysis were taken
Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|---|
TypeCheckFile - ReSharper - old | 5.689 s | 0.0472 s | 0.0419 s | 8000.0000 | 6000.0000 | 5000.0000 | 7.35 GB |
TypeCheckFiles - ReSharper - new | 5.635 s | 0.0553 s | 0.0517 s | 7000.0000 | 5000.0000 | 4000.0000 | 7.29 GB |
Unit-test for ILReading
The benchmark checks for additional expenses when called typeDefReader
.
450 assemblies have been taken from the benchmark for ReSharper.FSharp/FSharp.Psi.Services
, which contain approximately 1,000,000 types, 287,000 of which have extensions
The code of an existing test from CompilerServiceBenchmarks
was rewritten to the following
for fileName in assemblies do
let reader = OpenILModuleReader fileName readerOptions
let ilModuleDef = reader.ILModuleDef
for typedef in ilModuleDef.TypeDefs.AsArray() do
typedef.IsKnownToBeAttribute |> ignore
Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|---|
ILReading - old | 377.4 ms | 7.55 ms | 11.06 ms | 3000.0000 | 2000.0000 | 2000.0000 | 731.9 MB |
ILReading - new | 485.8 ms | 9.57 ms | 12.77 ms | 3000.0000 | 2000.0000 | 2000.0000 | 749.5 MB |
Profiling the ReSharper.FSharp project inside the Rider soluition
In this scenario F# projects refer directly to C# projects in the Rider solution instead of DLLs from its SDK. Based on the C# sources, the metadata required for FCS analysis is built. Lazy evaluation of custom attributes (our own implementation inside ReSharper.FSharp) for ILTypeDef
and the presence of the extension methods flag allowed us to eliminate unnecessary forced attribute calculations (from 30 seconds to 1 second), reduce the ILTypeDef
creation time by 30 seconds, and the overall analysis time of the F# file inside the Rider solution from 70 seconds to 40 seconds.
Notes and further work
1
On all profiles captured during the investigation, it is clear that a significant amount of time is reduced from IsTyconRefUsedForCSharpStyleExtensionMembers
calculation because of CanContainExtensionMethods
flag
2
Full line-by-line profile for merged typeDefReader
calls from sequential analysis of all files in src/Giraffe
benchmark looks like
The search for the range of attributes for the type adds here 150 ms.
2.1
On all profiles captured during the investigation, it is clear that a significant amount of time is consumed by non-lazy calls to the following two functions in typeDefReader
:
In a separate PR, we can think about making their computation more lazy. For example, the list of generic parameters was needed inside IsTyconRefUsedForCSharpStyleExtensionMembers
, which after adding the CanContainExtensionMethods
flag, probably, no longer needs to be computed eagerly
Conclusions
- The benchmarks showed that the analysis time, at the very least, did not increase. Memory consumption has slightly decreased.
- Profiling showed that the
IsTyconRefUsedForCSharpStyleExtensionMembers
call time has significantly decreased due to the absence of the need to read custom attributes. - The flag
CanContainExtensionMembers
and the ability to lazily compute attributes in the public API ofILTypeDef
can significantly reduce analysis time. - The
typeDefReader
call time has naturally increased due to the search for indexes of custom attributes for each readable type. - There is room for optimizations in the
typeDefReader
for reading interfaces and generic parameters.
With a larger number of types being checked for the presence of ExtensionAttribute
, it is likely analysis in NameResolution
should result in an overall greater gain in speed.
Note that statistical optimization for searching MethodDefParent has been added: if the index of the requested method does not fall within the range of methods of the checked type, it is not necessary to call The same optimization can be applied to fields in a separate PR fsharp/src/Compiler/AbstractIL/ilread.fs Lines 2926 to 2936 in 295844f
|
Currently, finding out whether a type in an IL assembly has
Extension
attribute requires reading all attributes from the metadata and constructing a representation ofILAttribute
for each one. This process is time-consuming and involves unnecessary work. It would be beneficial to have a more efficient method for determining if a type can contain extension methods.Furthermore,
ILTypeDef
constructor is a public API but lacks the ability to pass lazy computation of custom attributes. In contrast, the internal constructor in FCS supports this feature.This PR proposes:
ILTypeDef
public constructorExtension
attribute inILTypeDef
(and some refactoring aroundisKnownToBeAttribute
flag)Extension
attribute presenceThis solution will allow for more efficient filtering of
ILTypeDef
that definitely does not contain extension methods, both for users of the public API and within the FCS implementation.