Skip to content

Commit 2022100

Browse files
MichalStrehovskypull[bot]
authored andcommitted
Eliminate unused virtual methods we detect after optimizations (#100286)
Before this PR, it was not possible to get rid of unused virtual method slots once whole program analysis determined they are needed. This PR makes it (sort of) possible. Codegen needs to know what numerical slots get assigned to virtual methods because we use the slots to index into vtable to do virtual calls/generic lookup. We also want to be able to skip generating unused virtual methods. Before we do actual codegen optimizations, we don't know what virtual method uses can actually be optimized away. ### Before this PR Without optimizations: don’t do codegen that needs slots inlined into code generated by RyuJIT. Instead instruct RyuJIT to call a helper. The helper is emitted as assembly by the compiler driver itself at the time all used virtual methods are known and we can stabilize slot numbers. When assigning slots, we skip the virtual methods that are unused. With optimizations: assign slots after whole program analysis is done and we know what slots can be skipped. Codegen might still do optimizations that avoids actually needing a slot. We still generate the slot and fill it out and it’s a bit of a waste. ### After this PR Without optimizations: assign slots based on what we see in metadata. Do codegen based on this information and allow inlining slot indices into codegen. Still try to find unused slots. Generate unused slots as null slots (place a literal null pointer in the vtable). This is a both an improvement and a regression. It’s debug so it doesn’t matter much. We can delete the handwritten assembly, so it’s an improvement there. With optimizations: assign slots after whole program analysis. Still try to find unused slots. If an unused slot is found, generate it as null. We can’t eliminate it anymore for regular vtable slots (because we did codegen that assumes indices into vtable), but at least we don’t have to generate a standalone method body and a reloc to it. For sealed vtable slots, we can actually eliminate the slot (the slot is not inlined in code) and this PR does that.
1 parent dfd85cc commit 2022100

23 files changed

+161
-335
lines changed

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

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ public MethodDesc ExpandIntrinsicForCallsite(MethodDesc intrinsicMethod, MethodD
210210
return intrinsicMethod;
211211
}
212212

213-
public bool HasFixedSlotVTable(TypeDesc type)
213+
public bool NeedsSlotUseTracking(TypeDesc type)
214214
{
215-
return NodeFactory.VTable(type).HasFixedSlots;
215+
return !NodeFactory.VTable(type).HasKnownVirtualMethodUse;
216216
}
217217

218218
public bool IsEffectivelySealed(TypeDesc type)
@@ -388,43 +388,37 @@ public GenericDictionaryLookup ComputeGenericLookup(MethodDesc contextMethod, Re
388388
lookupKind = ReadyToRunHelperId.TypeHandle;
389389
}
390390

391-
// Can we do a fixed lookup? Start by checking if we can get to the dictionary.
392-
// Context source having a vtable with fixed slots is a prerequisite.
393-
if (contextSource == GenericContextSource.MethodParameter
394-
|| HasFixedSlotVTable(contextMethod.OwningType))
391+
DictionaryLayoutNode dictionaryLayout;
392+
if (contextSource == GenericContextSource.MethodParameter)
393+
dictionaryLayout = _nodeFactory.GenericDictionaryLayout(contextMethod);
394+
else
395+
dictionaryLayout = _nodeFactory.GenericDictionaryLayout(contextMethod.OwningType);
396+
397+
// If the dictionary layout has fixed slots, we can compute the lookup now. Otherwise defer to helper.
398+
if (dictionaryLayout.HasFixedSlots)
395399
{
396-
DictionaryLayoutNode dictionaryLayout;
397-
if (contextSource == GenericContextSource.MethodParameter)
398-
dictionaryLayout = _nodeFactory.GenericDictionaryLayout(contextMethod);
399-
else
400-
dictionaryLayout = _nodeFactory.GenericDictionaryLayout(contextMethod.OwningType);
400+
int pointerSize = _nodeFactory.Target.PointerSize;
401401

402-
// If the dictionary layout has fixed slots, we can compute the lookup now. Otherwise defer to helper.
403-
if (dictionaryLayout.HasFixedSlots)
402+
GenericLookupResult lookup = ReadyToRunGenericHelperNode.GetLookupSignature(_nodeFactory, lookupKind, targetOfLookup);
403+
if (dictionaryLayout.TryGetSlotForEntry(lookup, out int dictionarySlot))
404404
{
405-
int pointerSize = _nodeFactory.Target.PointerSize;
405+
int dictionaryOffset = dictionarySlot * pointerSize;
406406

407-
GenericLookupResult lookup = ReadyToRunGenericHelperNode.GetLookupSignature(_nodeFactory, lookupKind, targetOfLookup);
408-
if (dictionaryLayout.TryGetSlotForEntry(lookup, out int dictionarySlot))
407+
if (contextSource == GenericContextSource.MethodParameter)
409408
{
410-
int dictionaryOffset = dictionarySlot * pointerSize;
411-
412-
if (contextSource == GenericContextSource.MethodParameter)
413-
{
414-
return GenericDictionaryLookup.CreateFixedLookup(contextSource, dictionaryOffset);
415-
}
416-
else
417-
{
418-
int vtableSlot = VirtualMethodSlotHelper.GetGenericDictionarySlot(_nodeFactory, contextMethod.OwningType);
419-
int vtableOffset = EETypeNode.GetVTableOffset(pointerSize) + vtableSlot * pointerSize;
420-
return GenericDictionaryLookup.CreateFixedLookup(contextSource, vtableOffset, dictionaryOffset);
421-
}
409+
return GenericDictionaryLookup.CreateFixedLookup(contextSource, dictionaryOffset);
422410
}
423411
else
424412
{
425-
return GenericDictionaryLookup.CreateNullLookup(contextSource);
413+
int vtableSlot = VirtualMethodSlotHelper.GetGenericDictionarySlot(_nodeFactory, contextMethod.OwningType);
414+
int vtableOffset = EETypeNode.GetVTableOffset(pointerSize) + vtableSlot * pointerSize;
415+
return GenericDictionaryLookup.CreateFixedLookup(contextSource, vtableOffset, dictionaryOffset);
426416
}
427417
}
418+
else
419+
{
420+
return GenericDictionaryLookup.CreateNullLookup(contextSource);
421+
}
428422
}
429423

430424
// Fixed lookup not possible - use helper.

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
386386
DefType defType = _type.GetClosestDefType();
387387

388388
// If we're producing a full vtable, none of the dependencies are conditional.
389-
if (!factory.VTable(defType).HasFixedSlots)
389+
if (!factory.VTable(defType).HasKnownVirtualMethodUse)
390390
{
391391
bool isNonInterfaceAbstractType = !defType.IsInterface && ((MetadataType)defType).IsAbstract;
392392

@@ -1035,7 +1035,7 @@ private void OutputVirtualSlots(NodeFactory factory, ref ObjectDataBuilder objDa
10351035

10361036
// It's only okay to touch the actual list of slots if we're in the final emission phase
10371037
// or the vtable is not built lazily.
1038-
if (relocsOnly && !declVTable.HasFixedSlots)
1038+
if (relocsOnly && !declVTable.HasKnownVirtualMethodUse)
10391039
return;
10401040

10411041
// Interface types don't place anything else in their physical vtable.
@@ -1063,13 +1063,19 @@ private void OutputVirtualSlots(NodeFactory factory, ref ObjectDataBuilder objDa
10631063
// No generic virtual methods can appear in the vtable!
10641064
Debug.Assert(!declMethod.HasInstantiation);
10651065

1066-
MethodDesc implMethod = implType.GetClosestDefType().FindVirtualFunctionTargetMethodOnObjectType(declMethod);
1067-
10681066
// Final NewSlot methods cannot be overridden, and therefore can be placed in the sealed-vtable to reduce the size of the vtable
10691067
// of this type and any type that inherits from it.
10701068
if (declMethod.CanMethodBeInSealedVTable(factory) && !declType.IsArrayTypeWithoutGenericInterfaces())
10711069
continue;
10721070

1071+
if (!declVTable.IsSlotUsed(declMethod))
1072+
{
1073+
objData.EmitZeroPointer();
1074+
continue;
1075+
}
1076+
1077+
MethodDesc implMethod = implType.GetClosestDefType().FindVirtualFunctionTargetMethodOnObjectType(declMethod);
1078+
10731079
bool shouldEmitImpl = !implMethod.IsAbstract;
10741080

10751081
// We do a size optimization that removes support for built-in ValueType Equals/GetHashCode

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ public override IEnumerable<DependencyNodeCore<NodeFactory>> NonRelocDependencie
837837

838838
// If we're producing a full vtable for the type, we don't need to report virtual method use.
839839
// We also don't report virtual method use for generic virtual methods - tracking those is orthogonal.
840-
if (!factory.VTable(canonMethod.OwningType).HasFixedSlots && !canonMethod.HasInstantiation)
840+
if (!factory.VTable(canonMethod.OwningType).HasKnownVirtualMethodUse && !canonMethod.HasInstantiation)
841841
{
842842
// Report the method as virtually used so that types that could be used here at runtime
843843
// have the appropriate implementations generated.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
5454
{
5555
DependencyList result = new DependencyList();
5656

57-
if (!factory.VTable(_targetMethod.OwningType).HasFixedSlots)
57+
if (!factory.VTable(_targetMethod.OwningType).HasKnownVirtualMethodUse)
5858
{
5959
result.Add(factory.VirtualMethodUse(_targetMethod), "Interface method use");
6060
}

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

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,8 @@ public static bool MightHaveInterfaceDispatchMap(TypeDesc type, NodeFactory fact
9696
null :
9797
(InstantiatedType)declType.GetTypeDefinition().RuntimeInterfaces[interfaceIndex];
9898

99-
IEnumerable<MethodDesc> slots;
100-
101-
// If the vtable has fixed slots, we can query it directly.
102-
// If it's a lazily built vtable, we might not be able to query slots
103-
// just yet, so approximate by looking at all methods.
10499
VTableSliceNode vtableSlice = factory.VTable(interfaceType);
105-
if (vtableSlice.HasFixedSlots)
106-
slots = vtableSlice.Slots;
107-
else
108-
slots = interfaceType.GetAllVirtualMethods();
109-
110-
foreach (MethodDesc slotMethod in slots)
100+
foreach (MethodDesc slotMethod in vtableSlice.Slots)
111101
{
112102
MethodDesc declMethod = slotMethod;
113103

@@ -176,12 +166,16 @@ private void EmitDispatchMap(ref ObjectDataBuilder builder, NodeFactory factory)
176166
if (!factory.InterfaceUse(interfaceType.GetTypeDefinition()).Marked)
177167
continue;
178168

179-
IReadOnlyList<MethodDesc> virtualSlots = factory.VTable(interfaceType).Slots;
169+
VTableSliceNode interfaceVTable = factory.VTable(interfaceType);
170+
IReadOnlyList<MethodDesc> virtualSlots = interfaceVTable.Slots;
180171

181172
for (int interfaceMethodSlot = 0; interfaceMethodSlot < virtualSlots.Count; interfaceMethodSlot++)
182173
{
183174
MethodDesc declMethod = virtualSlots[interfaceMethodSlot];
184175

176+
if (!interfaceVTable.IsSlotUsed(declMethod))
177+
continue;
178+
185179
if (!declMethod.Signature.IsStatic && !needsEntriesForInstanceInterfaceMethodImpls)
186180
continue;
187181

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,7 +1405,7 @@ private static void ProcessVTableEntriesForCallingConventionSignatureGeneration(
14051405
break;
14061406

14071407
case VTableEntriesToProcess.AllOnTypesThatShouldProduceFullVTables:
1408-
if (factory.VTable(declType).HasFixedSlots)
1408+
if (factory.VTable(declType).HasKnownVirtualMethodUse)
14091409
{
14101410
vtableEntriesToProcess = factory.VTable(declType).Slots;
14111411
}
@@ -1416,7 +1416,7 @@ private static void ProcessVTableEntriesForCallingConventionSignatureGeneration(
14161416
break;
14171417

14181418
case VTableEntriesToProcess.AllOnTypesThatProducePartialVTables:
1419-
if (factory.VTable(declType).HasFixedSlots)
1419+
if (factory.VTable(declType).HasKnownVirtualMethodUse)
14201420
{
14211421
vtableEntriesToProcess = Array.Empty<MethodDesc>();
14221422
}
@@ -1639,7 +1639,7 @@ public sealed override IEnumerable<DependencyListEntry> GetStaticDependencies(No
16391639
if (method.IsRuntimeDeterminedExactMethod)
16401640
method = method.GetCanonMethodTarget(CanonicalFormKind.Specific);
16411641

1642-
if (!factory.VTable(method.OwningType).HasFixedSlots)
1642+
if (!factory.VTable(method.OwningType).HasKnownVirtualMethodUse)
16431643
{
16441644
yield return new DependencyListEntry(factory.VirtualMethodUse(method), "Slot number");
16451645
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ private void CreateNodeCaches()
361361
{
362362
// We don't need to track virtual method uses for types that have a vtable with a known layout.
363363
// It's a waste of CPU time and memory.
364-
Debug.Assert(!VTable(method.OwningType).HasFixedSlots);
364+
Debug.Assert(method.OwningType.IsGenericDefinition || !VTable(method.OwningType).HasKnownVirtualMethodUse);
365365

366366
return new VariantInterfaceMethodUseNode(method);
367367
});
@@ -1159,7 +1159,7 @@ protected override VirtualMethodUseNode CreateValueFromKey(MethodDesc key)
11591159
{
11601160
// We don't need to track virtual method uses for types that have a vtable with a known layout.
11611161
// It's a waste of CPU time and memory.
1162-
Debug.Assert(!_factory.VTable(key.OwningType).HasFixedSlots);
1162+
Debug.Assert(!_factory.VTable(key.OwningType).HasKnownVirtualMethodUse);
11631163
return new VirtualMethodUseNode(key);
11641164
}
11651165
protected override int GetKeyHashCode(MethodDesc key) => key.GetHashCode();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public IEnumerable<DependencyListEntry> InstantiateDependencies(NodeFactory fact
154154
if (createInfo.NeedsVirtualMethodUseTracking)
155155
{
156156
MethodDesc instantiatedTargetMethod = createInfo.TargetMethod.GetNonRuntimeDeterminedMethodFromRuntimeDeterminedMethodViaSubstitution(typeInstantiation, methodInstantiation);
157-
if (!factory.VTable(instantiatedTargetMethod.OwningType).HasFixedSlots)
157+
if (!factory.VTable(instantiatedTargetMethod.OwningType).HasKnownVirtualMethodUse)
158158
{
159159
result.Add(
160160
new DependencyListEntry(

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

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public enum ReadyToRunHelperId
4343
ConstrainedDirectCall,
4444
}
4545

46-
public partial class ReadyToRunHelperNode : AssemblyStubNode, INodeWithDebugInfo
46+
public partial class ReadyToRunHelperNode : AssemblyStubNode
4747
{
4848
private readonly ReadyToRunHelperId _id;
4949
private readonly object _target;
@@ -64,7 +64,6 @@ public ReadyToRunHelperNode(ReadyToRunHelperId id, object target)
6464
defType.ComputeStaticFieldLayout(StaticLayoutKind.StaticRegionSizesAndFields);
6565
}
6666
break;
67-
case ReadyToRunHelperId.VirtualCall:
6867
case ReadyToRunHelperId.ResolveVirtualFunction:
6968
{
7069
// Make sure we aren't trying to callvirt Object.Finalize
@@ -92,9 +91,6 @@ public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilde
9291
{
9392
switch (_id)
9493
{
95-
case ReadyToRunHelperId.VirtualCall:
96-
sb.Append("__VirtualCall_").Append(nameMangler.GetMangledMethodName((MethodDesc)_target));
97-
break;
9894
case ReadyToRunHelperId.GetNonGCStaticBase:
9995
sb.Append("__GetNonGCStaticBase_").Append(nameMangler.GetMangledTypeName((TypeDesc)_target));
10096
break;
@@ -122,7 +118,7 @@ public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilde
122118

123119
protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFactory factory)
124120
{
125-
if (_id == ReadyToRunHelperId.VirtualCall || _id == ReadyToRunHelperId.ResolveVirtualFunction)
121+
if (_id == ReadyToRunHelperId.ResolveVirtualFunction)
126122
{
127123
var targetMethod = (MethodDesc)_target;
128124

@@ -131,7 +127,7 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact
131127
#if !SUPPORT_JIT
132128
factory.MetadataManager.GetDependenciesDueToVirtualMethodReflectability(ref dependencyList, factory, targetMethod);
133129

134-
if (!factory.VTable(targetMethod.OwningType).HasFixedSlots)
130+
if (!factory.VTable(targetMethod.OwningType).HasKnownVirtualMethodUse)
135131

136132
{
137133
dependencyList.Add(factory.VirtualMethodUse((MethodDesc)_target), "ReadyToRun Virtual Method Call");
@@ -152,7 +148,7 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact
152148
#if !SUPPORT_JIT
153149
factory.MetadataManager.GetDependenciesDueToVirtualMethodReflectability(ref dependencyList, factory, targetMethod);
154150

155-
if (!factory.VTable(info.TargetMethod.OwningType).HasFixedSlots)
151+
if (!factory.VTable(info.TargetMethod.OwningType).HasKnownVirtualMethodUse)
156152
{
157153
dependencyList ??= new DependencyList();
158154
dependencyList.Add(factory.VirtualMethodUse(info.TargetMethod), "ReadyToRun Delegate to virtual method");
@@ -176,39 +172,6 @@ public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDep
176172
return dependencyList;
177173
}
178174

179-
IEnumerable<NativeSequencePoint> INodeWithDebugInfo.GetNativeSequencePoints()
180-
{
181-
if (_id == ReadyToRunHelperId.VirtualCall)
182-
{
183-
// Generate debug information that lets debuggers step into the virtual calls.
184-
// We generate a step into sequence point at the point where the helper jumps to
185-
// the target of the virtual call.
186-
TargetDetails target = ((MethodDesc)_target).Context.Target;
187-
int debuggerStepInOffset = -1;
188-
switch (target.Architecture)
189-
{
190-
case TargetArchitecture.X64:
191-
debuggerStepInOffset = 3;
192-
break;
193-
}
194-
if (debuggerStepInOffset != -1)
195-
{
196-
return new NativeSequencePoint[]
197-
{
198-
new NativeSequencePoint(0, string.Empty, WellKnownLineNumber.DebuggerStepThrough),
199-
new NativeSequencePoint(debuggerStepInOffset, string.Empty, WellKnownLineNumber.DebuggerStepIn)
200-
};
201-
}
202-
}
203-
204-
return Array.Empty<NativeSequencePoint>();
205-
}
206-
207-
IEnumerable<DebugVarInfoMetadata> INodeWithDebugInfo.GetDebugVars()
208-
{
209-
return Array.Empty<DebugVarInfoMetadata>();
210-
}
211-
212175
#if !SUPPORT_JIT
213176
public override int ClassCode => -911637948;
214177

@@ -224,7 +187,6 @@ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer
224187
case ReadyToRunHelperId.GetGCStaticBase:
225188
case ReadyToRunHelperId.GetThreadStaticBase:
226189
return comparer.Compare((TypeDesc)_target, (TypeDesc)((ReadyToRunHelperNode)other)._target);
227-
case ReadyToRunHelperId.VirtualCall:
228190
case ReadyToRunHelperId.ResolveVirtualFunction:
229191
return comparer.Compare((MethodDesc)_target, (MethodDesc)((ReadyToRunHelperNode)other)._target);
230192
case ReadyToRunHelperId.DelegateCtor:

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(factory, slotDefiningMethod) && !factory.VTable(slotDefiningMethod.OwningType).HasFixedSlots)
71+
if (ReflectionVirtualInvokeMapNode.NeedsVirtualInvokeInfo(factory, slotDefiningMethod) && !factory.VTable(slotDefiningMethod.OwningType).HasKnownVirtualMethodUse)
7272
dependencies.Add(factory.VirtualMethodUse(slotDefiningMethod), "Virtually callable reflectable method");
7373
}
7474
}

0 commit comments

Comments
 (0)