-
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
Don't remove unused interfaces in library mode #2711
Conversation
This will make sure that we keep all interfaces and all interface method implementations on such type.
src/linker/Linker.Steps/MarkStep.cs
Outdated
if (unusedInterfacesOptimizationEnabled) | ||
continue; | ||
|
||
// If the optimization is disabled, make sure to mark all methods which implement interfaces |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why we need this logic. It doesn't actually seem to mark all methods which implement interfaces, so maybe the comment needs clarification - but either way I would have expected that the logic in ProcessOverride
would already be marking the required methods which implement interface methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be right - I must admit that I still don't fully grasp the exact order in which things happen in this case.
I'm pretty sure the comment was there because that was what I wanted to do - but then I realized that I don't need that actually since ProcessOverride will do it for me. So I simplified the code. It's possible it can be entirely removed.
What the code does currently is that it will mark methods which are overrides/impls of methods from non-link assemblies (in library mode that's basically anything outside of the current assembly). I'm not sure if we can remove that fully - it depends if in library mode we mark all of the non-link assemblies, I'm not sure we do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure what we currently do for "library mode" since we never really came up with a design for it, but I think you're right that we don't mark assemblies other than the library. That's also what would make sense to me.
So I guess ProcessOverride
won't be called for the case of a non-virtual method implementing an interface method defined in a referenced assembly - then I can see why we would need this logic. I wonder if there's a way we could simplify this, since it's very similar to IsVirtualNeededByTypeDueToPreservedScope
. It feels like we want to do substantially the same thing for virtual method overrides and for interface method implementations.
Maybe that could be generalized to IsMethodNeededByTypeDueToPreservedScope
so that it works for non-virtual methods, and methods implementing interfaces.
// UnusedInterfaces optimization is turned off mark all interface implementations | ||
bool unusedInterfacesOptimizationEnabled = Context.IsOptimizationEnabled (CodeOptimizations.UnusedInterfaces, type); | ||
if (!Annotations.IsInstantiated (type) && !Annotations.IsRelevantToVariantCasting (type) && | ||
unusedInterfacesOptimizationEnabled) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this is marking the .interfaceimpl
unconditionally. Is that what we want? It seems like it could result in keeping the impl for private interfaces, which isn't strictly necessary for library mode - it should be ok to remove impls of unused private interfaces.
If we want to keep all impls for simplicity, it's worth clarifying whether the intention is to keep all interface methods as well. My gut reaction is that it would be confusing to keep all impls but still allow trimming unused interface methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right - this didn't consider private interfaces.
We might need to modify the behavior of this change. I think the best description of the desired behavior for library mode is:
The change doesn't try to implement exactly that, instead it tries to define and implement what is the correct behavior if the What library mode already does:
What we could do is change the linker such that if This would mean that since we already mark all public types and interfaces, we will preserve all public interfaces on all public types. It also means that the public interfaces will keep all their methods and thus we should keep all the method impls of such interfaces. The remaining question is what is the desired behavior of Personally I would keep this simple and "relax" the library mode trimming - the size impact of this should be relatively small, but we need to measure for sure. But my preference is almost purely based on the simplicity of the solution - with the assumption that size impact is negligible. |
Discussed this with Sven offline: we would like to keep this as simple as possible - so we should try to go with the "keep all interfaces on marked types" solution (basically fix the behavior of |
@@ -8,6 +12,7 @@ namespace Mono.Linker.Tests.Cases.Libraries | |||
{ | |||
[SetupLinkerArgument ("-a", "test.exe", "library")] | |||
[SetupLinkerArgument ("--enable-opt", "ipconstprop")] | |||
[SetupLinkerArgument ("--skip-unresolved")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you come up with a test that does not require this extra setting?
I spent some time looking into this in more detail and also into the static interface problem mentioned in dotnet/runtime#67453. I also had several discussions about this. My preferred solution is:
For the implementation I would keep it simple for now, that is very close to the current state of this PR - keep the delayed processing of types with interfaces, and simply disable the "is instantiated" check if the optimization is off. The alternative would be to mark the interface implementations eagerly, but that would introduce much larger difference in behavior with the optimization on/off which is more risky (and really adds complexity where it's unnecessary). This means we need the new code which iterates over all methods on a type if its interfaces should be kept and uses I also debugged the static interface case and that one seems to be a simple case of "forgetting". The Annotations class already handles static interface methods correctly (so the "get bases" and "get overrides" already cover static interface methods as well). The bug is in https://github.com/dotnet/linker/pull/2711/files#diff-f3ab7d627296ba105613b9cc039ca6f4ddc7a9aa66c6060ca82f6456ae0ede4fL2314: |
Check for optimization before skipping marking interface methods for PreservedScope Add doc comments
// if the type is never instantiated, interfaces will be removed | ||
if (@base.DeclaringType.IsInterface) | ||
// if the type is never instantiated, interfaces will be removed - but only if the optimization is enabled | ||
if (@base.DeclaringType.IsInterface && Context.IsOptimizationEnabled (CodeOptimizations.UnusedInterfaces, method.DeclaringType)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check here eliminates the necessity for marking methods in IsVirtualMethodNeededByInstantiatedTypeDueToPreservedScope. Since we don't exit early here when the optimization is disabled, we mark methods where the interface comes from an IgnoreScope assembly.
Finally got managed to get sizes comparing this branch vs main on runtime that I am fairly confident in.
I have no idea why the packages would be smaller after the fix, and I can drill down into this further, but just not 100% sure which outputs are trimmed or not. |
The |
@@ -1931,7 +1922,7 @@ internal void MarkStaticConstructorVisibleToReflection (TypeDefinition type, in | |||
MarkGenericParameterProvider (type); | |||
|
|||
// There are a number of markings we can defer until later when we know it's possible a reference type could be instantiated | |||
// For example, if no instance of a type exist, then we don't need to mark the interfaces on that type | |||
// For example, if no instance of a type exist, then we don't need to mark the interfaces on that type -- Note this is not true for static interfaces |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the added comment is correct, I think it makes this whole section confusing. Maybe you should add that the MarkRequiermentsForInstantiatedTypes
also works for static interfaces - and maybe we should rename that method then.
src/linker/Linker.Steps/MarkStep.cs
Outdated
{ | ||
if (!method.IsVirtual) | ||
if (!(method.IsVirtual || method.IsStatic)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a comment that this handles static interface methods (it's not obvious why statics are interesting here otherwise).
src/linker/Linker.Steps/MarkStep.cs
Outdated
/// Returns true if any of the base methods of <paramref name="method" /> is defined in an assembly that is not trimmed (i.e. action!=trim) | ||
/// </summary> | ||
/// <remarks>This is very similar to <see cref="IsVirtualNeededByTypeDueToPreservedScope(MethodDefinition)"/>, except this also checks if the base method is an interface.</remarks> | ||
bool IsVirtualNeededByInstantiatedTypeDueToPreservedScope (MethodDefinition method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new name is confusing - since this method explicitly handles interface methods as well (not just virtuals). I personally don't mind the previous method, it's not super descriptive, but at least it's not wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually with the removal of the call to this method from the delayed processing, this method should not handle static interface methods. Now it's only called when the type is instantiated - and in that case only virtual methods are affected. static interface methods must be marked regardless of instantiation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the new name seems correct, but the method should not deal with statics at all.
src/linker/Linker.Steps/MarkStep.cs
Outdated
/// <summary> | ||
/// Returns true if any of the base methods of the <paramref name="method"/> passed is in an assembly that is not trimmed (i.e. action != trim) | ||
/// </summary> | ||
/// <remarks>This ignores any base methods defined in interfaces. To also check methods defined in interfaces</remarks> | ||
bool IsVirtualNeededByTypeDueToPreservedScope (MethodDefinition method) | ||
{ | ||
if (!method.IsVirtual) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think with the current changes this will have to handle static interface methods as well.
Yes, this is a release build. Here's the results for windows for the artifacts/bin/teshost folder (using the same runtime commit as before: 6ffb7f0)
|
Rename IsVirtualNeededByInstantiatedTypeDueToPreservedScope to IsOverrideNeededByInstantiatedTypeDueToPreservedScope Added more comments describing purpose of methods Moved the static interface method check to the method that is called on all types regardless of instantiation
I added some more comments on the methods with my current understanding, please correct me if they're wrong. |
This doesn't change product behavior, only renamed methods and improved comments. Added new tests for the RootLibrary: - Added a dependency to an "copy" assembly (mainly because I can define a static interface in it) - Added more combinations to the interfaces/classes in the test - Since this uses static interface methods I had to enable "preview" language features for the test project and for the test infra.
Validated this change against the static interface issue found in runtime with Numerics. With this change the static interface methods are correctly preserved. |
src/linker/Linker.Steps/MarkStep.cs
Outdated
/// </summary> | ||
/// <remarks>This ignores any base methods defined in interfaces. To also check methods defined in interfaces</remarks> | ||
/// <remarks> | ||
/// When the unusedinterfaces optimization is on, this is used to mark methods that override a virtual from a non-link assembly and must be kept. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// When the unusedinterfaces optimization is on, this is used to mark methods that override a virtual from a non-link assembly and must be kept. | |
/// When the unusedinterfaces optimization is on, this is used to mark methods that override an abstract method from a non-link assembly and must be kept. |
src/linker/Linker.Steps/MarkStep.cs
Outdated
/// When the unusedinterfaces optimization is on, this is used to mark methods that override a virtual from a non-link assembly and must be kept. | ||
/// When the unusedinterfaces optimization is off, this will do the same as when on but will also mark interface methods from interfaces defined in a non-link assembly. | ||
/// If the containing type is instantiated, the caller should use <see cref="IsOverrideNeededByInstantiatedTypeDueToPreservedScope (MethodDefinition)" /> | ||
/// </remarks> | ||
bool IsVirtualNeededByTypeDueToPreservedScope (MethodDefinition method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rename this to IsMethodNeededByTypeDueToPreservedScope
- the caller uses it that way (the caller doesn't filter on virtuals - and it must not anyway).
This will make sure that we keep all interfaces and all interface method implementations on such type. * Change the solution to rely on the optimization setting only * wip * Add implicit interface implementation case * Edit tests to remove issue-specific code and names * Mark members of CollectedType as kept * Add private interface test and simplify external interface example * Replace early exit for non-interfaces * License headers and use IsMethodNeededByTypeDueToPreservedScope instead of Interface specific version * Add check for static methods before skipping virtual marking Check for optimization before skipping marking interface methods for PreservedScope Add doc comments * Add more clarifying comments, move static method check, rename method Rename IsVirtualNeededByInstantiatedTypeDueToPreservedScope to IsOverrideNeededByInstantiatedTypeDueToPreservedScope Added more comments describing purpose of methods Moved the static interface method check to the method that is called on all types regardless of instantiation * Renames and comment cleanup + tests This doesn't change product behavior, only renamed methods and improved comments. Added new tests for the RootLibrary: - Added a dependency to an "copy" assembly (mainly because I can define a static interface in it) - Added more combinations to the interfaces/classes in the test - Since this uses static interface methods I had to enable "preview" language features for the test project and for the test infra. * More tests for interface behavior Co-authored-by: vitek-karas <[email protected]> Co-authored-by: vitek-karas <[email protected]> Commit migrated from dotnet/linker@8c33ad3
@vitek-karas's changes in the linker appears to fix the library interface trimming issue without any additions. I've just added another tests with behavior closer to the issue.
After linking,
IPAddressInformationCollection
retains theICollection
interface and the properties and methods:Fixes #2238