Make virtual delegate targets less costly (#87308)
authorMichal Strehovský <MichalStrehovsky@users.noreply.github.com>
Fri, 16 Jun 2023 00:55:40 +0000 (09:55 +0900)
committerGitHub <noreply@github.com>
Fri, 16 Jun 2023 00:55:40 +0000 (09:55 +0900)
Saves 1.5% on the Stage2 app.

We make sure delegate targets are reflection visible to support Delegate.GetMethodInfo. When I originally did this work in #70198 I made a shortcut to avoid yet another node kind in the system (with a giant comment explaining what the problem is). But there's a lot of benefit in having this new node for apps with many reflection visible virtual methods.

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DelegateTargetVirtualMethodNode.cs [new file with mode: 0644]
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/UsageBasedMetadataManager.cs
src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj
src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/StaticInterfaceMethodDataflow.cs

diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DelegateTargetVirtualMethodNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DelegateTargetVirtualMethodNode.cs
new file mode 100644 (file)
index 0000000..ac11ee0
--- /dev/null
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+
+using ILCompiler.DependencyAnalysisFramework;
+
+using Internal.TypeSystem;
+
+namespace ILCompiler.DependencyAnalysis
+{
+    /// <summary>
+    /// Represents a reflection-visible virtual method that is a target of a delegate.
+    /// </summary>
+    public class DelegateTargetVirtualMethodNode : DependencyNodeCore<NodeFactory>
+    {
+        private readonly MethodDesc _method;
+
+        public DelegateTargetVirtualMethodNode(MethodDesc method)
+        {
+            Debug.Assert(method.GetCanonMethodTarget(CanonicalFormKind.Specific) == method);
+            _method = method;
+        }
+
+        protected override string GetName(NodeFactory factory)
+        {
+            return "Delegate target method: " + _method.ToString();
+        }
+
+        public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFactory factory) => null;
+        public override bool InterestingForDynamicDependencyAnalysis => false;
+        public override bool HasDynamicDependencies => false;
+        public override bool HasConditionalStaticDependencies => false;
+        public override bool StaticDependenciesAreComputed => true;
+        public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory factory) => null;
+        public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory factory) => null;
+    }
+}
index 2b3d9f0..18a27e4 100644 (file)
@@ -308,6 +308,11 @@ namespace ILCompiler.DependencyAnalysis
                 return new TypeGVMEntriesNode(type);
             });
 
+            _delegateTargetMethods = new NodeCache<MethodDesc, DelegateTargetVirtualMethodNode>(method =>
+            {
+                return new DelegateTargetVirtualMethodNode(method);
+            });
+
             _reflectedMethods = new NodeCache<MethodDesc, ReflectedMethodNode>(method =>
             {
                 return new ReflectedMethodNode(method);
@@ -978,6 +983,12 @@ namespace ILCompiler.DependencyAnalysis
             return _gvmTableEntries.GetOrAdd(type);
         }
 
+        private NodeCache<MethodDesc, DelegateTargetVirtualMethodNode> _delegateTargetMethods;
+        public DelegateTargetVirtualMethodNode DelegateTargetVirtualMethod(MethodDesc method)
+        {
+            return _delegateTargetMethods.GetOrAdd(method);
+        }
+
         private NodeCache<MethodDesc, ReflectedMethodNode> _reflectedMethods;
         public ReflectedMethodNode ReflectedMethod(MethodDesc method)
         {
index c8c49f2..4a42b5e 100644 (file)
@@ -541,6 +541,9 @@ namespace ILCompiler
             {
                 dependencies ??= new DependencyList();
                 dependencies.Add(factory.ReflectedMethod(target), "Target of a delegate");
+
+                if (target.IsVirtual)
+                    dependencies.Add(factory.DelegateTargetVirtualMethod(target.GetCanonMethodTarget(CanonicalFormKind.Specific)), "Target of a delegate");
             }
         }
 
@@ -548,29 +551,14 @@ namespace ILCompiler
         {
             Debug.Assert(decl.IsVirtual && MetadataVirtualMethodAlgorithm.FindSlotDefiningMethodForVirtualMethod(decl) == decl);
 
-            // If a virtual method slot is reflection visible, all implementations become reflection visible.
-            //
-            // We could technically come up with a weaker position on this because the code below just needs to
-            // to ensure that delegates to virtual methods can have their GetMethodInfo() called.
-            // Delegate construction introduces a ReflectableMethod for the slot defining method; it doesn't need to.
-            // We could have a specialized node type to track that specific thing and introduce a conditional dependency
-            // on that.
-            //
-            // class Base { abstract Boo(); }
-            // class Derived1 : Base { override Boo() { } }
-            // class Derived2 : Base { override Boo() { } }
-            //
-            // typeof(Derived2).GetMethods(...)
-            //
-            // In the above case, we don't really need Derived1.Boo to become reflection visible
-            // but the below code will do that because ReflectedMethodNode tracks all reflectable methods,
-            // without keeping information about subtleities like "reflectable delegate".
+            // If a virtual method slot is a target of a delegate, all implementations become reflection visible
+            // to support Delegate.GetMethodInfo().
             if (!IsReflectionBlocked(decl) && !IsReflectionBlocked(impl))
             {
                 dependencies ??= new CombinedDependencyList();
                 dependencies.Add(new DependencyNodeCore<NodeFactory>.CombinedDependencyListEntry(
                     factory.ReflectedMethod(impl.GetCanonMethodTarget(CanonicalFormKind.Specific)),
-                    factory.ReflectedMethod(decl.GetCanonMethodTarget(CanonicalFormKind.Specific)),
+                    factory.DelegateTargetVirtualMethod(decl.GetCanonMethodTarget(CanonicalFormKind.Specific)),
                     "Virtual method declaration is reflectable"));
             }
         }
index 99967af..ff83282 100644 (file)
     <Compile Include="Compiler\Dataflow\ValueNode.cs" />
     <Compile Include="Compiler\DebugInformationProvider.cs" />
     <Compile Include="Compiler\DependencyAnalysis\CustomAttributeMetadataNode.cs" />
+    <Compile Include="Compiler\DependencyAnalysis\DelegateTargetVirtualMethodNode.cs" />
     <Compile Include="Compiler\DependencyAnalysis\EmbeddedTrimmingDescriptorNode.cs" />
     <Compile Include="Compiler\DependencyAnalysis\DataflowAnalyzedMethodNode.cs" />
     <Compile Include="Compiler\DependencyAnalysis\DataflowAnalyzedTypeDefinitionNode.cs" />
index 2f92294..e73372b 100644 (file)
@@ -16,6 +16,7 @@ namespace Mono.Linker.Tests.Cases.DataFlow
                public static void Main ()
                {
                        DamOnGenericKeepsMethod.Test ();
+                       DamOnGenericKeepsMethod_UseImplType.Test ();
                        DamOnMethodParameter.Test ();
                }
 
@@ -33,7 +34,9 @@ namespace Mono.Linker.Tests.Cases.DataFlow
                        [KeptInterface (typeof (IFoo))]
                        class ImplIFoo : IFoo
                        {
-                               [Kept]
+                               // NativeAOT correctly finds out that the method is not actually used by anything
+                               // and removes it. The only caveat is GetInterfaceMap - see https://github.com/dotnet/runtimelab/issues/861
+                               [Kept (By = Tool.Trimmer)]
                                public static void VirtualMethod () { }
                        }
 
@@ -55,6 +58,48 @@ namespace Mono.Linker.Tests.Cases.DataFlow
                }
 
                [Kept]
+               static class DamOnGenericKeepsMethod_UseImplType
+               {
+                       [Kept]
+                       interface IFoo
+                       {
+                               [Kept]
+                               public static virtual void VirtualMethod () { }
+                       }
+
+                       [Kept]
+                       [KeptInterface (typeof (IFoo))]
+                       class ImplIFoo : IFoo
+                       {
+                               [Kept]
+                               public static void VirtualMethod () { }
+                       }
+
+
+                       [Kept]
+                       static void MethodWithDamOnType<
+                               [KeptAttributeAttribute (typeof (DynamicallyAccessedMembersAttribute))]
+                       [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
+                       T> ()
+                       {
+                       }
+
+                       [Kept]
+                       class UseStaticInterface<T> where T : IFoo
+                       {
+                               [Kept]
+                               public static void Do () { T.VirtualMethod (); }
+                       }
+
+                       [Kept]
+                       public static void Test ()
+                       {
+                               MethodWithDamOnType<IFoo> ();
+                               UseStaticInterface<ImplIFoo>.Do ();
+                       }
+               }
+
+               [Kept]
                static class DamOnMethodParameter
                {
                        [Kept]
@@ -70,9 +115,13 @@ namespace Mono.Linker.Tests.Cases.DataFlow
                        [KeptInterface (typeof (IFoo))]
                        class ImplIFoo : IFoo
                        {
-                               [Kept]
+                               // NativeAOT correctly finds out that the method is not actually used by anything
+                               // and removes it. The only caveat is GetInterfaceMap - see https://github.com/dotnet/runtimelab/issues/861
+                               [Kept (By = Tool.Trimmer)]
                                public static void VirtualMethod () { }
-                               [Kept]
+                               // NativeAOT correctly finds out that the method is not actually used by anything
+                               // and removes it. The only caveat is GetInterfaceMap - see https://github.com/dotnet/runtimelab/issues/861
+                               [Kept (By = Tool.Trimmer)]
                                public static void AbstractMethod () { }
                        }