Skip to content

Commit 17f8138

Browse files
Trim unused interfaces (#100000)
So far, the compiler could only trim unused interface __methods__; the interface __types__ themselves were left alone. There is not a huge cost associated with having these extra `MethodTable`s, but it sill impacts size/working set/startup. Initial estimate showed this could actually be up to 2% for BasicMinimalApi so I decided to investigate a fix. This is an attempt to start trimming them. I chose a relatively conservative approach: * Stop unconditionally rooting the interface `MethodTable` in the implementing class `MethodTable` InterfaceList. Instead check whether someone else marked it first. * Track whether an interface type definition is used in any way. This avoids problem with variance, etc. If an interface definition is used, all instantiations that we were trying to optimize away get the `MethodTable` and won't be optimized. We should be able to do better than this, maybe, at some point. * Classes that implement generic interfaces with default methods need special treatment because we index into interface list at runtime and we might not know the correct index yet. So just forget the optimization in that case. Fixes #66716.
1 parent 40cb4b6 commit 17f8138

21 files changed

Lines changed: 224 additions & 57 deletions

File tree

src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/TypeCast.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,17 @@ internal enum AssignmentVariation
148148
interfaceMap++;
149149
interfaceCount--;
150150
} while (interfaceCount > 0);
151+
}
151152

152-
extra:
153-
if (mt->IsIDynamicInterfaceCastable)
154-
{
155-
goto slowPath;
156-
}
153+
extra:
154+
// NOTE: this check is outside the `if (interfaceCount != 0)` check because
155+
// we could have devirtualized and inlined all uses of IDynamicInterfaceCastable
156+
// (and optimized the interface MethodTable away) and still have a type that
157+
// is legitimately marked IDynamicInterfaceCastable (without having the MethodTable
158+
// of IDynamicInterfaceCastable in the interface list).
159+
if (mt->IsIDynamicInterfaceCastable)
160+
{
161+
goto slowPath;
157162
}
158163

159164
obj = null;

src/coreclr/tools/Common/Compiler/Dataflow/DynamicallyAccessedMembersBinder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ public static IEnumerable<TypeSystemEntity> GetDynamicallyAccessedMembers(this T
121121
foreach (var e in typeDefinition.GetEventsOnTypeHierarchy(filter: null, bindingFlags: BindingFlags.Public | declaredOnlyFlags))
122122
yield return e;
123123
}
124+
125+
if (memberTypes.HasFlag(DynamicallyAccessedMemberTypes.Interfaces))
126+
{
127+
foreach (DefType iface in typeDefinition.GetAllInterfaceImplementations(declaredOnly))
128+
yield return iface;
129+
}
124130
}
125131

126132
public static IEnumerable<MethodDesc> GetConstructorsOnType(this TypeDesc type, Func<MethodDesc, bool> filter, BindingFlags? bindingFlags = null)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@ public bool GeneratesMetadata(EcmaModule module, ExportedTypeHandle exportedType
261261
return GeneratesMetadata(targetType);
262262
}
263263

264+
public bool GeneratesInterfaceImpl(MetadataType typeDef, MetadataType interfaceImpl)
265+
{
266+
return _parent.IsInterfaceUsed(interfaceImpl.GetTypeDefinition());
267+
}
268+
264269
public bool IsBlocked(MetadataType typeDef)
265270
{
266271
return _blockingPolicy.IsBlocked(typeDef);

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/ReflectionMarker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ internal void MarkTypeSystemEntity(in MessageOrigin origin, TypeSystemEntity ent
7979
MarkEvent(origin, @event, reason, accessKind);
8080
break;
8181
// case InterfaceImplementation
82-
// Nothing to do currently as Native AOT will preserve all interfaces on a preserved type
82+
// This is handled in the MetadataType case above
8383
}
8484
}
8585

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,12 @@ protected override void OutputGCDesc(ref ObjectDataBuilder builder)
9696

9797
protected override void OutputInterfaceMap(NodeFactory factory, ref ObjectDataBuilder objData)
9898
{
99-
for (int i = 0; i < _type.RuntimeInterfaces.Length; i++)
99+
foreach (DefType intface in _type.RuntimeInterfaces)
100100
{
101+
// If the interface was optimized away, skip it
102+
if (!factory.InterfaceUse(intface.GetTypeDefinition()).Marked)
103+
continue;
104+
101105
// Interface omitted for canonical instantiations (constructed at runtime for dynamic types from the native layout info)
102106
objData.EmitZeroPointer();
103107
}

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

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,12 @@ public sealed override bool HasConditionalStaticDependencies
299299
return true;
300300
}
301301

302+
// If the type implements at least one interface, calls against that interface could result in this type's
303+
// implementation being used.
304+
// It might also be necessary for casting purposes.
305+
if (_type.RuntimeInterfaces.Length > 0)
306+
return true;
307+
302308
if (!EmitVirtualSlots)
303309
return false;
304310

@@ -328,11 +334,6 @@ public sealed override bool HasConditionalStaticDependencies
328334
currentType = currentType.BaseType;
329335
}
330336

331-
// If the type implements at least one interface, calls against that interface could result in this type's
332-
// implementation being used.
333-
if (_type.RuntimeInterfaces.Length > 0)
334-
return true;
335-
336337
return _hasConditionalDependenciesFromMetadataManager;
337338
}
338339
}
@@ -367,6 +368,18 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
367368
"Information about static bases for type with template"));
368369
}
369370

371+
if (!_type.IsGenericDefinition && !_type.IsCanonicalSubtype(CanonicalFormKind.Any))
372+
{
373+
foreach (DefType iface in _type.RuntimeInterfaces)
374+
{
375+
var ifaceDefinition = (DefType)iface.GetTypeDefinition();
376+
result.Add(new CombinedDependencyListEntry(
377+
GetInterfaceTypeNode(factory, iface),
378+
factory.InterfaceUse(ifaceDefinition),
379+
"Interface definition was visible"));
380+
}
381+
}
382+
370383
if (!EmitVirtualSlots)
371384
return result;
372385

@@ -526,6 +539,14 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
526539
{
527540
// Canonical instance default methods need to go through a thunk that adds the right generic context
528541
defaultIntfMethod = factory.TypeSystemContext.GetDefaultInterfaceMethodImplementationThunk(defaultIntfMethod, defType.ConvertToCanonForm(CanonicalFormKind.Specific), providingInterfaceDefinitionType);
542+
543+
// The above thunk will index into interface list to find the right context. Make sure to keep all interfaces prior to this one
544+
for (int i = 0; i < interfaceIndex; i++)
545+
{
546+
result.Add(new CombinedDependencyListEntry(
547+
factory.InterfaceUse(defTypeRuntimeInterfaces[i].GetTypeDefinition()),
548+
factory.VirtualMethodUse(interfaceMethod), "Interface with shared default methods folows this"));
549+
}
529550
}
530551
result.Add(new CombinedDependencyListEntry(factory.MethodEntrypoint(defaultIntfMethod), factory.VirtualMethodUse(interfaceMethod), "Interface method"));
531552

@@ -580,6 +601,9 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact
580601
// emitting it.
581602
dependencies.Add(new DependencyListEntry(_optionalFieldsNode, "Optional fields"));
582603

604+
if (_type.IsInterface)
605+
dependencies.Add(factory.InterfaceUse(_type.GetTypeDefinition()), "Interface is used");
606+
583607
if (EmitVirtualSlots)
584608
{
585609
if (!_type.IsArrayTypeWithoutGenericInterfaces())
@@ -690,7 +714,11 @@ protected override ObjectData GetDehydratableData(NodeFactory factory, bool relo
690714

691715
// Emit interface map
692716
SlotCounter interfaceSlotCounter = SlotCounter.BeginCounting(ref /* readonly */ objData);
693-
OutputInterfaceMap(factory, ref objData);
717+
718+
if (!relocsOnly)
719+
{
720+
OutputInterfaceMap(factory, ref objData);
721+
}
694722

695723
// Update slot count
696724
int numberOfInterfaceSlots = interfaceSlotCounter.CountSlots(ref /* readonly */ objData);
@@ -1090,7 +1118,13 @@ protected virtual void OutputInterfaceMap(NodeFactory factory, ref ObjectDataBui
10901118
{
10911119
foreach (var itf in _type.RuntimeInterfaces)
10921120
{
1093-
objData.EmitPointerReloc(GetInterfaceTypeNode(factory, itf));
1121+
IEETypeNode interfaceTypeNode = GetInterfaceTypeNode(factory, itf);
1122+
1123+
// Only emit interfaces that were not optimized away.
1124+
if (interfaceTypeNode.Marked)
1125+
{
1126+
objData.EmitPointerReloc(interfaceTypeNode);
1127+
}
10941128
}
10951129
}
10961130

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,18 @@ private void EmitDispatchMap(ref ObjectDataBuilder builder, NodeFactory factory)
164164
bool needsEntriesForInstanceInterfaceMethodImpls = !isInterface
165165
|| ((MetadataType)declType).IsDynamicInterfaceCastableImplementation();
166166

167+
int entryIndex = 0;
168+
167169
// Resolve all the interfaces, but only emit non-static and non-default implementations
168170
for (int interfaceIndex = 0; interfaceIndex < declTypeRuntimeInterfaces.Length; interfaceIndex++)
169171
{
170172
var interfaceType = declTypeRuntimeInterfaces[interfaceIndex];
171173
var definitionInterfaceType = declTypeDefinitionRuntimeInterfaces[interfaceIndex];
172174
Debug.Assert(interfaceType.IsInterface);
173175

176+
if (!factory.InterfaceUse(interfaceType.GetTypeDefinition()).Marked)
177+
continue;
178+
174179
IReadOnlyList<MethodDesc> virtualSlots = factory.VTable(interfaceType).Slots;
175180

176181
for (int interfaceMethodSlot = 0; interfaceMethodSlot < virtualSlots.Count; interfaceMethodSlot++)
@@ -210,11 +215,11 @@ private void EmitDispatchMap(ref ObjectDataBuilder builder, NodeFactory factory)
210215
int genericContext = targetMethod.GetCanonMethodTarget(CanonicalFormKind.Specific).RequiresInstArg()
211216
? StaticVirtualMethodContextSource.ContextFromThisClass
212217
: StaticVirtualMethodContextSource.None;
213-
staticImplementations.Add((interfaceIndex, emittedInterfaceSlot, emittedImplSlot, genericContext));
218+
staticImplementations.Add((entryIndex, emittedInterfaceSlot, emittedImplSlot, genericContext));
214219
}
215220
else
216221
{
217-
builder.EmitShort((short)checked((ushort)interfaceIndex));
222+
builder.EmitShort((short)checked((ushort)entryIndex));
218223
builder.EmitShort((short)checked((ushort)emittedInterfaceSlot));
219224
builder.EmitShort((short)checked((ushort)emittedImplSlot));
220225
entryCount++;
@@ -271,21 +276,23 @@ private void EmitDispatchMap(ref ObjectDataBuilder builder, NodeFactory factory)
271276
}
272277
}
273278
staticDefaultImplementations.Add((
274-
interfaceIndex,
279+
entryIndex,
275280
emittedInterfaceSlot,
276281
implSlot.Value,
277282
genericContext));
278283
}
279284
else
280285
{
281286
defaultImplementations.Add((
282-
interfaceIndex,
287+
entryIndex,
283288
emittedInterfaceSlot,
284289
implSlot.Value));
285290
}
286291
}
287292
}
288293
}
294+
295+
entryIndex++;
289296
}
290297

291298
// Now emit the default implementations
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
6+
using ILCompiler.DependencyAnalysisFramework;
7+
8+
using Internal.TypeSystem;
9+
10+
using Debug = System.Diagnostics.Debug;
11+
12+
namespace ILCompiler.DependencyAnalysis
13+
{
14+
/// <summary>
15+
/// Tracks uses of interface in IL sense: at the level of type definitions.
16+
/// Trim warning suppressions within the framework prevent us from optimizing
17+
/// at a smaller granularity (e.g. canonical forms or concrete instantiations).
18+
/// </summary>
19+
internal sealed class InterfaceUseNode : DependencyNodeCore<NodeFactory>
20+
{
21+
public TypeDesc Type { get; }
22+
23+
public InterfaceUseNode(TypeDesc type)
24+
{
25+
Debug.Assert(type.IsTypeDefinition);
26+
Debug.Assert(type.IsInterface);
27+
Type = type;
28+
}
29+
30+
protected override string GetName(NodeFactory factory) => $"Interface use: {Type}";
31+
32+
public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFactory factory) => null;
33+
public override bool InterestingForDynamicDependencyAnalysis => false;
34+
public override bool HasDynamicDependencies => false;
35+
public override bool HasConditionalStaticDependencies => false;
36+
public override bool StaticDependenciesAreComputed => true;
37+
public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory factory) => null;
38+
public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory context) => null;
39+
}
40+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,15 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
909909
}
910910
}
911911

912+
foreach (GenericParameterDesc genericParam in _method.GetTypicalMethodDefinition().Instantiation)
913+
{
914+
foreach (TypeDesc typeConstraint in genericParam.TypeConstraints)
915+
{
916+
if (typeConstraint.IsInterface)
917+
yield return new DependencyListEntry(context.InterfaceUse(typeConstraint.GetTypeDefinition()), "Used as constraint");
918+
}
919+
}
920+
912921
yield return new DependencyListEntry(context.GenericDictionaryLayout(_method), "Dictionary layout");
913922
}
914923

@@ -1026,6 +1035,15 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
10261035
yield return new DependencyListEntry(context.MethodEntrypoint(_type.GetStaticConstructor().GetCanonMethodTarget(CanonicalFormKind.Specific)), "cctor for template");
10271036
}
10281037

1038+
foreach (GenericParameterDesc genericParam in _type.GetTypeDefinition().Instantiation)
1039+
{
1040+
foreach (TypeDesc typeConstraint in genericParam.TypeConstraints)
1041+
{
1042+
if (typeConstraint.IsInterface)
1043+
yield return new DependencyListEntry(context.InterfaceUse(typeConstraint.GetTypeDefinition()), "Used as constraint");
1044+
}
1045+
}
1046+
10291047
if (!_isUniversalCanon)
10301048
{
10311049
DefType closestCanonDefType = (DefType)_type.GetClosestDefType().ConvertToCanonForm(CanonicalFormKind.Specific);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ public NecessaryCanonicalEETypeNode(NodeFactory factory, TypeDesc type) : base(f
2323

2424
protected override void OutputInterfaceMap(NodeFactory factory, ref ObjectDataBuilder objData)
2525
{
26-
for (int i = 0; i < _type.RuntimeInterfaces.Length; i++)
26+
foreach (DefType intface in _type.RuntimeInterfaces)
2727
{
28+
// If the interface was optimized away, skip it
29+
if (!factory.InterfaceUse(intface.GetTypeDefinition()).Marked)
30+
continue;
31+
2832
// Interface omitted for canonical instantiations (constructed at runtime for dynamic types from the native layout info)
2933
objData.EmitZeroPointer();
3034
}

0 commit comments

Comments
 (0)