From 255a1287964c28b94a88fb1accae1c9527619dc1 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 31 Oct 2022 16:28:01 +0100 Subject: [PATCH] Add ability for crossgen2 to synthesize PGO histograms Add an option --synthesize-random-mibc. When passed, crossgen2 will use metadata from the compilation group to synthesize PGO histograms for methods that did not have any input profile data. Mainly for testing purposes. Currently only works in R2R mode, but hopefully will also work to embed random static PGO data in the future. --- .../tools/Common/JitInterface/CorInfoImpl.cs | 11 +- .../Compiler/ProfileData.cs | 5 +- .../Compiler/ProfileDataManager.cs | 342 +++++++++++++++--- .../ReadyToRunCompilationModuleGroupBase.cs | 2 +- ...RunSingleAssemblyCompilationModuleGroup.cs | 2 +- .../Compiler/ProfileDataManager.cs | 4 +- .../aot/crossgen2/Crossgen2RootCommand.cs | 3 + src/coreclr/tools/aot/crossgen2/Program.cs | 3 +- src/coreclr/tools/dotnet-pgo/Program.cs | 3 +- 9 files changed, 302 insertions(+), 73 deletions(-) diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index a5c324a33fada..42dcfabd3a8d6 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3919,11 +3919,6 @@ private uint getJitFlags(ref CORJIT_FLAGS flags, uint sizeInBytes) return (uint)sizeof(CORJIT_FLAGS); } - private PgoSchemaElem[] getPgoInstrumentationResults(MethodDesc method) - { - return _compilation.ProfileData[method]?.SchemaData; - } - private MemoryStream _cachedMemoryStream = new MemoryStream(); public static void ComputeJitPgoInstrumentationSchema(Func objectToHandle, PgoSchemaElem[] pgoResultsSchemas, out PgoInstrumentationSchema[] nativeSchemas, MemoryStream instrumentationData, Func typeFilter = null) @@ -4001,7 +3996,11 @@ private HRESULT getPgoInstrumentationResults(CORINFO_METHOD_STRUCT_* ftnHnd, ref if (!_pgoResults.TryGetValue(methodDesc, out PgoInstrumentationResults pgoResults)) { - var pgoResultsSchemas = getPgoInstrumentationResults(methodDesc); +#if READYTORUN + PgoSchemaElem[] pgoResultsSchemas = _compilation.ProfileData.GetAllowSynthesis(_compilation, methodDesc)?.SchemaData; +#else + PgoSchemaElem[] pgoResultsSchemas = _compilation.ProfileData[methodDesc]?.SchemaData; +#endif if (pgoResultsSchemas == null) { pgoResults.hr = HRESULT.E_NOTIMPL; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileData.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileData.cs index acc55fb5aa601..54d0c1968ea8f 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileData.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileData.cs @@ -62,11 +62,8 @@ public abstract class ProfileData public abstract IEnumerable GetAllMethodProfileData(); public abstract byte[] GetMethodBlockCount(MethodDesc m); - public static void MergeProfileData(ref bool partialNgen, Dictionary mergedProfileData, ProfileData profileData) + public static void MergeProfileData(Dictionary mergedProfileData, ProfileData profileData) { - if (profileData.PartialNGen) - partialNgen = true; - PgoSchemaElem[][] schemaElemMergerArray = new PgoSchemaElem[2][]; foreach (MethodProfileData data in profileData.GetAllMethodProfileData()) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs index c7560253c38d1..9c1f5e5076fe2 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ProfileDataManager.cs @@ -3,11 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection.PortableExecutable; - using ILCompiler.IBC; - +using Internal.IL; using Internal.Pgo; using Internal.TypeSystem; using Internal.TypeSystem.Ecma; @@ -18,12 +18,11 @@ public class ProfileDataManager { private readonly IBCProfileParser _ibcParser; private readonly List _inputData = new List(); - private readonly Dictionary _mergedProfileData = new Dictionary(); - private readonly Dictionary> _placedProfileMethods = new Dictionary>(); - private readonly HashSet _placedProfileMethodsAll = new HashSet(); - private readonly bool _partialNGen; + private readonly ProfileDataMap _inputProfileData; + private readonly ProfileDataMap _synthesizedProfileData; private readonly ReadyToRunCompilationModuleGroupBase _compilationGroup; private readonly CallChainProfile _callChainProfile; + private readonly GdvEntityFinder _gdvEntityFinder; public ProfileDataManager(Logger logger, IEnumerable possibleReferenceModules, @@ -38,7 +37,8 @@ public ProfileDataManager(Logger logger, ReadyToRunCompilationModuleGroupBase compilationGroup, bool embedPgoDataInR2RImage, bool parseIbcData, - Func canBeIncludedInCurrentCompilation) + Func canBeIncludedInCurrentCompilation, + bool synthesizeRandomPgoData) { EmbedPgoDataInR2RImage = embedPgoDataInR2RImage; _ibcParser = new IBCProfileParser(logger, possibleReferenceModules); @@ -84,86 +84,318 @@ public ProfileDataManager(Logger logger, } } - // Ensure each module has a hashset of methods available - foreach (var module in inputModules) + _inputProfileData = new ProfileDataMap(nonLocalGenericsHome, _compilationGroup); + _inputProfileData.LoadByMerging(_inputData); + + if (synthesizeRandomPgoData) + { + _gdvEntityFinder = new GdvEntityFinder(versionBubbleModules); + _synthesizedProfileData = new ProfileDataMap(nonLocalGenericsHome, _compilationGroup); + } + } + + public IEnumerable GetMethodsForModuleDesc(ModuleDesc moduleDesc) + { + return _inputProfileData.GetPlacedMethodsForModuleDesc(moduleDesc); + } + + public bool IsMethodInInputProfileData(MethodDesc method) + { + return _inputProfileData.Contains(method); + } + + public MethodProfileData this[MethodDesc method] + { + get { - _placedProfileMethods.Add(module, new HashSet()); + MethodProfileData mpd = _inputProfileData[method]; + if (mpd == null && _synthesizedProfileData != null) + { + lock (_synthesizedProfileData) + { + mpd = _synthesizedProfileData[method]; + } + } + + return mpd; } + } + + public bool EmbedPgoDataInR2RImage { get; } + public CallChainProfile CallChainProfile => _callChainProfile; - // Merge all data together - foreach (ProfileData profileData in _inputData) + public MethodProfileData GetAllowSynthesis(Compilation comp, MethodDesc method) + { + MethodProfileData existingProfileData = this[method]; + if (_synthesizedProfileData == null || existingProfileData != null) { - ProfileData.MergeProfileData(ref _partialNGen, _mergedProfileData, profileData); + return existingProfileData; } - // With the merged data find the set of methods to be placed within this module - foreach (var profileData in _mergedProfileData) + MethodProfileData profileData = null; + // We only support synthesizing PGO data for normal methods. + if (method is EcmaMethod or MethodForInstantiatedType or InstantiatedMethod) { - // If the method is not excluded from processing - if (!profileData.Value.Flags.HasFlag(MethodProfilingDataFlags.ExcludeHotMethodCode) && - !profileData.Value.Flags.HasFlag(MethodProfilingDataFlags.ExcludeColdMethodCode)) + profileData = new MethodProfileData(method, MethodProfilingDataFlags.ReadMethodCode, 0, null, 0, SynthesizeSchema(comp, method)); + + lock (_synthesizedProfileData) { - // Check for methods which are defined within the version bubble, and only rely on other modules within the bubble - if (!_compilationGroup.VersionsWithMethodBody(profileData.Key) && !_compilationGroup.CrossModuleCompileable(profileData.Key)) - continue; // Method not contained within version bubble and not cross module compileable + // We may race here but SynthesizeSchema is deterministic. + // Still, ensure that all threads end up seeing the same + // profile data instance. + existingProfileData = _synthesizedProfileData[method]; + if (existingProfileData != null) + profileData = existingProfileData; + else + _synthesizedProfileData.Add(profileData); + } + } - if (_compilationGroup.ContainsType(profileData.Key.OwningType) && - (profileData.Key.OwningType is MetadataType declaringType)) + return profileData; + } + + private PgoSchemaElem[] SynthesizeSchema(Compilation comp, MethodDesc method) + { + Random rand = new Random(method.GetHashCode()); + // We may be asked to synthesize PGO data for methods not in the + // compilation group due to cross-version bubble inlining (e.g. + // System.Object..ctor). + if (!_compilationGroup.ContainsMethodBody(method, false)) + { + return null; + } + + List elems = new(); + MethodIL il = comp.GetMethodIL(method); + ILReader reader = new ILReader(il.GetILBytes()); + while (reader.HasNext) + { + int instructionOffset = reader.Offset; + ILOpcode opcode = reader.ReadILOpcode(); + if (opcode is ILOpcode.call or ILOpcode.callvirt) + { + int token = reader.ReadILToken(); + object obj = il.GetObject(token); + MethodDesc targetMeth = obj as MethodDesc; + if (targetMeth == null) + continue; + + if (targetMeth.Signature.IsStatic) + continue; + + bool isDelegateInvoke = targetMeth.OwningType.IsDelegate && targetMeth.Name == "Invoke"; + + if (opcode != ILOpcode.callvirt && !isDelegateInvoke) + continue; + + List entities; + PgoInstrumentationKind kind; + + if (isDelegateInvoke) { - // In this case the method is placed in its natural home (which is the defining module of the method) - _placedProfileMethods[declaringType.Module].Add(profileData.Key); - _placedProfileMethodsAll.Add(profileData.Key); + entities = SampleMethodsCompatibleWithDelegateInvocation(targetMeth.Signature, rand); + kind = PgoInstrumentationKind.HandleHistogramMethods; } else { - // If the defining module is not within the input set, if the nonLocalGenericsHome is provided, place it there - if ((nonLocalGenericsHome != null) && (profileData.Key.GetTypicalMethodDefinition() != profileData.Key)) - { - _placedProfileMethods[nonLocalGenericsHome].Add(profileData.Key); - _placedProfileMethodsAll.Add(profileData.Key); - } + entities = new List(0); + kind = PgoInstrumentationKind.HandleHistogramTypes; } + + elems.Add( + new PgoSchemaElem + { + InstrumentationKind = PgoInstrumentationKind.HandleHistogramIntCount, + Count = 1, + ILOffset = instructionOffset, + DataLong = 104729, + }); + + elems.Add( + new PgoSchemaElem + { + InstrumentationKind = kind, + Count = entities.Count, + ILOffset = instructionOffset, + DataObject = entities.ToArray() + }); + } + else + { + reader.Skip(opcode); } } + + if (elems.Count > 0) + { + // TODO: JIT should be able to handle PGO data that only contains histograms. + elems.Insert(0, new PgoSchemaElem { InstrumentationKind = PgoInstrumentationKind.BasicBlockIntCount, ILOffset = 0, Count = 1, DataLong = 5 }); + return elems.ToArray(); + } + else + { + return null; + } } - /// - /// Get the defining module for a method which is entirely defined within the version bubble - /// If a module is a generic which has interaction modules outside of the version bubble, return null. - /// - private ModuleDesc GetDefiningModuleForMethodWithinVersionBubble(MethodDesc method, HashSet versionBubble) + private List SampleMethodsCompatibleWithDelegateInvocation(MethodSignature delegateSignature, Random rand) { - if (_compilationGroup.VersionsWithMethodBody(method) && (method.OwningType is MetadataType metadataType)) + Debug.Assert(_gdvEntityFinder != null); + + const int sampleSize = 3; + List result = new(sampleSize); + + IList compatible = _gdvEntityFinder.GetCompatibleWithDelegateInvoke(delegateSignature); + if (compatible.Count <= sampleSize) { - return metadataType.Module; + foreach (MethodDesc md in compatible) + result.Add(new TypeSystemEntityOrUnknown(md)); + } + else + { + while (result.Count < sampleSize) + { + int index = rand.Next(compatible.Count); + MethodDesc md = compatible[index]; + bool contains = false; + foreach (TypeSystemEntityOrUnknown existing in result) + { + if (existing.AsMethod == md) + { + contains = true; + break; + } + } + + if (!contains) + result.Add(new TypeSystemEntityOrUnknown(md)); + } } - return null; + return result; } - public IEnumerable GetMethodsForModuleDesc(ModuleDesc moduleDesc) + private class ProfileDataMap { - if (_placedProfileMethods.TryGetValue(moduleDesc, out var precomputedProfileData)) - return precomputedProfileData.ToArray(); + private readonly ModuleDesc _nonLocalGenericsHome; + private readonly ReadyToRunCompilationModuleGroupBase _compilationGroup; + private readonly Dictionary _profileData = new(); + private readonly Dictionary> _placedProfileMethods = new(); + private readonly HashSet _placedProfileMethodsAll = new(); - return Array.Empty(); - } + public ProfileDataMap(ModuleDesc nonLocalGenericsHome, ReadyToRunCompilationModuleGroupBase compilationGroup) + { + _nonLocalGenericsHome = nonLocalGenericsHome; + _compilationGroup = compilationGroup; + } - public bool IsMethodInProfileData(MethodDesc method) - { - return _placedProfileMethodsAll.Contains(method); + public MethodProfileData this[MethodDesc method] + => _profileData.GetValueOrDefault(method); + + public void LoadByMerging(IEnumerable data) + { + // Merge all data together + foreach (ProfileData profileData in data) + { + ProfileData.MergeProfileData(_profileData, profileData); + } + + // With the merged data find the set of methods to be placed within this module + foreach ((MethodDesc method, MethodProfileData profileData) in _profileData) + { + AssociateMethodProfileDataWithModule(method, profileData); + } + } + + public bool Contains(MethodDesc md) => _profileData.ContainsKey(md); + + private void AssociateMethodProfileDataWithModule(MethodDesc method, MethodProfileData profileData) + { + // If the method is not excluded from processing + if (profileData.Flags.HasFlag(MethodProfilingDataFlags.ExcludeHotMethodCode) || + profileData.Flags.HasFlag(MethodProfilingDataFlags.ExcludeColdMethodCode)) + { + return; + } + + // Check for methods which are defined within the version bubble, and only rely on other modules within the bubble + if (!_compilationGroup.VersionsWithMethodBody(method) && !_compilationGroup.CrossModuleCompileable(method)) + return; // Method not contained within version bubble and not cross module compileable + + ModuleDesc home = null; + if (_compilationGroup.ContainsType(method.OwningType) && + (method.OwningType is MetadataType declaringType)) + { + // In this case the method is placed in its natural home (which is the defining module of the method) + home = declaringType.Module; + } + else + { + // If the defining module is not within the input set, if the nonLocalGenericsHome is provided, place it there + if ((_nonLocalGenericsHome != null) && (method.GetTypicalMethodDefinition() != method)) + { + home = _nonLocalGenericsHome; + } + } + + if (home != null) + { + if (!_placedProfileMethods.TryGetValue(home, out HashSet set)) + _placedProfileMethods.Add(home, set = new HashSet()); + + set.Add(method); + _placedProfileMethodsAll.Add(method); + } + } + + public IEnumerable GetPlacedMethodsForModuleDesc(ModuleDesc moduleDesc) + { + if (_placedProfileMethods.TryGetValue(moduleDesc, out var precomputedProfileData)) + return precomputedProfileData.ToArray(); + + return Array.Empty(); + } + + + public void Add(MethodProfileData profileData) + { + _profileData.Add(profileData.Method, profileData); + AssociateMethodProfileDataWithModule(profileData.Method, profileData); + } } - public MethodProfileData this[MethodDesc method] + private class GdvEntityFinder { - get + // Currently, we just use direct signature equality between the + // delegate's Invoke method and the target method. This does not + // take covariance into account in addition to other more + // restrictive MethodSignature checks. + private readonly Dictionary> _delegateTargets = new(); + + public GdvEntityFinder(IEnumerable modules) { - _mergedProfileData.TryGetValue(method, out var profileData); - return profileData; + foreach (ModuleDesc module in modules) + { + foreach (MetadataType type in module.GetAllTypes()) + { + foreach (MethodDesc method in type.GetMethods()) + { + if (method.Signature.IsStatic) + continue; + + if (!_delegateTargets.TryGetValue(method.Signature, out List list)) + _delegateTargets.Add(method.Signature, list = new List()); + + list.Add(method); + } + } + } } - } - public bool EmbedPgoDataInR2RImage { get; } - public CallChainProfile CallChainProfile => _callChainProfile; + public IList GetCompatibleWithDelegateInvoke(MethodSignature delegateInvokeSignature) + { + return _delegateTargets.TryGetValue(delegateInvokeSignature, out List methods) ? methods : Array.Empty(); + } + } } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs index cb4532ac16705..94115c782acfb 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs @@ -509,7 +509,7 @@ private bool CrossModuleInlineableInternal(MethodDesc method) return _crossModuleInlineableCache.GetOrAdd(method, _crossModuleInlineableCacheUncached); } - private bool CrossModuleInlineableUncached(MethodDesc method) + private bool CrossModuleInlineableUncached(MethodDesc method) { // Defined in corelib MetadataType owningMetadataType = method.OwningType.GetTypeDefinition() as MetadataType; diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs index 11eaac885050b..db114cba999b9 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunSingleAssemblyCompilationModuleGroup.cs @@ -29,7 +29,7 @@ public sealed override bool ContainsMethodBody(MethodDesc method, bool unboxingS if (_profileGuidedCompileRestriction != null) { - if (!_profileGuidedCompileRestriction.IsMethodInProfileData(method)) + if (!_profileGuidedCompileRestriction.IsMethodInInputProfileData(method)) return false; } diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/ProfileDataManager.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/ProfileDataManager.cs index 7e2ac2051b5c0..22cf86291fd10 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/ProfileDataManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/ProfileDataManager.cs @@ -26,12 +26,10 @@ public ProfileDataManager(IEnumerable mibcFiles, } } - bool dummy = false; - // Merge all data together foreach (ProfileData profileData in _inputData) { - ProfileData.MergeProfileData(ref dummy, _mergedProfileData, profileData); + ProfileData.MergeProfileData(_mergedProfileData, profileData); } } diff --git a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs index d823ea26ca26d..54014fabf999f 100644 --- a/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs +++ b/src/coreclr/tools/aot/crossgen2/Crossgen2RootCommand.cs @@ -178,6 +178,8 @@ internal class Crossgen2RootCommand : RootCommand new(new[] { "--make-repro-path" }, "Path where to place a repro package"); public Option HotColdSplitting { get; } = new(new[] { "--hot-cold-splitting" }, SR.HotColdSplittingOption); + public Option SynthesizeRandomMibc { get; } = + new(new[] { "--synthesize-random-mibc" }); public bool CompositeOrInputBubble { get; private set; } public OptimizationMode OptimizationMode { get; private set; } @@ -243,6 +245,7 @@ public Crossgen2RootCommand(string[] args) : base(SR.Crossgen2BannerText) AddOption(CallChainProfileFile); AddOption(MakeReproPath); AddOption(HotColdSplitting); + AddOption(SynthesizeRandomMibc); this.SetHandler(context => { diff --git a/src/coreclr/tools/aot/crossgen2/Program.cs b/src/coreclr/tools/aot/crossgen2/Program.cs index c09d42c7afd1a..76724216063d6 100644 --- a/src/coreclr/tools/aot/crossgen2/Program.cs +++ b/src/coreclr/tools/aot/crossgen2/Program.cs @@ -503,7 +503,8 @@ private void RunSingleCompilation(Dictionary inFilePaths, Instru compilationGroup, Get(_command.EmbedPgoData), Get(_command.SupportIbc), - crossModuleInlineableCode.Count == 0 ? compilationGroup.VersionsWithMethodBody : compilationGroup.CrossModuleInlineable); + crossModuleInlineableCode.Count == 0 ? compilationGroup.VersionsWithMethodBody : compilationGroup.CrossModuleInlineable, + Get(_command.SynthesizeRandomMibc)); bool partial = Get(_command.Partial); compilationGroup.ApplyProfileGuidedOptimizationData(profileDataManager, partial); diff --git a/src/coreclr/tools/dotnet-pgo/Program.cs b/src/coreclr/tools/dotnet-pgo/Program.cs index f7ba495f5acb5..1cd2c0f74084a 100644 --- a/src/coreclr/tools/dotnet-pgo/Program.cs +++ b/src/coreclr/tools/dotnet-pgo/Program.cs @@ -428,13 +428,12 @@ private int InnerMergeMain() { var tsc = new TypeRefTypeSystem.TypeRefTypeSystemContext(mibcReaders); - bool partialNgen = false; Dictionary mergedProfileData = new Dictionary(); for (int i = 0; i < mibcReaders.Length; i++) { var peReader = mibcReaders[i]; PrintDetailedMessage($"Merging {paths[i]}"); - ProfileData.MergeProfileData(ref partialNgen, mergedProfileData, MIbcProfileParser.ParseMIbcFile(tsc, peReader, assemblyNamesInBubble, onlyDefinedInAssembly: null)); + ProfileData.MergeProfileData(mergedProfileData, MIbcProfileParser.ParseMIbcFile(tsc, peReader, assemblyNamesInBubble, onlyDefinedInAssembly: null)); } MibcConfig mergedConfig = ParseMibcConfigsAndMerge(tsc, mibcReaders);