Skip to content

Commit b4c820c

Browse files
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 #108153 (comment)
1 parent c60288e commit b4c820c

File tree

4 files changed

+201
-16
lines changed

4 files changed

+201
-16
lines changed

src/coreclr/tools/Common/Compiler/MethodExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public static bool NotCallableWithoutOwningEEType(this MethodDesc method)
130130
TypeDesc owningType = method.OwningType;
131131
return !method.Signature.IsStatic && /* Static methods don't have this */
132132
!owningType.IsValueType && /* Value type instance methods take a ref to data */
133+
!owningType.IsInterface && /* Interface MethodTable can be optimized away but the instance method can still be callable (`this` is of a non-interface type) */
133134
!owningType.IsArrayTypeWithoutGenericInterfaces() && /* Type loader can make these at runtime */
134135
(owningType is not MetadataType mdType || !mdType.IsModuleType) && /* Compiler parks some instance methods on the <Module> type */
135136
!method.IsSharedByGenericInstantiations; /* Current impl limitation; can be lifted */

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -541,16 +541,27 @@ public ScannedDevirtualizationManager(NodeFactory factory, ImmutableArray<Depend
541541
}
542542

543543
TypeDesc canonType = type.ConvertToCanonForm(CanonicalFormKind.Specific);
544+
TypeDesc baseType;
544545

545-
if (!canonType.IsDefType || !((MetadataType)canonType).IsAbstract)
546+
if (canonType is not MetadataType { IsAbstract: true })
547+
{
546548
_canonConstructedTypes.Add(canonType.GetClosestDefType());
549+
baseType = canonType.BaseType;
550+
while (baseType != null)
551+
{
552+
baseType = baseType.ConvertToCanonForm(CanonicalFormKind.Specific);
553+
if (!_canonConstructedTypes.Add(baseType))
554+
break;
555+
baseType = baseType.BaseType;
556+
}
557+
}
547558

548-
TypeDesc baseType = canonType.BaseType;
549-
bool added = true;
550-
while (baseType != null && added)
559+
baseType = canonType.BaseType;
560+
while (baseType != null)
551561
{
552562
baseType = baseType.ConvertToCanonForm(CanonicalFormKind.Specific);
553-
added = _unsealedTypes.Add(baseType);
563+
if (!_unsealedTypes.Add(baseType))
564+
break;
554565
baseType = baseType.BaseType;
555566
}
556567

@@ -686,20 +697,16 @@ public override bool IsEffectivelySealed(MethodDesc method)
686697

687698
protected override MethodDesc ResolveVirtualMethod(MethodDesc declMethod, DefType implType, out CORINFO_DEVIRTUALIZATION_DETAIL devirtualizationDetail)
688699
{
689-
MethodDesc result = base.ResolveVirtualMethod(declMethod, implType, out devirtualizationDetail);
690-
if (result != null)
700+
// If we would resolve into a type that wasn't seen as allocated, don't allow devirtualization.
701+
// It would go past what we scanned in the scanner and that doesn't lead to good things.
702+
if (!_canonConstructedTypes.Contains(implType.ConvertToCanonForm(CanonicalFormKind.Specific)))
691703
{
692-
// If we would resolve into a type that wasn't seen as allocated, don't allow devirtualization.
693-
// It would go past what we scanned in the scanner and that doesn't lead to good things.
694-
if (!_canonConstructedTypes.Contains(result.OwningType.ConvertToCanonForm(CanonicalFormKind.Specific)))
695-
{
696-
// FAILED_BUBBLE_IMPL_NOT_REFERENCEABLE is close enough...
697-
devirtualizationDetail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_BUBBLE_IMPL_NOT_REFERENCEABLE;
698-
return null;
699-
}
704+
// FAILED_BUBBLE_IMPL_NOT_REFERENCEABLE is close enough...
705+
devirtualizationDetail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_BUBBLE_IMPL_NOT_REFERENCEABLE;
706+
return null;
700707
}
701708

702-
return result;
709+
return base.ResolveVirtualMethod(declMethod, implType, out devirtualizationDetail);
703710
}
704711

705712
public override bool CanReferenceConstructedMethodTable(TypeDesc type)

src/tests/nativeaot/SmokeTests/UnitTests/Devirtualization.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Devirtualization
1111
{
1212
internal static int Run()
1313
{
14+
TestDevirtualizationIntoAbstract.Run();
1415
RegressionBug73076.Run();
1516
RegressionGenericHierarchy.Run();
1617
DevirtualizationCornerCaseTests.Run();
@@ -19,6 +20,39 @@ internal static int Run()
1920
return 100;
2021
}
2122

23+
class TestDevirtualizationIntoAbstract
24+
{
25+
class Something { }
26+
27+
abstract class Base
28+
{
29+
[MethodImpl(MethodImplOptions.NoInlining)]
30+
public virtual Type GetSomething() => typeof(Something);
31+
}
32+
33+
sealed class Derived : Base { }
34+
35+
class Unrelated : Base
36+
{
37+
public override Type GetSomething() => typeof(Unrelated);
38+
}
39+
40+
public static void Run()
41+
{
42+
TestUnrelated(new Unrelated());
43+
44+
// We were getting a scanning failure because GetSomething got devirtualized into
45+
// Base.GetSomething, but that's unreachable.
46+
Test(null);
47+
48+
[MethodImpl(MethodImplOptions.NoInlining)]
49+
static Type Test(Derived d) => d?.GetSomething();
50+
51+
[MethodImpl(MethodImplOptions.NoInlining)]
52+
static Type TestUnrelated(Base d) => d?.GetSomething();
53+
}
54+
}
55+
2256
class RegressionBug73076
2357
{
2458
interface IFactory

src/tests/nativeaot/SmokeTests/UnitTests/Interfaces.cs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public static int Run()
3838

3939
TestPublicAndNonpublicDifference.Run();
4040
TestDefaultInterfaceMethods.Run();
41+
TestDefaultInterfaceMethodsDevirtNoInline.Run();
42+
TestDefaultInterfaceMethodsNoDevirt.Run();
4143
TestDefaultInterfaceVariance.Run();
4244
TestVariantInterfaceOptimizations.Run();
4345
TestSharedInterfaceMethods.Run();
@@ -581,6 +583,147 @@ public static void Run()
581583
}
582584
}
583585

586+
class TestDefaultInterfaceMethodsDevirtNoInline
587+
{
588+
interface IFoo
589+
{
590+
[MethodImpl(MethodImplOptions.NoInlining)]
591+
int GetNumber() => 42;
592+
}
593+
594+
interface IBar : IFoo
595+
{
596+
[MethodImpl(MethodImplOptions.NoInlining)]
597+
int IFoo.GetNumber() => 43;
598+
}
599+
600+
class Foo : IFoo { }
601+
class Bar : IBar { }
602+
603+
class Baz : IFoo
604+
{
605+
[MethodImpl(MethodImplOptions.NoInlining)]
606+
public int GetNumber() => 100;
607+
}
608+
609+
interface IFoo<T>
610+
{
611+
[MethodImpl(MethodImplOptions.NoInlining)]
612+
Type GetInterfaceType() => typeof(IFoo<T>);
613+
}
614+
615+
class Foo<T> : IFoo<T> { }
616+
617+
class Base : IFoo
618+
{
619+
[MethodImpl(MethodImplOptions.NoInlining)]
620+
int IFoo.GetNumber() => 100;
621+
}
622+
623+
class Derived : Base, IBar { }
624+
625+
public static void Run()
626+
{
627+
Console.WriteLine("Testing default interface methods that can be devirtualized but not inlined...");
628+
629+
typeof(IFoo).ToString();
630+
631+
if (((IFoo)new Foo()).GetNumber() != 42)
632+
throw new Exception();
633+
634+
if (((IFoo)new Bar()).GetNumber() != 43)
635+
throw new Exception();
636+
637+
if (((IFoo)new Baz()).GetNumber() != 100)
638+
throw new Exception();
639+
640+
if (((IFoo)new Derived()).GetNumber() != 100)
641+
throw new Exception();
642+
643+
if (((IFoo<object>)new Foo<object>()).GetInterfaceType() != typeof(IFoo<object>))
644+
throw new Exception();
645+
646+
if (((IFoo<int>)new Foo<int>()).GetInterfaceType() != typeof(IFoo<int>))
647+
throw new Exception();
648+
}
649+
}
650+
651+
class TestDefaultInterfaceMethodsNoDevirt
652+
{
653+
interface IFoo
654+
{
655+
int GetNumber() => 42;
656+
}
657+
658+
interface IBar : IFoo
659+
{
660+
int IFoo.GetNumber() => 43;
661+
}
662+
663+
class Foo : IFoo { }
664+
class Bar : IBar { }
665+
666+
class Baz : IFoo
667+
{
668+
public int GetNumber() => 100;
669+
}
670+
671+
interface IFoo<T>
672+
{
673+
Type GetInterfaceType() => typeof(IFoo<T>);
674+
}
675+
676+
class Foo<T> : IFoo<T> { }
677+
678+
class Base : IFoo
679+
{
680+
int IFoo.GetNumber() => 100;
681+
}
682+
683+
class Derived : Base, IBar { }
684+
685+
public static void Run()
686+
{
687+
Console.WriteLine("Testing default interface methods that cannot be devirtualized...");
688+
689+
if (GetFoo().GetNumber() != 42)
690+
throw new Exception();
691+
692+
[MethodImpl(MethodImplOptions.NoInlining)]
693+
static IFoo GetFoo() => new Foo();
694+
695+
if (GetBar().GetNumber() != 43)
696+
throw new Exception();
697+
698+
[MethodImpl(MethodImplOptions.NoInlining)]
699+
static IFoo GetBar() => new Bar();
700+
701+
if (GetBaz().GetNumber() != 100)
702+
throw new Exception();
703+
704+
[MethodImpl(MethodImplOptions.NoInlining)]
705+
static IFoo GetBaz() => new Baz();
706+
707+
if (GetDerived().GetNumber() != 100)
708+
throw new Exception();
709+
710+
[MethodImpl(MethodImplOptions.NoInlining)]
711+
static IFoo GetDerived() => new Derived();
712+
713+
if (GetFooObject().GetInterfaceType() != typeof(IFoo<object>))
714+
throw new Exception();
715+
716+
[MethodImpl(MethodImplOptions.NoInlining)]
717+
static IFoo<object> GetFooObject() => new Foo<object>();
718+
719+
if (GetFooInt().GetInterfaceType() != typeof(IFoo<int>))
720+
throw new Exception();
721+
722+
[MethodImpl(MethodImplOptions.NoInlining)]
723+
static IFoo<int> GetFooInt() => new Foo<int>();
724+
}
725+
}
726+
584727
class TestDefaultInterfaceVariance
585728
{
586729
class Foo : IVariant<string>, IVariant<object>

0 commit comments

Comments
 (0)