Skip to content

Commit 89bba37

Browse files
Place vtable slots deemed final by analysis into sealed vtable (#97951)
Virtual method table slots typically go into vtable - a variable-length part of the `MethodTable` data structure. Virtual method table slots are always inherited by `MethodTable` of the derived class. This means that vtable slots that are introduced in a class that has many derived classes can be quite expensive. One example are arrays - array types inherit all virtual methods from `System.Array` and there's many arrays. We therefore have an optimization - if a virtual method is newly introduced, but it's sealed at the same time, we place it into a separate data structure - a "sealed vtable". Sealed vtable slots do not get inherited by derived classes. Because they don't get inherited, we also need to make sure all non-interface calls into this virtual method get devirtualized (virtual dispatch needs to be aware of the special lookup that happens for sealed slots and only interface resolution is aware of how to do that). This PR makes this optimization kick in more often - not just when method is `virtual sealed` in metadata (which effectively only happens for non-virtual method in C# implementing an interface), but also when it's virtual and nothing else overrides it in the program.
1 parent 4a4760b commit 89bba37

15 files changed

Lines changed: 68 additions & 48 deletions

File tree

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

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using ILCompiler.DependencyAnalysis;
5+
46
using Internal.TypeSystem;
57
using Internal.TypeSystem.Ecma;
68

9+
using Debug = System.Diagnostics.Debug;
10+
711
namespace ILCompiler
812
{
913
public static class MethodExtensions
@@ -73,6 +77,7 @@ public static string GetUnmanagedCallersOnlyExportName(this EcmaMethod This)
7377
return null;
7478
}
7579

80+
#if !READYTORUN
7681
/// <summary>
7782
/// Determine whether a method can go into the sealed vtable of a type. Such method must be a sealed virtual
7883
/// method that is not overriding any method on a base type.
@@ -82,25 +87,43 @@ public static string GetUnmanagedCallersOnlyExportName(this EcmaMethod This)
8287
/// since all of their collection interface methods are sealed and implemented on the System.Array and
8388
/// System.Array&lt;T&gt; base types, and therefore we can minimize the vtable sizes of all derived array types.
8489
/// </summary>
85-
public static bool CanMethodBeInSealedVTable(this MethodDesc method)
90+
public static bool CanMethodBeInSealedVTable(this MethodDesc method, NodeFactory factory)
8691
{
87-
bool isInterfaceMethod = method.OwningType.IsInterface;
92+
Debug.Assert(!method.OwningType.ContainsSignatureVariables(treatGenericParameterLikeSignatureVariable: true));
93+
94+
TypeDesc owningType = method.OwningType;
8895

89-
// Methods on interfaces never go into sealed vtable
90-
// We would hit this code path for default implementations of interface methods (they are newslot+final).
91-
// Interface types don't get physical slots, but they have logical slot numbers and that logic shouldn't
92-
// attempt to place final+newslot methods differently.
93-
if (method.IsFinal && method.IsNewSlot && !isInterfaceMethod)
96+
// Interface types don't have physical slots so we never optimize to sealed slots
97+
if (owningType.IsInterface)
98+
return false;
99+
100+
// Implementations of static virtual methods go into the sealed vtable.
101+
if (method.Signature.IsStatic)
94102
return true;
95103

96-
// Implementations of static virtual method also go into the sealed vtable.
97-
// Again, we don't let that happen for interface methods because the slot numbers are only logical,
98-
// not physical.
99-
if (method.Signature.IsStatic && !isInterfaceMethod)
104+
// If the owning type is already considered sealed, there's little benefit in placing the slots
105+
// in the sealed vtable: the sealed vtable has these properties:
106+
//
107+
// 1. We don't need to repeat them in derived classes.
108+
// 2. The slots use 4-byte relative pointers, so they can be smaller.
109+
// 3. The sealed vtable is shared among canonically-equivalent types.
110+
//
111+
// Benefit 1 doesn't apply to sealed types by definition. Benefit 2 doesn't manifest itself
112+
// when data dehydration is enabled (which is the default) since pointers are compressed either way.
113+
// Benefit 3 is still real, so we condition this opt out on type not having a canonical form.
114+
if (factory.DevirtualizationManager.IsEffectivelySealed(owningType)
115+
&& !owningType.ConvertToCanonForm(CanonicalFormKind.Specific).IsCanonicalSubtype(CanonicalFormKind.Any))
116+
{
117+
return false;
118+
}
119+
120+
// Newslot final methods go into the sealed vtable.
121+
if (method.IsNewSlot && factory.DevirtualizationManager.IsEffectivelySealed(method))
100122
return true;
101123

102124
return false;
103125
}
126+
#endif
104127

105128
public static bool NotCallableWithoutOwningEEType(this MethodDesc method)
106129
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public DelegateCreationInfo GetDelegateCtor(TypeDesc delegateType, MethodDesc ta
113113
// If we're creating a delegate to a virtual method that cannot be overridden, devirtualize.
114114
// This is not just an optimization - it's required for correctness in the presence of sealed
115115
// vtable slots.
116-
if (followVirtualDispatch && (target.IsFinal || target.OwningType.IsSealed()))
116+
if (followVirtualDispatch && NodeFactory.DevirtualizationManager.IsEffectivelySealed(target))
117117
followVirtualDispatch = false;
118118

119119
if (followVirtualDispatch)

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/EETypeNode.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
407407
// We don't do this if the method can be placed in the sealed vtable since
408408
// those can never be overriden by children anyway.
409409
bool canUseTentativeMethod = isNonInterfaceAbstractType
410-
&& !decl.CanMethodBeInSealedVTable()
410+
&& !decl.CanMethodBeInSealedVTable(factory)
411411
&& factory.CompilationModuleGroup.AllowVirtualMethodOnAbstractTypeOptimization(canonImpl);
412412
IMethodNode implNode = canUseTentativeMethod ?
413413
factory.TentativeMethodEntrypoint(canonImpl, impl.OwningType.IsValueType) :
@@ -1039,7 +1039,7 @@ private void OutputVirtualSlots(NodeFactory factory, ref ObjectDataBuilder objDa
10391039

10401040
// Final NewSlot methods cannot be overridden, and therefore can be placed in the sealed-vtable to reduce the size of the vtable
10411041
// of this type and any type that inherits from it.
1042-
if (declMethod.CanMethodBeInSealedVTable() && !declType.IsArrayTypeWithoutGenericInterfaces())
1042+
if (declMethod.CanMethodBeInSealedVTable(factory) && !declType.IsArrayTypeWithoutGenericInterfaces())
10431043
continue;
10441044

10451045
bool shouldEmitImpl = !implMethod.IsAbstract;

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NativeLayoutVertexNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1433,7 +1433,7 @@ private static void ProcessVTableEntriesForCallingConventionSignatureGeneration(
14331433

14341434
MethodDesc implMethod = closestDefType.FindVirtualFunctionTargetMethodOnObjectType(declMethod);
14351435

1436-
if (implMethod.CanMethodBeInSealedVTable() && !implType.IsArrayTypeWithoutGenericInterfaces())
1436+
if (implMethod.CanMethodBeInSealedVTable(factory) && !implType.IsArrayTypeWithoutGenericInterfaces())
14371437
{
14381438
// Sealed vtable entries on other types in the hierarchy should not be reported (types read entries
14391439
// from their own sealed vtables, and not from the sealed vtables of base types).

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReflectedMethodNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
6868
}
6969
else
7070
{
71-
if (ReflectionVirtualInvokeMapNode.NeedsVirtualInvokeInfo(slotDefiningMethod) && !factory.VTable(slotDefiningMethod.OwningType).HasFixedSlots)
71+
if (ReflectionVirtualInvokeMapNode.NeedsVirtualInvokeInfo(factory, slotDefiningMethod) && !factory.VTable(slotDefiningMethod.OwningType).HasFixedSlots)
7272
dependencies.Add(factory.VirtualMethodUse(slotDefiningMethod), "Virtually callable reflectable method");
7373
}
7474
}

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReflectionInvokeMapNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
156156
if (method.IsDefaultConstructor)
157157
flags |= InvokeTableFlags.IsDefaultConstructor;
158158

159-
if (ReflectionVirtualInvokeMapNode.NeedsVirtualInvokeInfo(method))
159+
if (ReflectionVirtualInvokeMapNode.NeedsVirtualInvokeInfo(factory, method))
160160
flags |= InvokeTableFlags.HasVirtualInvoke;
161161

162162
if (!method.IsAbstract)

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ReflectionVirtualInvokeMapNode.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,12 @@ public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
3939
public override bool StaticDependenciesAreComputed => true;
4040
protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler);
4141

42-
public static bool NeedsVirtualInvokeInfo(MethodDesc method)
42+
public static bool NeedsVirtualInvokeInfo(NodeFactory factory, MethodDesc method)
4343
{
4444
if (!method.IsVirtual)
4545
return false;
4646

47-
if (method.IsFinal)
48-
return false;
49-
50-
if (method.OwningType.IsSealed())
47+
if (factory.DevirtualizationManager.IsEffectivelySealed(method))
5148
return false;
5249

5350
return true;
@@ -83,7 +80,7 @@ public static MethodDesc GetDeclaringVirtualMethodAndHierarchyDistance(MethodDes
8380

8481
public static void GetVirtualInvokeMapDependencies(ref DependencyList dependencies, NodeFactory factory, MethodDesc method)
8582
{
86-
if (NeedsVirtualInvokeInfo(method))
83+
if (NeedsVirtualInvokeInfo(factory, method))
8784
{
8885
dependencies ??= new DependencyList();
8986

@@ -137,7 +134,7 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
137134
continue;
138135

139136
// Only virtual methods are interesting
140-
if (!NeedsVirtualInvokeInfo(method))
137+
if (!NeedsVirtualInvokeInfo(factory, method))
141138
continue;
142139

143140
//

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/SealedVTableNode.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public bool BuildSealedVTableSlots(NodeFactory factory, bool relocsOnly)
125125

126126
MethodDesc implMethod = declType.FindVirtualFunctionTargetMethodOnObjectType(virtualSlots[i]);
127127

128-
if (implMethod.CanMethodBeInSealedVTable())
128+
if (implMethod.CanMethodBeInSealedVTable(factory))
129129
_sealedVTableEntries.Add(SealedVTableEntry.FromVirtualMethod(implMethod));
130130
}
131131

@@ -162,8 +162,7 @@ public bool BuildSealedVTableSlots(NodeFactory factory, bool relocsOnly)
162162
// dispatch will walk the inheritance chain).
163163
if (implMethod != null)
164164
{
165-
if (implMethod.Signature.IsStatic ||
166-
(implMethod.CanMethodBeInSealedVTable() && !implMethod.OwningType.HasSameTypeDefinition(declType)))
165+
if (implMethod.Signature.IsStatic || !implMethod.OwningType.HasSameTypeDefinition(declType))
167166
{
168167
TypeDesc implType = declType;
169168
while (!implType.HasSameTypeDefinition(implMethod.OwningType))
@@ -173,7 +172,8 @@ public bool BuildSealedVTableSlots(NodeFactory factory, bool relocsOnly)
173172
if (!implType.IsTypeDefinition)
174173
targetMethod = factory.TypeSystemContext.GetMethodForInstantiatedType(implMethod.GetTypicalMethodDefinition(), (InstantiatedType)implType);
175174

176-
_sealedVTableEntries.Add(SealedVTableEntry.FromVirtualMethod(targetMethod));
175+
if (targetMethod.CanMethodBeInSealedVTable(factory) || implMethod.Signature.IsStatic)
176+
_sealedVTableEntries.Add(SealedVTableEntry.FromVirtualMethod(targetMethod));
177177
}
178178
}
179179
else

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/Target_ARM/ARMReadyToRunHelperNode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected override void EmitCode(NodeFactory factory, ref ARMEmitter encoder, bo
2222
MethodDesc targetMethod = (MethodDesc)Target;
2323

2424
Debug.Assert(!targetMethod.OwningType.IsInterface);
25-
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable());
25+
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable(factory));
2626

2727
int pointerSize = factory.Target.PointerSize;
2828

@@ -126,7 +126,7 @@ protected override void EmitCode(NodeFactory factory, ref ARMEmitter encoder, bo
126126

127127
if (target.TargetNeedsVTableLookup)
128128
{
129-
Debug.Assert(!target.TargetMethod.CanMethodBeInSealedVTable());
129+
Debug.Assert(!target.TargetMethod.CanMethodBeInSealedVTable(factory));
130130

131131
encoder.EmitLDR(encoder.TargetRegister.Arg2, encoder.TargetRegister.Arg1);
132132

@@ -175,7 +175,7 @@ protected override void EmitCode(NodeFactory factory, ref ARMEmitter encoder, bo
175175

176176
encoder.EmitLDR(encoder.TargetRegister.Result, encoder.TargetRegister.Arg0);
177177

178-
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable());
178+
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable(factory));
179179

180180
int slot = VirtualMethodSlotHelper.GetVirtualMethodSlot(factory, targetMethod, targetMethod.OwningType);
181181
Debug.Assert(slot != -1);

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/Target_ARM64/ARM64ReadyToRunHelperNode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ protected override void EmitCode(NodeFactory factory, ref ARM64Emitter encoder,
2323
MethodDesc targetMethod = (MethodDesc)Target;
2424

2525
Debug.Assert(!targetMethod.OwningType.IsInterface);
26-
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable());
26+
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable(factory));
2727

2828
int pointerSize = factory.Target.PointerSize;
2929

@@ -161,7 +161,7 @@ protected override void EmitCode(NodeFactory factory, ref ARM64Emitter encoder,
161161

162162
if (target.TargetNeedsVTableLookup)
163163
{
164-
Debug.Assert(!target.TargetMethod.CanMethodBeInSealedVTable());
164+
Debug.Assert(!target.TargetMethod.CanMethodBeInSealedVTable(factory));
165165

166166
encoder.EmitLDR(encoder.TargetRegister.Arg2, encoder.TargetRegister.Arg1);
167167

@@ -210,7 +210,7 @@ protected override void EmitCode(NodeFactory factory, ref ARM64Emitter encoder,
210210

211211
encoder.EmitLDR(encoder.TargetRegister.Result, encoder.TargetRegister.Arg0);
212212

213-
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable());
213+
Debug.Assert(!targetMethod.CanMethodBeInSealedVTable(factory));
214214

215215
int slot = VirtualMethodSlotHelper.GetVirtualMethodSlot(factory, targetMethod, targetMethod.OwningType);
216216
Debug.Assert(slot != -1);

0 commit comments

Comments
 (0)