[dotnet][linker] Remove unused backing fields (#12001)

Problems:

* `Dispose` set the generated backing fields to `null` which means
  the linker will mark every backing fields, even if not used
  elsewhere (generally properties) inside the class

* Backing fields increase the memory footprint of the managed peer
  instance (for the type and all it's subclasses)

* Backing fields also increase the app size. Not a huge problem as
  they are all declared _weakly_ as `NSObject` but still...

Solution:

* When the linker process a `Dispose` method of an `NSObject`
  subclass with the _optimizable_ attribute then we remove the
  method body. This way the linker cannot mark the fields.

* Before saving back the assemblies we replace the cached method
  body and NOP every field that were not marked by something else
  than the `Dispose` method.

```diff
--- a.cs	2021-06-22 16:56:57.000000000 -0400
+++ b.cs	2021-06-22 16:57:00.000000000 -0400
@@ -3107,8 +3107,6 @@

 		private static readonly IntPtr class_ptr = Class.GetHandle("UIApplication");

-		private object __mt_WeakDelegate_var;
-
 		public override IntPtr ClassHandle => class_ptr;

 		[DllImport("__Internal")]
@@ -3141,9 +3139,8 @@
 		protected override void Dispose(bool P_0)
 		{
 			base.Dispose(P_0);
-			if (base.Handle == IntPtr.Zero)
+			if (!(base.Handle == IntPtr.Zero))
 			{
-				__mt_WeakDelegate_var = null;
 			}
 		}
 	}
@@ -3209,10 +3206,6 @@
 	{
 		private static readonly IntPtr class_ptr = Class.GetHandle("UIScreen");

-		private object __mt_FocusedItem_var;
-
-		private object __mt_FocusedView_var;
-
 		public override IntPtr ClassHandle => class_ptr;

 		public virtual CGRect Bounds
@@ -3242,10 +3235,8 @@
 		protected override void Dispose(bool P_0)
 		{
 			base.Dispose(P_0);
-			if (base.Handle == IntPtr.Zero)
+			if (!(base.Handle == IntPtr.Zero))
 			{
-				__mt_FocusedItem_var = null;
-				__mt_FocusedView_var = null;
 			}
 		}
 	}
@@ -3254,10 +3245,6 @@
 	{
 		private static readonly IntPtr class_ptr = Class.GetHandle("UIView");

-		private object __mt_ParentFocusEnvironment_var;
-
-		private object __mt_PreferredFocusedView_var;
-
 		public override IntPtr ClassHandle => class_ptr;

 		public virtual CGRect Bounds
@@ -3303,10 +3290,8 @@
 		protected override void Dispose(bool P_0)
 		{
 			base.Dispose(P_0);
-			if (base.Handle == IntPtr.Zero)
+			if (!(base.Handle == IntPtr.Zero))
 			{
-				__mt_ParentFocusEnvironment_var = null;
-				__mt_PreferredFocusedView_var = null;
 			}
 		}
 	}
@@ -3315,12 +3300,6 @@
 	{
 		private static readonly IntPtr class_ptr = Class.GetHandle("UIViewController");

-		private object __mt_ParentFocusEnvironment_var;
-
-		private object __mt_PreferredFocusedView_var;
-
-		private object __mt_WeakTransitioningDelegate_var;
-
 		public override IntPtr ClassHandle => class_ptr;

 		public virtual UIView View
@@ -3363,11 +3342,8 @@
 		protected override void Dispose(bool P_0)
 		{
 			base.Dispose(P_0);
-			if (base.Handle == IntPtr.Zero)
+			if (!(base.Handle == IntPtr.Zero))
 			{
-				__mt_ParentFocusEnvironment_var = null;
-				__mt_PreferredFocusedView_var = null;
-				__mt_WeakTransitioningDelegate_var = null;
 			}
 		}
 	}
@@ -3376,8 +3352,6 @@
 	{
 		private static readonly IntPtr class_ptr = Class.GetHandle("UIWindow");

-		private object __mt_WindowScene_var;
-
 		public override IntPtr ClassHandle => class_ptr;

 		public virtual UIViewController RootViewController
@@ -3411,9 +3385,8 @@
 		protected override void Dispose(bool P_0)
 		{
 			base.Dispose(P_0);
-			if (base.Handle == IntPtr.Zero)
+			if (!(base.Handle == IntPtr.Zero))
 			{
-				__mt_WindowScene_var = null;
 			}
 		}
 	}
```

* Do not consider bindings with `[Dispose (...)]` as optimizable

Injected code makes it impossible for `bgen` to decide if it's optimizable (or not)
Filed https://github.com/xamarin/xamarin-macios/issues/12150 with more details (and for other similar attributes)
This commit is contained in:
Sebastien Pouliot 2021-07-21 09:03:25 -04:00 коммит произвёл GitHub
Родитель f521537e0a
Коммит fccd43f0c2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 120 добавлений и 8 удалений

Просмотреть файл

@ -459,6 +459,7 @@
-->
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_LinkMode)' != 'None'" Type="Xamarin.Linker.Steps.PreserveBlockCodeHandler" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_LinkMode)' != 'None'" Type="Xamarin.Linker.OptimizeGeneratedCodeHandler" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_LinkMode)' != 'None'" Type="Xamarin.Linker.BackingFieldDelayHandler" />
<!-- MarkDispatcher substeps will run for all marked assemblies. -->
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_LinkMode)' != 'None'" Type="Xamarin.Linker.Steps.MarkDispatcher" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_LinkMode)' != 'None'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" />

Просмотреть файл

@ -3143,11 +3143,14 @@ public partial class Generator : IMemberGatherer {
// this attribute allows the linker to be more clever in removing unused code in bindings - without risking breaking user code
// only generate those for monotouch now since we can ensure they will be linked away before reaching the devices
public void GeneratedCode (StreamWriter sw, int tabs)
public void GeneratedCode (StreamWriter sw, int tabs, bool optimizable = true)
{
for (int i=0; i < tabs; i++)
sw.Write ('\t');
sw.WriteLine ("[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]");
sw.Write ("[BindingImpl (BindingImplOptions.GeneratedCode");
if (optimizable)
sw.Write (" | BindingImplOptions.Optimizable");
sw.WriteLine (")]");
}
static void WriteIsDirectBindingCondition (StreamWriter sw, ref int tabs, bool? is_direct_binding, string is_direct_binding_value, Func<string> trueCode, Func<string> falseCode)
@ -3184,9 +3187,9 @@ public partial class Generator : IMemberGatherer {
}
}
public void print_generated_code ()
public void print_generated_code (bool optimizable = true)
{
GeneratedCode (sw, indent);
GeneratedCode (sw, indent, optimizable);
}
public void print (string format)
@ -7422,7 +7425,7 @@ public partial class Generator : IMemberGatherer {
if (!is_static_class){
var disposeAttr = AttributeManager.GetCustomAttributes<DisposeAttribute> (type);
if (disposeAttr.Length > 0 || instance_fields_to_clear_on_dispose.Count > 0){
print_generated_code ();
print_generated_code (optimizable: disposeAttr.Length == 0);
print ("protected override void Dispose (bool disposing)");
print ("{");
indent++;

Просмотреть файл

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Linker;
using Mono.Linker.Steps;
using Xamarin.Tuner;
namespace Xamarin.Linker {
// Problems:
// * `Dispose` set the generated backing fields to `null` which means
// the linker will mark every backing fields, even if not used
// elsewhere (generally properties) inside the class
// * Backing fields increase the memory footprint of the managed peer
// instance (for the type and all it's subclasses)
// * Backing fields also increase the app size. Not a huge problem as
// they are all declared _weakly_ as `NSObject` but still...
//
// Solution:
// * When the linker process a `Dispose` method of an `NSObject`
// subclass with the _optimizable_ attribute then we remove the
// method body. This way the linker cannot mark the fields.
// * Before saving back the assemblies we replace the cached method
// body and NOP every field that were not marked by something else
// than the `Dispose` method.
public class BackingFieldDelayHandler : ConfigurationAwareMarkHandler {
protected override string Name { get; } = "Backing Fields Optimizer";
protected override int ErrorCode { get; } = 2400;
public override void Initialize (LinkContext context, MarkContext markContext)
{
base.Initialize (context);
markContext.RegisterMarkMethodAction (ProcessMethod);
}
// cache `Dispose` body of optimization NSObject subclasses
static Dictionary<MethodDefinition, MethodBody> dispose = new ();
protected override void Process (MethodDefinition method)
{
if (!method.HasParameters || !method.IsVirtual || !method.HasBody)
return;
if (method.Name != "Dispose")
return;
// only process methods that are marked as optimizable
if (!method.IsBindingImplOptimizableCode (LinkContext))
return;
var t = method.DeclaringType;
if (!t.IsNSObject (LinkContext))
return;
// keep original for later (if needed)
dispose.Add (method, method.Body);
// setting body to null will only cause it to be reloaded again
// same if we don't get a new IL processor
// and we do not want that (as it would mark the fields)
var body = new MethodBody (method);
var il = body.GetILProcessor ();
il.Emit (OpCodes.Ret);
method.Body = body;
}
public static void ReapplyDisposedFields (DerivedLinkContext context, string operation)
{
// note: all methods in the dictionary are marked (since they were added from an IMarkHandler)
foreach ((var method, var body) in dispose) {
foreach (var ins in body.Instructions) {
switch (ins.OpCode.OperandType) {
case OperandType.InlineField:
var field = (ins.Operand as FieldReference)?.Resolve ();
if (!context.Annotations.IsMarked (field)) {
var store_field = ins;
var load_null = ins.Previous;
var load_this = ins.Previous.Previous;
if (OptimizeGeneratedCodeHandler.ValidateInstruction (method, store_field, operation, Code.Stfld) &&
OptimizeGeneratedCodeHandler.ValidateInstruction (method, load_null, operation, Code.Ldnull) &&
OptimizeGeneratedCodeHandler.ValidateInstruction (method, load_this, operation, Code.Ldarg_0)) {
store_field.OpCode = OpCodes.Nop;
load_null.OpCode = OpCodes.Nop;
load_this.OpCode = OpCodes.Nop;
}
}
break;
}
}
method.Body = body;
}
}
}
public class BackingFieldReintroductionSubStep : ExceptionalSubStep {
public override SubStepTargets Targets => SubStepTargets.Assembly;
protected override string Name => "Backing Field Reintroduction";
protected override int ErrorCode { get; } = 2410;
public override void Initialize (LinkContext context)
{
base.Initialize (context);
BackingFieldDelayHandler.ReapplyDisposedFields (LinkContext, Name);
}
}
}

Просмотреть файл

@ -4,7 +4,10 @@ using Xamarin.Linker;
namespace Xamarin.Linker.Steps {
class PreOutputDispatcher : SubStepsDispatcher {
public PreOutputDispatcher ()
: base (new [] { new RemoveUserResourcesSubStep () })
: base (new BaseSubStep [] {
new RemoveUserResourcesSubStep (),
new BackingFieldReintroductionSubStep (),
})
{
}
}

Просмотреть файл

@ -218,7 +218,7 @@ namespace Xamarin.Linker {
ins.Operand = null;
}
protected static bool ValidateInstruction (MethodDefinition caller, Instruction ins, string operation, Code expected)
internal static bool ValidateInstruction (MethodDefinition caller, Instruction ins, string operation, Code expected)
{
if (ins.OpCode.Code != expected) {
Driver.Log (1, "Could not {0} in {1} at offset {2}, expected {3} got {4}", operation, caller, ins.Offset, expected, ins);
@ -228,7 +228,7 @@ namespace Xamarin.Linker {
return true;
}
protected static bool ValidateInstruction (MethodDefinition caller, Instruction ins, string operation, params Code [] expected)
internal static bool ValidateInstruction (MethodDefinition caller, Instruction ins, string operation, params Code [] expected)
{
foreach (var code in expected) {
if (ins.OpCode.Code == code)