-
Notifications
You must be signed in to change notification settings - Fork 552
[linker] remove unneeded string.Format calls #4260
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] remove unneeded string.Format calls #4260
Conversation
43e1cdc
to
c5fc434
Compare
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
The short circuit and reduced memory usage is nice and all but...are we still doing too much? The point behind PR #861 and the above If that fails, then we fallback to...a whole mess of string work. Why do we need to do that whole mess of string work at all? Why do we need Why isn't (With an added point that method names being |
More on the "F# names don't match the C# convention", consider this F# code which implements the open System;
type MyServiceProvider() =
interface IServiceProvider with
member this.GetService(serviceType : Type): System.Object = null When using "Microsoft (R) F# Compiler version 10.2.3 for F# 4.5", the
That is, the method name which implements Nor should we! The IL explicitly states which interface method is implemented!
|
IIRC, the @jonathanpeppers do you have your new test case IL around? I think that might show that as the test case is going through the string comparison branch, right? |
...and this is the point where I start burbling like a looney. In fact, virtual methods overrides do not use class App {
public override string ToString ()
{
return "Hello, App!";
}
} disassembles (for // method line 2
.method public virtual hidebysig
instance default string ToString () cil managed
{
// Method begins at RVA 0x2061
// Code size 6 (0x6)
.maxstack 8
IL_0000: ldstr "Hello, App!"
IL_0005: ret
} // end of method App::ToString which in no way specifies what method it's overriding. This is why we need to fallback to signature-based matching: for 😭 |
|
||
static void CreateNestedInterface (string assemblyPath, AssemblyDefinition android) | ||
{ | ||
using (var assm = AssemblyDefinition.CreateAssembly (new AssemblyNameDefinition ("NestedIFaceTest", new Version ()), "NestedIFaceTest", ModuleKind.Dll)) { |
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.
Possibly "silly" question, but why are we using System.Reflection.Emit to create MyAssembly.dll
instead of using some C# code?
(Yes, FixAbstractMethodsStep_NestedTypes()
does that too, but now I wonder the same thing as well!)
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 tried using CodeDom... It seems like sacrificing too many chickens, it required me adding global alias for System.dll
:
ILMerge
puts CodeDom types in Xamarin.Android.Build.Tasks.dll
:
I didn't get to the point of how to "add" a new method to an interface of an existing assembly. I guess it would overwrite Mono.Android.dll
with a new one?
Using only Mono.Cecil it's easy to fabricate the types to make FixAbstractMethods
do its work.
Still, that doesn't quite explain why we need the previous The only instances -- that I know of -- where a method name would be Which means the "original" question is still unanswered:
An actual test case would be useful here. 😕 |
I was using the Mono profiler for our build, and I noticed: Allocation summary Bytes Count Average Type name 180765632 2142803 84 System.String 53218472 bytes from: MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethodsUnconditional (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.TypeDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameSignature (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) (wrapper managed-to-native) string:FastAllocateString (int) ~53MB of string from this one method is a lot! I found that one of these `string.Format` calls are running for almost every method on every interface in every assembly: (string.Format ("{0}.{1}", iface.FullName, iMethod.Name) != tMethod.Name) : (string.Format ("{0}.{1}.{2}", iMethod.DeclaringType.DeclaringType, iface.Name, iMethod.Name) != tMethod.Name)) Even the simplest non-matching cases would call `string.Format`: iMethod.Name == "Foo" tMethod.Name == "Bar" In all of these cases... * Class implements interface implicitly * Class implements interface explicitly * Class implements abstract class We found that the `IsInOverrides` check should work or merely compare `iMethod.Name` and `tMethod.Name`. We don't seem to need the `string.Format` calls at all? ~~ Results ~~ An initial build of the Xamarin.Forms integration project on Windows/.NET framework: Before: 796 ms LinkAssembliesNoShrink 1 calls After: 767 ms LinkAssembliesNoShrink 1 calls The same project on macOS/Mono (slightly slower machine, too): Before: 1341 ms LinkAssembliesNoShrink 1 calls After: 1025 ms LinkAssembliesNoShrink 1 calls String allocations are way better, too: Before: Allocation summary Bytes Count Average Type name 180765632 2142803 84 System.String After: Allocation summary Bytes Count Average Type name 127736832 1652070 77 System.String Shows 53,028,800 bytes saved. Before: 53218472 bytes from: MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethodsUnconditional (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.TypeDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameSignature (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) After: 1933624 bytes from: MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethodsUnconditional (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.TypeDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameSignature (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameMethodName (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) Shows 51,284,848 bytes saved. I don't know about the discrepancy between these two sets of numbers. But I think this is roughly ~50MB less string allocations.
c5fc434
to
83928ff
Compare
@jonpryor I think I did what we needed here:
I also experimented with using C# source code for these tests, such as: I encountered a lot of issues trying to make it work (beyond the hacks in java.interop). It might be worth revisiting later I think. |
Using the Mono profiler, I found: Allocation summary Bytes Count Average Type name 7636736 59662 128 System.Func<Mono.Cecil.TypeDefinition,System.Boolean> 3685952 57593 64 Java.Interop.Tools.Cecil.TypeDefinitionRocks.<GetTypeAndBaseTypes>d__3 The stack traces of these are from: 2498944 bytes from: Xamarin.Android.Tasks.GenerateJavaStubs:Run (Java.Interop.Tools.Cecil.DirectoryAssemblyResolver) Xamarin.Android.Tasks.ManifestDocument:Merge (Microsoft.Build.Utilities.TaskLoggingHelper,Java.Interop.Tools.Cecil.TypeDefinitionCache,System.Collections.Generic.List`1<Mono.Cecil.TypeDefinition>,string,bool,string,System.Collections.Generic.IEnumerable`1<string>) Xamarin.Android.Tasks.ManifestDocument:GetGenerator (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) (wrapper alloc) object:ProfilerAllocSmall (intptr,intptr) (wrapper managed-to-native) object:__icall_wrapper_mono_profiler_raise_gc_allocation (object) ... 1273344 bytes from: Xamarin.Android.Tasks.ManifestDocument:Merge (Microsoft.Build.Utilities.TaskLoggingHelper,Java.Interop.Tools.Cecil.TypeDefinitionCache,System.Collections.Generic.List`1<Mono.Cecil.TypeDefinition>,string,bool,string,System.Collections.Generic.IEnumerable`1<string>) Xamarin.Android.Tasks.ManifestDocument:GetGenerator (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:GetTypeAndBaseTypes (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) (wrapper alloc) object:ProfilerAllocSmall (intptr,intptr) (wrapper managed-to-native) object:__icall_wrapper_mono_profiler_raise_gc_allocation (object) This `IsSubclassOf` method gets called ~40K times during a build: Method call summary Total(ms) Self(ms) Calls Method name 2431 117 43493 Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) Reviewing `IsSubclassOf` we can just remove the System.Linq usage and use a `foreach` loop instead. The results of building the Xamarin.Forms integration project on Windows: Before: 559 ms GenerateJavaStubs 1 calls After: 535 ms GenerateJavaStubs 1 calls A ~24ms savings is pretty good for a small app. I suspect it would have even better improvements on macOS / Mono, due to what I saw in: dotnet/android#4260
I was using the Mono profiler for our build, and I noticed: Allocation summary Bytes Count Average Type name 180765632 2142803 84 System.String 53218472 bytes from: MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethodsUnconditional (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.TypeDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameSignature (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) (wrapper managed-to-native) string:FastAllocateString (int) ~53MB of string from this one method is a lot! I found that one of these `string.Format()` calls are running for almost every method on every interface in every assembly: ? (string.Format ("{0}.{1}", iface.FullName, iMethod.Name) != tMethod.Name) : (string.Format ("{0}.{1}.{2}", iMethod.DeclaringType.DeclaringType, iface.Name, iMethod.Name) != tMethod.Name)) Even the simplest non-matching cases would call `string.Format()`: iMethod.Name == "Foo" tMethod.Name == "Bar" In all of these cases... * Class implements interface implicitly, or * Class implements interface explicitly, or * Class implements abstract class We found that either the `IsInOverrides()` check should work or comparing `iMethod.Name` and `tMethod.Name` should work. We don't seem to need the `string.Format()` calls at all? ~~ Results ~~ An initial build of the Xamarin.Forms integration project on Windows/.NET framework saves ~30ms: * Before: 796 ms LinkAssembliesNoShrink 1 calls * After: 767 ms LinkAssembliesNoShrink 1 calls The same project on macOS/Mono (slightly slower machine, too) saves ~316ms: * Before: 1341 ms LinkAssembliesNoShrink 1 calls * After: 1025 ms LinkAssembliesNoShrink 1 calls String allocations are way better, too: * Before: Allocation summary Bytes Count Average Type name 180765632 2142803 84 System.String * After: Allocation summary Bytes Count Average Type name 127736832 1652070 77 System.String Shows 53,028,800 bytes saved. * Before: 53218472 bytes from: MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethodsUnconditional (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.TypeDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameSignature (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) * After: 1933624 bytes from: MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethodsUnconditional (Mono.Cecil.AssemblyDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:FixAbstractMethods (Mono.Cecil.TypeDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameSignature (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) MonoDroid.Tuner.FixAbstractMethodsStep:HaveSameMethodName (Mono.Cecil.TypeReference,Mono.Cecil.MethodDefinition,Mono.Cecil.MethodDefinition) Shows 51,284,848 bytes saved. I don't know about the discrepancy between these two sets of numbers. But I think this is roughly ~50MB less string allocations.
Using the Mono profiler, I found: Allocation summary Bytes Count Average Type name 7636736 59662 128 System.Func<Mono.Cecil.TypeDefinition,System.Boolean> 3685952 57593 64 Java.Interop.Tools.Cecil.TypeDefinitionRocks.<GetTypeAndBaseTypes>d__3 The stack traces of these are from: 2498944 bytes from: Xamarin.Android.Tasks.GenerateJavaStubs:Run (Java.Interop.Tools.Cecil.DirectoryAssemblyResolver) Xamarin.Android.Tasks.ManifestDocument:Merge (Microsoft.Build.Utilities.TaskLoggingHelper,Java.Interop.Tools.Cecil.TypeDefinitionCache,System.Collections.Generic.List`1<Mono.Cecil.TypeDefinition>,string,bool,string,System.Collections.Generic.IEnumerable`1<string>) Xamarin.Android.Tasks.ManifestDocument:GetGenerator (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) (wrapper alloc) object:ProfilerAllocSmall (intptr,intptr) (wrapper managed-to-native) object:__icall_wrapper_mono_profiler_raise_gc_allocation (object) ... 1273344 bytes from: Xamarin.Android.Tasks.ManifestDocument:Merge (Microsoft.Build.Utilities.TaskLoggingHelper,Java.Interop.Tools.Cecil.TypeDefinitionCache,System.Collections.Generic.List`1<Mono.Cecil.TypeDefinition>,string,bool,string,System.Collections.Generic.IEnumerable`1<string>) Xamarin.Android.Tasks.ManifestDocument:GetGenerator (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:GetTypeAndBaseTypes (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) (wrapper alloc) object:ProfilerAllocSmall (intptr,intptr) (wrapper managed-to-native) object:__icall_wrapper_mono_profiler_raise_gc_allocation (object) This `TypeDefinitionRocks.IsSubclassOf()` method gets called ~40K times during a build: Method call summary Total(ms) Self(ms) Calls Method name 2431 117 43493 Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) Reviewing `IsSubclassOf()` we can just remove the System.Linq usage and use a `foreach` loop instead. The results of building the Xamarin.Forms integration project on Windows: * Before: 559 ms GenerateJavaStubs 1 calls * After: 535 ms GenerateJavaStubs 1 calls A ~24ms savings is pretty good for a small app. I suspect it would have even better improvements on macOS / Mono, due to what I saw in: dotnet/android#4260
Using the Mono profiler, I found: Allocation summary Bytes Count Average Type name 7636736 59662 128 System.Func<Mono.Cecil.TypeDefinition,System.Boolean> 3685952 57593 64 Java.Interop.Tools.Cecil.TypeDefinitionRocks.<GetTypeAndBaseTypes>d__3 The stack traces of these are from: 2498944 bytes from: Xamarin.Android.Tasks.GenerateJavaStubs:Run (Java.Interop.Tools.Cecil.DirectoryAssemblyResolver) Xamarin.Android.Tasks.ManifestDocument:Merge (Microsoft.Build.Utilities.TaskLoggingHelper,Java.Interop.Tools.Cecil.TypeDefinitionCache,System.Collections.Generic.List`1<Mono.Cecil.TypeDefinition>,string,bool,string,System.Collections.Generic.IEnumerable`1<string>) Xamarin.Android.Tasks.ManifestDocument:GetGenerator (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) (wrapper alloc) object:ProfilerAllocSmall (intptr,intptr) (wrapper managed-to-native) object:__icall_wrapper_mono_profiler_raise_gc_allocation (object) ... 1273344 bytes from: Xamarin.Android.Tasks.ManifestDocument:Merge (Microsoft.Build.Utilities.TaskLoggingHelper,Java.Interop.Tools.Cecil.TypeDefinitionCache,System.Collections.Generic.List`1<Mono.Cecil.TypeDefinition>,string,bool,string,System.Collections.Generic.IEnumerable`1<string>) Xamarin.Android.Tasks.ManifestDocument:GetGenerator (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) Java.Interop.Tools.Cecil.TypeDefinitionRocks:GetTypeAndBaseTypes (Mono.Cecil.TypeDefinition,Java.Interop.Tools.Cecil.TypeDefinitionCache) (wrapper alloc) object:ProfilerAllocSmall (intptr,intptr) (wrapper managed-to-native) object:__icall_wrapper_mono_profiler_raise_gc_allocation (object) This `TypeDefinitionRocks.IsSubclassOf()` method gets called ~40K times during a build: Method call summary Total(ms) Self(ms) Calls Method name 2431 117 43493 Java.Interop.Tools.Cecil.TypeDefinitionRocks:IsSubclassOf (Mono.Cecil.TypeDefinition,string,Java.Interop.Tools.Cecil.TypeDefinitionCache) Reviewing `IsSubclassOf()` we can just remove the System.Linq usage and use a `foreach` loop instead. The results of building the Xamarin.Forms integration project on Windows: * Before: 559 ms GenerateJavaStubs 1 calls * After: 535 ms GenerateJavaStubs 1 calls A ~24ms savings is pretty good for a small app. I suspect it would have even better improvements on macOS / Mono, due to what I saw in: dotnet/android#4260
I was using the Mono profiler for our build, and I noticed:
~53MB of string from this one method is a lot!
I found that one of these
string.Format
calls are running for almostevery method on every interface in every assembly:
Even the simplest non-matching cases would call
string.Format
:In all of these cases...
We found that the
IsInOverrides
check should work or merely compareiMethod.Name
andtMethod.Name
. We don't seem to need thestring.Format
calls at all?Results
An initial build of the Xamarin.Forms integration project on
Windows/.NET framework:
The same project on macOS/Mono (slightly slower machine, too):
String allocations are way better, too:
Shows 53,028,800 bytes saved.
Shows 51,284,848 bytes saved.
I don't know about the discrepancy between these two sets of numbers.
But I think this is roughly ~50MB less string allocations.