-
Notifications
You must be signed in to change notification settings - Fork 565
[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 = nullWhen 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::ToStringwhich 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.Formatcalls 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
IsInOverridescheck should work or merely compareiMethod.NameandtMethod.Name. We don't seem to need thestring.Formatcalls 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.