From 8a8c2199559849dbe0764df28a0ca1444ab8ea5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Thu, 3 Oct 2024 19:49:56 +0900 Subject: [PATCH] Allow devirt into abstract classes if we saw a non-abstract child (#108379) We avoid devirtualizing into abstract classes because whole program view might have optimized away the method bodies and devirtualizing them doesn't lead to anything good. However, if the whole program view had a non-abstract child of this, we can no longer optimize this out and devirtualization should be fine. Fixes issue encountered in https://github.com/dotnet/runtime/pull/108153#issuecomment-2380094723 --- .../tools/Common/Compiler/MethodExtensions.cs | 1 + .../ILCompiler.Compiler/Compiler/ILScanner.cs | 39 +++-- .../SmokeTests/UnitTests/Devirtualization.cs | 34 +++++ .../SmokeTests/UnitTests/Interfaces.cs | 143 ++++++++++++++++++ 4 files changed, 201 insertions(+), 16 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/MethodExtensions.cs b/src/coreclr/tools/Common/Compiler/MethodExtensions.cs index 160b685987125..8943f7b3908a1 100644 --- a/src/coreclr/tools/Common/Compiler/MethodExtensions.cs +++ b/src/coreclr/tools/Common/Compiler/MethodExtensions.cs @@ -130,6 +130,7 @@ public static bool NotCallableWithoutOwningEEType(this MethodDesc method) TypeDesc owningType = method.OwningType; return !method.Signature.IsStatic && /* Static methods don't have this */ !owningType.IsValueType && /* Value type instance methods take a ref to data */ + !owningType.IsInterface && /* Interface MethodTable can be optimized away but the instance method can still be callable (`this` is of a non-interface type) */ !owningType.IsArrayTypeWithoutGenericInterfaces() && /* Type loader can make these at runtime */ (owningType is not MetadataType mdType || !mdType.IsModuleType) && /* Compiler parks some instance methods on the type */ !method.IsSharedByGenericInstantiations; /* Current impl limitation; can be lifted */ diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs index f93de22562731..25eefff027035 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs @@ -541,16 +541,27 @@ public ScannedDevirtualizationManager(NodeFactory factory, ImmutableArray typeof(Something); + } + + sealed class Derived : Base { } + + class Unrelated : Base + { + public override Type GetSomething() => typeof(Unrelated); + } + + public static void Run() + { + TestUnrelated(new Unrelated()); + + // We were getting a scanning failure because GetSomething got devirtualized into + // Base.GetSomething, but that's unreachable. + Test(null); + + [MethodImpl(MethodImplOptions.NoInlining)] + static Type Test(Derived d) => d?.GetSomething(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static Type TestUnrelated(Base d) => d?.GetSomething(); + } + } + class RegressionBug73076 { interface IFactory diff --git a/src/tests/nativeaot/SmokeTests/UnitTests/Interfaces.cs b/src/tests/nativeaot/SmokeTests/UnitTests/Interfaces.cs index 824336106ef9c..6a2ce7011def2 100644 --- a/src/tests/nativeaot/SmokeTests/UnitTests/Interfaces.cs +++ b/src/tests/nativeaot/SmokeTests/UnitTests/Interfaces.cs @@ -38,6 +38,8 @@ public static int Run() TestPublicAndNonpublicDifference.Run(); TestDefaultInterfaceMethods.Run(); + TestDefaultInterfaceMethodsDevirtNoInline.Run(); + TestDefaultInterfaceMethodsNoDevirt.Run(); TestDefaultInterfaceVariance.Run(); TestVariantInterfaceOptimizations.Run(); TestSharedInterfaceMethods.Run(); @@ -581,6 +583,147 @@ public static void Run() } } + class TestDefaultInterfaceMethodsDevirtNoInline + { + interface IFoo + { + [MethodImpl(MethodImplOptions.NoInlining)] + int GetNumber() => 42; + } + + interface IBar : IFoo + { + [MethodImpl(MethodImplOptions.NoInlining)] + int IFoo.GetNumber() => 43; + } + + class Foo : IFoo { } + class Bar : IBar { } + + class Baz : IFoo + { + [MethodImpl(MethodImplOptions.NoInlining)] + public int GetNumber() => 100; + } + + interface IFoo + { + [MethodImpl(MethodImplOptions.NoInlining)] + Type GetInterfaceType() => typeof(IFoo); + } + + class Foo : IFoo { } + + class Base : IFoo + { + [MethodImpl(MethodImplOptions.NoInlining)] + int IFoo.GetNumber() => 100; + } + + class Derived : Base, IBar { } + + public static void Run() + { + Console.WriteLine("Testing default interface methods that can be devirtualized but not inlined..."); + + typeof(IFoo).ToString(); + + if (((IFoo)new Foo()).GetNumber() != 42) + throw new Exception(); + + if (((IFoo)new Bar()).GetNumber() != 43) + throw new Exception(); + + if (((IFoo)new Baz()).GetNumber() != 100) + throw new Exception(); + + if (((IFoo)new Derived()).GetNumber() != 100) + throw new Exception(); + + if (((IFoo)new Foo()).GetInterfaceType() != typeof(IFoo)) + throw new Exception(); + + if (((IFoo)new Foo()).GetInterfaceType() != typeof(IFoo)) + throw new Exception(); + } + } + + class TestDefaultInterfaceMethodsNoDevirt + { + interface IFoo + { + int GetNumber() => 42; + } + + interface IBar : IFoo + { + int IFoo.GetNumber() => 43; + } + + class Foo : IFoo { } + class Bar : IBar { } + + class Baz : IFoo + { + public int GetNumber() => 100; + } + + interface IFoo + { + Type GetInterfaceType() => typeof(IFoo); + } + + class Foo : IFoo { } + + class Base : IFoo + { + int IFoo.GetNumber() => 100; + } + + class Derived : Base, IBar { } + + public static void Run() + { + Console.WriteLine("Testing default interface methods that cannot be devirtualized..."); + + if (GetFoo().GetNumber() != 42) + throw new Exception(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static IFoo GetFoo() => new Foo(); + + if (GetBar().GetNumber() != 43) + throw new Exception(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static IFoo GetBar() => new Bar(); + + if (GetBaz().GetNumber() != 100) + throw new Exception(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static IFoo GetBaz() => new Baz(); + + if (GetDerived().GetNumber() != 100) + throw new Exception(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static IFoo GetDerived() => new Derived(); + + if (GetFooObject().GetInterfaceType() != typeof(IFoo)) + throw new Exception(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static IFoo GetFooObject() => new Foo(); + + if (GetFooInt().GetInterfaceType() != typeof(IFoo)) + throw new Exception(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static IFoo GetFooInt() => new Foo(); + } + } + class TestDefaultInterfaceVariance { class Foo : IVariant, IVariant