-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
[android] .NET 8 performance regression in System.Reflection.MethodInfo/ConstructorInfo.Invoke() #83893
Comments
Tagging subscribers to 'arch-android': @steveisok, @akoeplinger Issue DetailsDescription.NET 7 added a code path that uses System.Reflection.Emit for ConstructorInfo/MethodInfo for coreclr. This makes sense for throughput, scenarios like ASP.NET. On mobile, however, we are more concerned about a fast startup. I found that we may have accidentally introduced this behavior for Mono (or at least Android) in .NET 8: In .NET MAUI apps that heavily use things like data-binding, Microsoft.Extensions.DI, etc. It appears that this change noticeably slows down startup. In my benchmark, I setup a "first run" / "cold start" scenario:
/cc @steveisok Reproduction StepsTry my instructions here: https://github.com/jonathanpeppers/BenchmarkDotNet-Android/tree/System.Reflection Expected behaviorWe probably shouldn't hit the S.R.E codepath by default on Android. We should have a flag to opt into it? It may be useful for some apps, although it appears to slow down startup in the apps I've tried. Actual behaviorWe hit the S.R.E codepath by default on Android. Regression?Yes, it appears .NET 7 does not have this issue. Known WorkaroundsUse .NET 7? Configuration.NET SDK: 8.0.100-preview.3.23170.5 Other informationNo response
|
@lambdageek what are your thoughts? |
Maybe we can have a heuristic: if SRE classes like DynamicMethod are already loaded and initialized for some other purpose, use the SRE invoke path. otherwise go via the runtime. We could add a static flag somewhere in the invoke code and toggle it from the static cctor for ILGenerator, for example. |
@SamMonoRT can someone from your team take a look and maybe prototype @lambdageek's idea? |
Should these somehow use the existing feature switches?
https://github.com/dotnet/runtime/blob/main/docs/workflow/trimming/feature-switches.md In theory, an MSBuild property would let users be able to opt into |
@ivanpovazan - can take a look sometime next week. |
These switches are used already. They disable the SRE codepath when we're on the interpreter or on fullaot, etc. Don't we want the SRE path in the steady state on platforms where we have a JIT? I assume the issue on Android is just that at startup we don't want to bother with loading SRE support for one-off callbacks. But presumably once the app is running, we want the SRE codepath? I'm assuming the SRE codepath is faster in the steady state (I think we have microbenchmarks to back that up, right @SamMonoRT ?)
I don't think this should be a user-visible switch - we should figure out how Android could have fast startup and fast steady state. Or if SRE is never faster for mono, then we should disable the SRE Invoke machinery. Messing with |
The SRE optimization is not used for one-off callbacks. It should be only used if the method is called at least twice. Maybe you want to bump this threshold to be more than 2 on Mono? |
I am currently working on experimental setup to profile how many times each method is called during startup, to determine if it possible to use a different strategy with SRE optimization. Will provide an update once I gather some data. |
I might be doing something wrong, but I was not able to reproduce the effect of the startup performance regression by using the provided benchmarks.
ObservationBy running the benchmarks multiple times and recording the outputs, the results showed oscillations between the runs.
The full benchmark logs are available here: From the provided results I was not able to confirm that the regression was introduced. @jonathanpeppers Am I maybe missing some configuration parameter or using a different build version? Further analysisEven though I was not able to reproduce the regression I have experimented with how to collect stats on dynamically invoked constructors and methods. On an experimental branch, I have extended
|
@ivanpovazan let's discuss in Teams when you have a chance. We first found new SRE calls in .NET 8 here: dotnet/android@30e9487#diff-dd3a2df0f1e5bd8fc55250ba096d4ee06782c3c9544dfdffbb30885ef5ea29caR2333 I updated the Maybe the issue isn't |
I have measured the MAUI android template app startup time with and without hitting the SRE path as described here. ResultsDotnet version:
NOTE: There is a regression of I am providing the repro steps bellow and the script I was using to measure this. ReproPrerequistes
Additional info
@jonathanpeppers could you give it a go on your end and check the results, as from what I measured it does not seem that SRE optimisation caused a regression. |
The ~5.3ms (maybe regression) might scale a lot worse when you go from a project template to a customer's real app. It appears that Java/C# interop can easily cause Can |
Yes, it is possible to add this to the project file for example: <ItemGroup Condition="'$(noSRE)' == 'true'">
<RuntimeHostConfigurationOption Include="Switch.System.Reflection.ForceInterpretedInvoke" Value="true" />
</ItemGroup> and the pass |
…nterpretedInvoke Fixes: dotnet/runtime#83893 In .NET 8, `System.Reflection.ConstructorInfo/MethodInfo.Invoke()` will call `System.Reflection.Emit` when called more than once. This impacts startup in mobile applications, so it may not be a desired feature. Unfortunately, this appears to happen quite easily in Android apps, some examples: * https://gist.github.com/ivanpovazan/2563ea9d2fea320e6425cfcc58da3ee5 * https://gist.github.com/ivanpovazan/d2546d4abad17900d4366cc29e1689b2 The types of situations this happens: * You call a Java method from C# that returns a `Java.Lang.Object` subclass. * You override a Java method in C#, that has a `Java.Lang.Object` parameter. To solve this problem, we can set: <ItemGroup> <RuntimeHostConfigurationOption Include="Switch.System.Reflection.ForceInterpretedInvoke" Value="$(_SystemReflectionForceInterpretedInvoke)" Trim="true" /> </ItemGroup> And we can set `$(_SystemReflectionForceInterpretedInvoke)` to test out the setting in various apps. I also updated the `.aotprofile` to verify that all `System.Reflection.Emit` code paths disappear from `dotnet new android` applications.
…nterpretedInvoke Fixes: dotnet/runtime#83893 In .NET 8, `System.Reflection.ConstructorInfo/MethodInfo.Invoke()` will call `System.Reflection.Emit` when called more than once. This impacts startup in mobile applications, so it may not be a desired feature. Unfortunately, this appears to happen quite easily in Android apps, some examples: * https://gist.github.com/ivanpovazan/2563ea9d2fea320e6425cfcc58da3ee5 * https://gist.github.com/ivanpovazan/d2546d4abad17900d4366cc29e1689b2 The types of situations this happens: * You call a Java method from C# that returns a `Java.Lang.Object` subclass. * You override a Java method in C#, that has a `Java.Lang.Object` parameter. To solve this problem, we can set: <ItemGroup> <RuntimeHostConfigurationOption Include="Switch.System.Reflection.ForceInterpretedInvoke" Value="$(_SystemReflectionForceInterpretedInvoke)" Trim="true" /> </ItemGroup> And we can set `$(_SystemReflectionForceInterpretedInvoke)` to test out the setting in various apps. I added a test to verify the "private" switch is actually set. I also updated the `.aotprofile` to verify that all `System.Reflection.Emit` code paths disappear from `dotnet new android` applications.
Description
.NET 7 added a code path that uses System.Reflection.Emit for ConstructorInfo/MethodInfo for coreclr. This makes sense for throughput, scenarios like ASP.NET.
On mobile, however, we are more concerned about a fast startup. I found that we may have accidentally introduced this behavior for Mono (or at least Android) in .NET 8:
#72717
In .NET MAUI apps that heavily use things like data-binding, Microsoft.Extensions.DI, etc. It appears that this change noticeably slows down startup.
In my benchmark, I setup a "first run" / "cold start" scenario:
/cc @steveisok
Reproduction Steps
Try my instructions here: https://github.com/jonathanpeppers/BenchmarkDotNet-Android/tree/System.Reflection
Expected behavior
We probably shouldn't hit the S.R.E codepath by default on Android.
We should have a flag to opt into it? It may be useful for some apps, although it appears to slow down startup in the apps I've tried.
Actual behavior
We hit the S.R.E codepath by default on Android.
Regression?
Yes, it appears .NET 7 does not have this issue.
Known Workarounds
Use .NET 7?
Configuration
.NET SDK: 8.0.100-preview.3.23170.5
Other information
No response
The text was updated successfully, but these errors were encountered: