-
Notifications
You must be signed in to change notification settings - Fork 127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Linker doesn't trim IEnumerable interface in console app even if it's not used #3189
Comments
My understanding is that the runtime's implementation of Array depends on specific order of interfaces on that type, and thus they can't be trimmed. For NativeAOT we might be able to do better though. @MichalStrehovsky - any ideas? I checked and in the dependency graph the presence of the interface vtable (which is what really counts in NativeAOT case) is first required by the |
Does this mean that once an interface type is rooted all its implementations will be as well? |
I wonder whether there is a difference between the runtime and user code rooting an interface, but this repro does suggest that if a specific method from the interface is used, then all of the implementations of that method are going to be left out (even if it's easily proven that it's never going to get called): static void Main(string[] args)
{
ICustomInterface t = new CustomClass();
t.Test();
CustomClass2 t2 = new CustomClass2();
}
interface ICustomInterface
{
void Test();
}
private sealed class CustomClass : ICustomInterface
{
public void Test()
{
}
}
private sealed class CustomClass2 : ICustomInterface
{
public void Test()
{
}
} |
Probably difficult to prove more complex cases where the other implementation is passed around as an ienumerable value. Would be very easy to thwart any tracing once we're dealing with those virtual methods. |
Linker is probably not as clever as you think :-)
Also of note, per the type system rules, when a type implements an interface, it has to implement all of the abstract members of that interface. This sometimes leads linker into keeping the method, but removing its body (replaced with a simple throw). That said - it probably doesn't happen in your example. Just to give you an example of a piece code which is really hard to track: interface IUtil { }
class MyUtil : IUtil {}
class Test
{
object _instance;
object _value;
public void One() { _instance = new MyUtil() }
public void Two() { ((IUtil)_value).DoSomething();
} For this to be solvable linker would have to be able to model full data flow of values across fields. Possible, but very hard (it's gets even weirder if you consider that fields can be accessed from multiple threads at once). The complexity of linker would be really high, and consequently its performance would probably be very bad. A grand example of this is the Right now linker implements only very few and very simple cross-method analysis - almost all analysis it does is within a single method's body - where it models data flow for some pieces of data (typically tied to reflection). This allows linker to get by with almost a single pass over all method bodies (it does two in reality because of the one cross-method thing). For a full data flow, it would have to do many passes. That gets slow really quickly, and while linker is a publish tool, it still needs to run in reasonable amount of time. It's also unclear how much memory would this need - in some cases linker already consumes a lot of memory, full data flow tracking might lead to real memory problems. |
For a foundational interface like IEnumerable, there's a good chance that something in CoreLib is going to use this. Even in a hello world, we need to keep a lot more than what is in user's main. For IEnumerable, just this weekend I was looking at this piece of code that uses IEnumerable even in a program that doesn't do anything in Main: This code is responsible for e.g. reading the exception message for |
I've been playing around with this but I can't replicate the requirements as described by @vitek-karas using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
class Program
{
static readonly string IMyListAqn = $"IMyList`1, {typeof(Program).Assembly.FullName}";
public static void Main()
{
Console.WriteLine(TrimmableGetType(IMyListAqn));
var objectList = new MyList<object>();
// IMyList<object> instantiation is kept, never used, maybe a __Canon opt?
Console.WriteLine("IMyList<object> kept: " + TrimmableCheckInterfaces(objectList.GetType()));
var intList = new MyList<int>();
// Nope IMyList<int> instantiation is kept too, also never used.
Console.WriteLine("IMyList<int> kept: " + TrimmableCheckInterfaces(objectList.GetType()));
// Throws 'not enough metadata' as expected.
var stringList = TrimmableGetType($"MyList`1[[System.String, System.Private.CoreLib]], {typeof(Program).Assembly.FullName}");
Console.WriteLine(stringList);
Console.WriteLine("IMyList<string> kept: " + TrimmableCheckInterfaces(stringList!));
}
[UnconditionalSuppressMessage("Trimming", "IL2070")]
static bool TrimmableCheckInterfaces(Type type) =>
type.GetInterfaces().Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == TrimmableGetType(IMyListAqn));
[UnconditionalSuppressMessage("Trimming", "IL2057")]
static Type? TrimmableGetType(string str) => Type.GetType(str);
}
interface IMyList<T>
{
void Add(T item);
T this[int index] { get; }
}
class MyList<T> : IMyList<T>
{
void IMyList<T>.Add(T item) => throw new NotImplementedException();
T IMyList<T>.this[int index] => throw new NotImplementedException();
} I've explicitly defined a new interface here to see if it's based on usage, it doesn't seem to be the case. So when are interface types trimmed? |
I played with this a little bit @NinoFloris and the behavior is different between the trimmer ( But it doesn't keep any of the methods. As for the reason, I must admit I don't know. @MichalStrehovsky might understand this a bit better - this is the dependency chain reported for it: |
Interface lists are not trimmed in native AOT right now; it's tracked in this issue: dotnet/runtime#66716 (comment). We do trim the method implementations but the type itself is kept. The cost of the type is in tens to hundreds of bytes so one needs to have many for trimming them to bring a meaningful difference (it hasn't risen up high enough in terms of priority, given the complexity). |
Npgsql implements that interface for
NpgsqlDataReader
(becauseNpgsqlDataReader
inherits fromDbDataReader
, inheritingIEnumerable
). Our implementation ofGetEnumerator
returnsSystem.Data.Common.DbEnumerator
, which touches quite a bit ofIDataReader
methods, and some of them are not AOT friendly (mostly related to size).The text was updated successfully, but these errors were encountered: