diff options
13 files changed, 252 insertions, 35 deletions
diff --git a/src/ILCompiler.Compiler/src/Compiler/Compilation.cs b/src/ILCompiler.Compiler/src/Compiler/Compilation.cs index 443716a15..dd3760c39 100644 --- a/src/ILCompiler.Compiler/src/Compiler/Compilation.cs +++ b/src/ILCompiler.Compiler/src/Compiler/Compilation.cs @@ -25,6 +25,7 @@ namespace ILCompiler protected readonly NodeFactory _nodeFactory; protected readonly Logger _logger; private readonly DebugInformationProvider _debugInformationProvider; + private readonly DevirtualizationManager _devirtualizationManager; public NameMangler NameMangler => _nodeFactory.NameMangler; public NodeFactory NodeFactory => _nodeFactory; @@ -41,12 +42,14 @@ namespace ILCompiler NodeFactory nodeFactory, IEnumerable<ICompilationRootProvider> compilationRoots, DebugInformationProvider debugInformationProvider, + DevirtualizationManager devirtualizationManager, Logger logger) { _dependencyGraph = dependencyGraph; _nodeFactory = nodeFactory; _logger = logger; _debugInformationProvider = debugInformationProvider; + _devirtualizationManager = devirtualizationManager; _dependencyGraph.ComputeDependencyRoutine += ComputeDependencyNodeDependencies; NodeFactory.AttachToDependencyGraph(_dependencyGraph); @@ -178,6 +181,21 @@ namespace ILCompiler return NodeFactory.VTable(type).HasFixedSlots; } + public bool IsEffectivelySealed(TypeDesc type) + { + return _devirtualizationManager.IsEffectivelySealed(type); + } + + public bool IsEffectivelySealed(MethodDesc method) + { + return _devirtualizationManager.IsEffectivelySealed(method); + } + + public MethodDesc ResolveVirtualMethod(MethodDesc declMethod, TypeDesc implType) + { + return _devirtualizationManager.ResolveVirtualMethod(declMethod, implType); + } + public bool NeedsRuntimeLookup(ReadyToRunHelperId lookupKind, object targetOfLookup) { switch (lookupKind) diff --git a/src/ILCompiler.Compiler/src/Compiler/CompilationBuilder.cs b/src/ILCompiler.Compiler/src/Compiler/CompilationBuilder.cs index b93fafd42..7bdaf6abe 100644 --- a/src/ILCompiler.Compiler/src/Compiler/CompilationBuilder.cs +++ b/src/ILCompiler.Compiler/src/Compiler/CompilationBuilder.cs @@ -26,6 +26,7 @@ namespace ILCompiler protected VTableSliceProvider _vtableSliceProvider = new LazyVTableSliceProvider(); protected DictionaryLayoutProvider _dictionaryLayoutProvider = new LazyDictionaryLayoutProvider(); protected DebugInformationProvider _debugInformationProvider = new DebugInformationProvider(); + protected DevirtualizationManager _devirtualizationManager = new DevirtualizationManager(); public CompilationBuilder(CompilerTypeSystemContext context, CompilationModuleGroup compilationGroup, NameMangler nameMangler) { @@ -77,6 +78,12 @@ namespace ILCompiler return this; } + public CompilationBuilder UseDevirtualizationManager(DevirtualizationManager manager) + { + _devirtualizationManager = manager; + return this; + } + public CompilationBuilder UseDebugInfoProvider(DebugInformationProvider provider) { _debugInformationProvider = provider; diff --git a/src/ILCompiler.Compiler/src/Compiler/DevirtualizationManager.cs b/src/ILCompiler.Compiler/src/Compiler/DevirtualizationManager.cs new file mode 100644 index 000000000..8a6227ee4 --- /dev/null +++ b/src/ILCompiler.Compiler/src/Compiler/DevirtualizationManager.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Internal.TypeSystem; + +using Debug = System.Diagnostics.Debug; + +namespace ILCompiler +{ + /// <summary> + /// Manages devirtualization behaviors. Devirtualization is the process of converting + /// virtual calls to direct calls in cases where we can compute the result of a virtual + /// lookup at compile time. + /// </summary> + public class DevirtualizationManager + { + /// <summary> + /// Returns true if <paramref name="type"/> cannot be the base class of any other + /// type. + /// </summary> + public virtual bool IsEffectivelySealed(TypeDesc type) + { + switch (type.Category) + { + case TypeFlags.Array: + case TypeFlags.SzArray: + case TypeFlags.ByRef: + case TypeFlags.Pointer: + case TypeFlags.FunctionPointer: + return true; + + default: + Debug.Assert(type.IsDefType); + var metadataType = (MetadataType)type; + return metadataType.IsSealed || metadataType.IsModuleType; + } + } + + /// <summary> + /// Returns true if <paramref name="method"/> cannot be overriden by any other method. + /// </summary> + public virtual bool IsEffectivelySealed(MethodDesc method) + { + return method.IsFinal || IsEffectivelySealed(method.OwningType); + } + + /// <summary> + /// Attempts to resolve the <paramref name="declMethod"/> virtual method into + /// a method on <paramref name="implType"/> that implements the declaring method. + /// Returns null if this is not possible. + /// </summary> + /// <remarks> + /// Note that if <paramref name="implType"/> is a value type, the result of the resolution + /// might have to be treated as an unboxing thunk by the caller. + /// </remarks> + public MethodDesc ResolveVirtualMethod(MethodDesc declMethod, TypeDesc implType) + { + Debug.Assert(declMethod.IsVirtual); + + // Quick check: if decl matches impl, we're done. + if (declMethod.OwningType == implType) + return declMethod; + + // We're operating on virtual methods. This means that if implType is an array, we need + // to get the type that has all the virtual methods provided by the class library. + return ResolveVirtualMethod(declMethod, implType.GetClosestDefType()); + } + + protected virtual MethodDesc ResolveVirtualMethod(MethodDesc declMethod, DefType implType) + { + MethodDesc impl; + + if (declMethod.OwningType.IsInterface) + { + impl = implType.ResolveInterfaceMethodTarget(declMethod); + if (impl != null) + { + impl = implType.FindVirtualFunctionTargetMethodOnObjectType(impl); + } + } + else + { + impl = implType.FindVirtualFunctionTargetMethodOnObjectType(declMethod); + } + + return impl; + } + } +} diff --git a/src/ILCompiler.Compiler/src/Compiler/ILScanner.cs b/src/ILCompiler.Compiler/src/Compiler/ILScanner.cs index 9b50db7db..aea6f4229 100644 --- a/src/ILCompiler.Compiler/src/Compiler/ILScanner.cs +++ b/src/ILCompiler.Compiler/src/Compiler/ILScanner.cs @@ -30,7 +30,7 @@ namespace ILCompiler IEnumerable<ICompilationRootProvider> roots, DebugInformationProvider debugInformationProvider, Logger logger) - : base(dependencyGraph, nodeFactory, roots, debugInformationProvider, logger) + : base(dependencyGraph, nodeFactory, roots, debugInformationProvider, null, logger) { } @@ -106,6 +106,11 @@ namespace ILCompiler return new ScannedDictionaryLayoutProvider(MarkedNodes); } + public DevirtualizationManager GetDevirtualizationManager() + { + return new ScannedDevirtualizationManager(MarkedNodes); + } + private class ScannedVTableProvider : VTableSliceProvider { private Dictionary<TypeDesc, IReadOnlyList<MethodDesc>> _vtableSlices = new Dictionary<TypeDesc, IReadOnlyList<MethodDesc>>(); @@ -175,5 +180,94 @@ namespace ILCompiler } } } + + private class ScannedDevirtualizationManager : DevirtualizationManager + { + private HashSet<TypeDesc> _constructedTypes = new HashSet<TypeDesc>(); + private HashSet<TypeDesc> _unsealedTypes = new HashSet<TypeDesc>(); + + public ScannedDevirtualizationManager(ImmutableArray<DependencyNodeCore<NodeFactory>> markedNodes) + { + foreach (var node in markedNodes) + { + if (node is ConstructedEETypeNode eetypeNode) + { + TypeDesc type = eetypeNode.Type; + + if (!type.IsInterface) + { + // + // We collect this information: + // + // 1. What types got allocated + // This is needed for optimizing codegens that might attempt to devirtualize + // calls to sealed types. The devirtualization is not allowed to succeed + // for types that never got allocated because the scanner likely didn't scan + // the target of the virtual call. + // 2. What types are the base types of other types + // This is needed for optimizations. We use this information to effectively + // seal types that are not base types for any other type. + // + + TypeDesc canonType = type.ConvertToCanonForm(CanonicalFormKind.Specific); + + _constructedTypes.Add(canonType); + + // Since this is used for the purposes of devirtualization, it's really convenient + // to also have Array<T> for each T[]. + if (canonType.IsArray) + _constructedTypes.Add(canonType.GetClosestDefType()); + + TypeDesc baseType = canonType.BaseType; + bool added = true; + while (baseType != null && added) + { + baseType = baseType.ConvertToCanonForm(CanonicalFormKind.Specific); + added = _unsealedTypes.Add(baseType); + baseType = baseType.BaseType; + } + } + + } + } + } + + public override bool IsEffectivelySealed(TypeDesc type) + { + // If we know we scanned a type that derives from this one, this for sure can't be reported as sealed. + TypeDesc canonType = type.ConvertToCanonForm(CanonicalFormKind.Specific); + if (_unsealedTypes.Contains(canonType)) + return false; + + // We don't want to report types that never got allocated as sealed because that would allow + // the codegen to do direct calls to the type's methods. That can potentially lead to codegen + // generating calls to methods we never scanned (consider a sealed type that never got allocated + // with a virtual method that can be devirtualized because the type is sealed). + // Codegen looking at code we didn't scan is never okay. + if (!_constructedTypes.Contains(canonType)) + return false; + + if (type is MetadataType metadataType) + { + // Due to how the compiler is structured, we might see "constructed" EETypes for things + // that never got allocated (doing a typeof() on a class that is otherwise never used is + // a good example of when that happens). This can put us into a position where we could + // report `sealed` on an `abstract` class, but that doesn't lead to anything good. + return !metadataType.IsAbstract; + } + + // Everything else can be considered sealed. + return true; + } + + public override bool IsEffectivelySealed(MethodDesc method) + { + // For the same reason as above, don't report methods on unallocated types as sealed. + if (!_constructedTypes.Contains(method.OwningType.ConvertToCanonForm(CanonicalFormKind.Specific))) + return false; + + return base.IsEffectivelySealed(method); + } + } } } diff --git a/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilation.cs b/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilation.cs index 1f993c832..97b6ce723 100644 --- a/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilation.cs +++ b/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilation.cs @@ -25,8 +25,9 @@ namespace ILCompiler IEnumerable<ICompilationRootProvider> roots, DebugInformationProvider debugInformationProvider, Logger logger, + DevirtualizationManager devirtualizationManager, JitConfigProvider configProvider) - : base(dependencyGraph, nodeFactory, roots, debugInformationProvider, logger) + : base(dependencyGraph, nodeFactory, roots, debugInformationProvider, devirtualizationManager, logger) { _jitConfigProvider = configProvider; } diff --git a/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilationBuilder.cs b/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilationBuilder.cs index fbd06eabc..773560798 100644 --- a/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilationBuilder.cs +++ b/src/ILCompiler.Compiler/src/Compiler/RyuJitCompilationBuilder.cs @@ -91,7 +91,7 @@ namespace ILCompiler var jitConfig = new JitConfigProvider(jitFlagBuilder.ToArray(), _ryujitOptions); DependencyAnalyzerBase<NodeFactory> graph = CreateDependencyGraph(factory, new ObjectNode.ObjectNodeComparer(new CompilerComparer())); - return new RyuJitCompilation(graph, factory, _compilationRoots, _debugInformationProvider, _logger, jitConfig); + return new RyuJitCompilation(graph, factory, _compilationRoots, _debugInformationProvider, _logger, _devirtualizationManager, jitConfig); } } } diff --git a/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj b/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj index 9215bb331..50d13989d 100644 --- a/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj +++ b/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj @@ -132,6 +132,7 @@ <Compile Include="Compiler\DependencyAnalysis\IMethodBodyNodeWithFuncletSymbols.cs" /> <Compile Include="Compiler\DependencyAnalysis\ISymbolNodeWithFuncletId.cs" /> <Compile Include="Compiler\DependencyAnalysis\LoopHijackFlagNode.cs" /> + <Compile Include="Compiler\DevirtualizationManager.cs" /> <Compile Include="Compiler\DictionaryLayoutProvider.cs" /> <Compile Include="Compiler\EmptyInteropStubManager.cs" /> <Compile Include="Compiler\DependencyAnalysis\SortableDependencyNode.cs" /> diff --git a/src/ILCompiler.CppCodeGen/src/Compiler/CppCodegenCompilation.cs b/src/ILCompiler.CppCodeGen/src/Compiler/CppCodegenCompilation.cs index 2a3e474a9..92a26eb42 100644 --- a/src/ILCompiler.CppCodeGen/src/Compiler/CppCodegenCompilation.cs +++ b/src/ILCompiler.CppCodeGen/src/Compiler/CppCodegenCompilation.cs @@ -25,7 +25,7 @@ namespace ILCompiler DebugInformationProvider debugInformationProvider, Logger logger, CppCodegenConfigProvider options) - : base(dependencyGraph, nodeFactory, GetCompilationRoots(roots, nodeFactory), debugInformationProvider, logger) + : base(dependencyGraph, nodeFactory, GetCompilationRoots(roots, nodeFactory), debugInformationProvider, null, logger) { Options = options; } diff --git a/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs b/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs index 2853751ec..94b35a3f4 100644 --- a/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs +++ b/src/ILCompiler.WebAssembly/src/Compiler/WebAssemblyCodegenCompilation.cs @@ -23,7 +23,7 @@ namespace ILCompiler IEnumerable<ICompilationRootProvider> roots, Logger logger, WebAssemblyCodegenConfigProvider options) - : base(dependencyGraph, nodeFactory, GetCompilationRoots(roots, nodeFactory), null, logger) + : base(dependencyGraph, nodeFactory, GetCompilationRoots(roots, nodeFactory), null, null, logger) { NodeFactory = nodeFactory; LLVM.LoadLibrary_libLLVM("./libLLVM-x64.dll"); diff --git a/src/ILCompiler/src/Program.cs b/src/ILCompiler/src/Program.cs index d0991609c..96e63fd2f 100644 --- a/src/ILCompiler/src/Program.cs +++ b/src/ILCompiler/src/Program.cs @@ -437,6 +437,12 @@ namespace ILCompiler // If we have a scanner, feed the generic dictionary results to the compilation. // This could be a command line switch if we really wanted to. builder.UseGenericDictionaryLayoutProvider(scanResults.GetDictionaryLayoutInfo()); + + // If we feed any outputs of the scanner into the compilation, it's essential + // we use scanner's devirtualization manager. It prevents optimizing codegens + // from accidentally devirtualizing cases that can never happen at runtime + // (e.g. devirtualizing a method on a type that never gets allocated). + builder.UseDevirtualizationManager(scanResults.GetDevirtualizationManager()); } ICompilation compilation = builder.ToCompilation(); diff --git a/src/JitInterface/src/CorInfoImpl.cs b/src/JitInterface/src/CorInfoImpl.cs index d71b9a8f1..04bb65cf9 100644 --- a/src/JitInterface/src/CorInfoImpl.cs +++ b/src/JitInterface/src/CorInfoImpl.cs @@ -723,11 +723,8 @@ namespace Internal.JitInterface // method body. // - var owningType = method.OwningType; - var owningMetadataType = owningType as MetadataType; - // method or class might have the final bit - if (method.IsFinal || (owningMetadataType != null && owningMetadataType.IsSealed)) + if (_compilation.IsEffectivelySealed(method)) result |= CorInfoFlag.CORINFO_FLG_FINAL; if (method.IsSharedByGenericInstantiations) @@ -754,7 +751,7 @@ namespace Internal.JitInterface result |= CorInfoFlag.CORINFO_FLG_FORCEINLINE; } - if (owningType.IsDelegate) + if (method.OwningType.IsDelegate) { if (method.Name == "Invoke") // This is now used to emit efficient invoke code for any delegate invoke, @@ -867,35 +864,20 @@ namespace Internal.JitInterface return null; } - implType = implType.GetClosestDefType(); - MethodDesc decl = HandleToObject(baseMethod); - Debug.Assert(decl.IsVirtual); Debug.Assert(!decl.HasInstantiation); - MethodDesc impl; - - TypeDesc declOwningType = decl.OwningType; - if (declOwningType.IsInterface) + if (ownerType != null) { - // Interface call devirtualization. - - if (implType.IsCanonicalSubtype(CanonicalFormKind.Any)) - { - // TODO: attempt to devirtualize methods on canonical interfaces - return null; - } - - impl = implType.ResolveInterfaceMethodTarget(decl); - if (impl != null) + TypeDesc ownerTypeDesc = typeFromContext(ownerType); + if (decl.OwningType != ownerTypeDesc) { - impl = implType.GetClosestDefType().FindVirtualFunctionTargetMethodOnObjectType(impl); + Debug.Assert(ownerTypeDesc is InstantiatedType); + decl = _compilation.TypeSystemContext.GetMethodForInstantiatedType(decl.GetTypicalMethodDefinition(), (InstantiatedType)ownerTypeDesc); } } - else - { - impl = implType.GetClosestDefType().FindVirtualFunctionTargetMethodOnObjectType(decl); - } + + MethodDesc impl = _compilation.ResolveVirtualMethod(decl, implType); return impl != null ? ObjectToHandle(impl) : null; } @@ -1320,6 +1302,9 @@ namespace Internal.JitInterface if (type.IsDelegate) result |= CorInfoFlag.CORINFO_FLG_DELEGATE; + if (_compilation.IsEffectivelySealed(type)) + result |= CorInfoFlag.CORINFO_FLG_FINAL; + if (metadataType != null) { if (metadataType.ContainsGCPointers) @@ -1328,9 +1313,6 @@ namespace Internal.JitInterface if (metadataType.IsBeforeFieldInit) result |= CorInfoFlag.CORINFO_FLG_BEFOREFIELDINIT; - if (metadataType.IsSealed) - result |= CorInfoFlag.CORINFO_FLG_FINAL; - // Assume overlapping fields for explicit layout. if (metadataType.IsExplicitLayout) result |= CorInfoFlag.CORINFO_FLG_OVERLAPPING_FIELDS; diff --git a/src/System.Private.Jit/src/Internal/Runtime/JitSupport/JitCompilation.cs b/src/System.Private.Jit/src/Internal/Runtime/JitSupport/JitCompilation.cs index 85eb8a03c..e628978b9 100644 --- a/src/System.Private.Jit/src/Internal/Runtime/JitSupport/JitCompilation.cs +++ b/src/System.Private.Jit/src/Internal/Runtime/JitSupport/JitCompilation.cs @@ -23,6 +23,7 @@ namespace ILCompiler _typeGetTypeMethodThunks = new TypeGetTypeMethodThunkCache(context.GetWellKnownType(WellKnownType.Object)); _methodILCache = new ILProvider(new PInvokeILProvider(new PInvokeILEmitterConfiguration(forceLazyResolution: true), null)); _nodeFactory = new NodeFactory(context); + _devirtualizationManager = new DevirtualizationManager(); } private readonly NodeFactory _nodeFactory; @@ -30,6 +31,7 @@ namespace ILCompiler protected readonly Logger _logger = Logger.Null; private readonly TypeGetTypeMethodThunkCache _typeGetTypeMethodThunks; private ILProvider _methodILCache; + private readonly DevirtualizationManager _devirtualizationManager; internal Logger Logger => _logger; @@ -102,6 +104,21 @@ namespace ILCompiler return true; } + public bool IsEffectivelySealed(TypeDesc type) + { + return _devirtualizationManager.IsEffectivelySealed(type); + } + + public bool IsEffectivelySealed(MethodDesc method) + { + return _devirtualizationManager.IsEffectivelySealed(method); + } + + public MethodDesc ResolveVirtualMethod(MethodDesc declMethod, TypeDesc implType) + { + return _devirtualizationManager.ResolveVirtualMethod(declMethod, implType); + } + public bool NeedsRuntimeLookup(ReadyToRunHelperId lookupKind, object targetOfLookup) { // The current plan seem to be to copy paste from ILCompiler.Compilation, but that's not a sustainable plan diff --git a/src/System.Private.Jit/src/System.Private.Jit.csproj b/src/System.Private.Jit/src/System.Private.Jit.csproj index 5df579bb4..270c33469 100644 --- a/src/System.Private.Jit/src/System.Private.Jit.csproj +++ b/src/System.Private.Jit/src/System.Private.Jit.csproj @@ -134,6 +134,7 @@ <Compile Include="$(ILCompilerBasePath)\Compiler\TypeExtensions.cs" /> <Compile Include="$(ILCompilerBasePath)\Compiler\NameMangler.cs" /> <Compile Include="$(ILCompilerBasePath)\Compiler\NodeMangler.cs" /> + <Compile Include="$(ILCompilerBasePath)\Compiler\DevirtualizationManager.cs" /> <Compile Include="$(ILCompilerBasePath)\IL\Stubs\PInvokeILProvider.cs" /> <Compile Include="$(DependencyAnalysisFrameworkBasePath)\IDependencyNode.cs" /> <Compile Include="$(DependencyAnalysisFrameworkBasePath)\DependencyNode.cs" /> |