Skip to content

Commit

Permalink
Trim unused interfaces (#100000)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
MichalStrehovsky committed Mar 23, 2024
1 parent 40cb4b6 commit 17f8138
Show file tree
Hide file tree
Showing 21 changed files with 224 additions and 57 deletions.
15 changes: 10 additions & 5 deletions src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/TypeCast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,17 @@ internal enum AssignmentVariation
interfaceMap++;
interfaceCount--;
} while (interfaceCount > 0);
}

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

obj = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ public static IEnumerable<TypeSystemEntity> GetDynamicallyAccessedMembers(this T
foreach (var e in typeDefinition.GetEventsOnTypeHierarchy(filter: null, bindingFlags: BindingFlags.Public | declaredOnlyFlags))
yield return e;
}

if (memberTypes.HasFlag(DynamicallyAccessedMemberTypes.Interfaces))
{
foreach (DefType iface in typeDefinition.GetAllInterfaceImplementations(declaredOnly))
yield return iface;
}
}

public static IEnumerable<MethodDesc> GetConstructorsOnType(this TypeDesc type, Func<MethodDesc, bool> filter, BindingFlags? bindingFlags = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ public bool GeneratesMetadata(EcmaModule module, ExportedTypeHandle exportedType
return GeneratesMetadata(targetType);
}

public bool GeneratesInterfaceImpl(MetadataType typeDef, MetadataType interfaceImpl)
{
return _parent.IsInterfaceUsed(interfaceImpl.GetTypeDefinition());
}

public bool IsBlocked(MetadataType typeDef)
{
return _blockingPolicy.IsBlocked(typeDef);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ internal void MarkTypeSystemEntity(in MessageOrigin origin, TypeSystemEntity ent
MarkEvent(origin, @event, reason, accessKind);
break;
// case InterfaceImplementation
// Nothing to do currently as Native AOT will preserve all interfaces on a preserved type
// This is handled in the MetadataType case above
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ protected override void OutputGCDesc(ref ObjectDataBuilder builder)

protected override void OutputInterfaceMap(NodeFactory factory, ref ObjectDataBuilder objData)
{
for (int i = 0; i < _type.RuntimeInterfaces.Length; i++)
foreach (DefType intface in _type.RuntimeInterfaces)
{
// If the interface was optimized away, skip it
if (!factory.InterfaceUse(intface.GetTypeDefinition()).Marked)
continue;

// Interface omitted for canonical instantiations (constructed at runtime for dynamic types from the native layout info)
objData.EmitZeroPointer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ public sealed override bool HasConditionalStaticDependencies
return true;
}

// If the type implements at least one interface, calls against that interface could result in this type's
// implementation being used.
// It might also be necessary for casting purposes.
if (_type.RuntimeInterfaces.Length > 0)
return true;

if (!EmitVirtualSlots)
return false;

Expand Down Expand Up @@ -328,11 +334,6 @@ public sealed override bool HasConditionalStaticDependencies
currentType = currentType.BaseType;
}

// If the type implements at least one interface, calls against that interface could result in this type's
// implementation being used.
if (_type.RuntimeInterfaces.Length > 0)
return true;

return _hasConditionalDependenciesFromMetadataManager;
}
}
Expand Down Expand Up @@ -367,6 +368,18 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
"Information about static bases for type with template"));
}

if (!_type.IsGenericDefinition && !_type.IsCanonicalSubtype(CanonicalFormKind.Any))
{
foreach (DefType iface in _type.RuntimeInterfaces)
{
var ifaceDefinition = (DefType)iface.GetTypeDefinition();
result.Add(new CombinedDependencyListEntry(
GetInterfaceTypeNode(factory, iface),
factory.InterfaceUse(ifaceDefinition),
"Interface definition was visible"));
}
}

if (!EmitVirtualSlots)
return result;

Expand Down Expand Up @@ -526,6 +539,14 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
{
// Canonical instance default methods need to go through a thunk that adds the right generic context
defaultIntfMethod = factory.TypeSystemContext.GetDefaultInterfaceMethodImplementationThunk(defaultIntfMethod, defType.ConvertToCanonForm(CanonicalFormKind.Specific), providingInterfaceDefinitionType);

// The above thunk will index into interface list to find the right context. Make sure to keep all interfaces prior to this one
for (int i = 0; i < interfaceIndex; i++)
{
result.Add(new CombinedDependencyListEntry(
factory.InterfaceUse(defTypeRuntimeInterfaces[i].GetTypeDefinition()),
factory.VirtualMethodUse(interfaceMethod), "Interface with shared default methods folows this"));
}
}
result.Add(new CombinedDependencyListEntry(factory.MethodEntrypoint(defaultIntfMethod), factory.VirtualMethodUse(interfaceMethod), "Interface method"));

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

if (_type.IsInterface)
dependencies.Add(factory.InterfaceUse(_type.GetTypeDefinition()), "Interface is used");

if (EmitVirtualSlots)
{
if (!_type.IsArrayTypeWithoutGenericInterfaces())
Expand Down Expand Up @@ -690,7 +714,11 @@ protected override ObjectData GetDehydratableData(NodeFactory factory, bool relo

// Emit interface map
SlotCounter interfaceSlotCounter = SlotCounter.BeginCounting(ref /* readonly */ objData);
OutputInterfaceMap(factory, ref objData);

if (!relocsOnly)
{
OutputInterfaceMap(factory, ref objData);
}

// Update slot count
int numberOfInterfaceSlots = interfaceSlotCounter.CountSlots(ref /* readonly */ objData);
Expand Down Expand Up @@ -1090,7 +1118,13 @@ protected virtual void OutputInterfaceMap(NodeFactory factory, ref ObjectDataBui
{
foreach (var itf in _type.RuntimeInterfaces)
{
objData.EmitPointerReloc(GetInterfaceTypeNode(factory, itf));
IEETypeNode interfaceTypeNode = GetInterfaceTypeNode(factory, itf);

// Only emit interfaces that were not optimized away.
if (interfaceTypeNode.Marked)
{
objData.EmitPointerReloc(interfaceTypeNode);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,18 @@ private void EmitDispatchMap(ref ObjectDataBuilder builder, NodeFactory factory)
bool needsEntriesForInstanceInterfaceMethodImpls = !isInterface
|| ((MetadataType)declType).IsDynamicInterfaceCastableImplementation();

int entryIndex = 0;

// Resolve all the interfaces, but only emit non-static and non-default implementations
for (int interfaceIndex = 0; interfaceIndex < declTypeRuntimeInterfaces.Length; interfaceIndex++)
{
var interfaceType = declTypeRuntimeInterfaces[interfaceIndex];
var definitionInterfaceType = declTypeDefinitionRuntimeInterfaces[interfaceIndex];
Debug.Assert(interfaceType.IsInterface);

if (!factory.InterfaceUse(interfaceType.GetTypeDefinition()).Marked)
continue;

IReadOnlyList<MethodDesc> virtualSlots = factory.VTable(interfaceType).Slots;

for (int interfaceMethodSlot = 0; interfaceMethodSlot < virtualSlots.Count; interfaceMethodSlot++)
Expand Down Expand Up @@ -210,11 +215,11 @@ private void EmitDispatchMap(ref ObjectDataBuilder builder, NodeFactory factory)
int genericContext = targetMethod.GetCanonMethodTarget(CanonicalFormKind.Specific).RequiresInstArg()
? StaticVirtualMethodContextSource.ContextFromThisClass
: StaticVirtualMethodContextSource.None;
staticImplementations.Add((interfaceIndex, emittedInterfaceSlot, emittedImplSlot, genericContext));
staticImplementations.Add((entryIndex, emittedInterfaceSlot, emittedImplSlot, genericContext));
}
else
{
builder.EmitShort((short)checked((ushort)interfaceIndex));
builder.EmitShort((short)checked((ushort)entryIndex));
builder.EmitShort((short)checked((ushort)emittedInterfaceSlot));
builder.EmitShort((short)checked((ushort)emittedImplSlot));
entryCount++;
Expand Down Expand Up @@ -271,21 +276,23 @@ private void EmitDispatchMap(ref ObjectDataBuilder builder, NodeFactory factory)
}
}
staticDefaultImplementations.Add((
interfaceIndex,
entryIndex,
emittedInterfaceSlot,
implSlot.Value,
genericContext));
}
else
{
defaultImplementations.Add((
interfaceIndex,
entryIndex,
emittedInterfaceSlot,
implSlot.Value));
}
}
}
}

entryIndex++;
}

// Now emit the default implementations
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

using ILCompiler.DependencyAnalysisFramework;

using Internal.TypeSystem;

using Debug = System.Diagnostics.Debug;

namespace ILCompiler.DependencyAnalysis
{
/// <summary>
/// Tracks uses of interface in IL sense: at the level of type definitions.
/// Trim warning suppressions within the framework prevent us from optimizing
/// at a smaller granularity (e.g. canonical forms or concrete instantiations).
/// </summary>
internal sealed class InterfaceUseNode : DependencyNodeCore<NodeFactory>
{
public TypeDesc Type { get; }

public InterfaceUseNode(TypeDesc type)
{
Debug.Assert(type.IsTypeDefinition);
Debug.Assert(type.IsInterface);
Type = type;
}

protected override string GetName(NodeFactory factory) => $"Interface use: {Type}";

public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFactory factory) => null;
public override bool InterestingForDynamicDependencyAnalysis => false;
public override bool HasDynamicDependencies => false;
public override bool HasConditionalStaticDependencies => false;
public override bool StaticDependenciesAreComputed => true;
public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory factory) => null;
public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory context) => null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,15 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
}
}

foreach (GenericParameterDesc genericParam in _method.GetTypicalMethodDefinition().Instantiation)
{
foreach (TypeDesc typeConstraint in genericParam.TypeConstraints)
{
if (typeConstraint.IsInterface)
yield return new DependencyListEntry(context.InterfaceUse(typeConstraint.GetTypeDefinition()), "Used as constraint");
}
}

yield return new DependencyListEntry(context.GenericDictionaryLayout(_method), "Dictionary layout");
}

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

foreach (GenericParameterDesc genericParam in _type.GetTypeDefinition().Instantiation)
{
foreach (TypeDesc typeConstraint in genericParam.TypeConstraints)
{
if (typeConstraint.IsInterface)
yield return new DependencyListEntry(context.InterfaceUse(typeConstraint.GetTypeDefinition()), "Used as constraint");
}
}

if (!_isUniversalCanon)
{
DefType closestCanonDefType = (DefType)_type.GetClosestDefType().ConvertToCanonForm(CanonicalFormKind.Specific);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ public NecessaryCanonicalEETypeNode(NodeFactory factory, TypeDesc type) : base(f

protected override void OutputInterfaceMap(NodeFactory factory, ref ObjectDataBuilder objData)
{
for (int i = 0; i < _type.RuntimeInterfaces.Length; i++)
foreach (DefType intface in _type.RuntimeInterfaces)
{
// If the interface was optimized away, skip it
if (!factory.InterfaceUse(intface.GetTypeDefinition()).Marked)
continue;

// Interface omitted for canonical instantiations (constructed at runtime for dynamic types from the native layout info)
objData.EmitZeroPointer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,11 @@ private void CreateNodeCaches()
return new VariantInterfaceMethodUseNode(method);
});

_interfaceUses = new NodeCache<TypeDesc, InterfaceUseNode>((TypeDesc type) =>
{
return new InterfaceUseNode(type);
});

_readyToRunHelpers = new NodeCache<ReadyToRunHelperKey, ISymbolNode>(CreateReadyToRunHelperNode);

_genericReadyToRunHelpersFromDict = new NodeCache<ReadyToRunGenericHelperKey, ISymbolNode>(CreateGenericLookupFromDictionaryNode);
Expand Down Expand Up @@ -1175,6 +1180,13 @@ public DependencyNodeCore<NodeFactory> VariantInterfaceMethodUse(MethodDesc decl
return _variantMethods.GetOrAdd(decl);
}

private NodeCache<TypeDesc, InterfaceUseNode> _interfaceUses;

public DependencyNodeCore<NodeFactory> InterfaceUse(TypeDesc type)
{
return _interfaceUses.GetOrAdd(type);
}

private NodeCache<ReadyToRunHelperKey, ISymbolNode> _readyToRunHelpers;

public ISymbolNode ReadyToRunHelper(ReadyToRunHelperId id, object target)
Expand Down
Loading

0 comments on commit 17f8138

Please sign in to comment.