-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
[API Proposal]: What's the future of dynamic code execution, AOT, JIT and interpreter in .NET-based mobile apps? #101466
Comments
The answers are not official.
The term is confusable. For current .NET there are two AOT methods, one is Mono AOT, the other is CoreCLR NativeAOT which evolved from .NET Native for UWP. Mono AOT is used for mobile platforms. Support for CoreCLR NativeAOT on mobile platforms is experimental.
There are actually a lot of effort to make AOT compilation correct by default. The users get warned for anything that can't work automatically.
It can't be answered without asking "which AOT". For Mono AOT, the interpreter can already be enabled, which is the only solution for iOS-like platforms.
Statically analyzable reflection is known by AOT toolchain and just works. It's useful for some cases like layering issue. Definitely less powerful though.
There're actually different levels to take the optimization. For example, you can enable trimming to remove unused code solely. The benefit for AOT can be in other areas:
It's also worth noting that maximum steady-state performance of AOT is worse than JIT. JIT can bake a lot of things into code, like memory addresses and one-time initialized configurations.
This is somehow strange and doesn't meet the expectations. "AOT not found" should not happen for statically analyzable application.
It already exists since .NET Core 3.0. We call it ReadyToRun. The pre-JITed code is put together into the assembly files. |
I'm not sure with long-term goal of Mono AOT. We are mostly reusing the effort done in the past. Many of the efforts are for CoreCLR NativeAOT. You can read documentation at https://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run and https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot for desktop environment. |
Thanks for the feedback!
I understand this request. Thus far we have not clearly delineated between what we see as "fundamental" limitations, and point-in-time limitations. The docs currently have a section called Limitations of Native AOT. We currently think of those limitations as being fundamental. It is unlikely that any of those restrictions will ever be lifted. I will try to make that clear in the documentation. Regarding the spectrum of AOT options that we provide and long-term thinking, I would categorize the approach as "fast, small, and limited" versus "less fast, larger, and full-featured." "Fast" here is in reference to the speed in the most restrictive configuration, meaning an environment without JITing. To detail each of the options you mentioned,
Mono full AOT. Fast and limited.
Full-featured, in-between performance. Ideally faster than the full-featured configuration, but dependent on the profile. Slow when the profile misses.
CoreCLR full AOT. Fast and limited.
An implementation of the full-featured runtime. Fast if JIT is allowed, slow if interpretation is required. An important feature that was not mentioned in any of the above options is the level of trimming. Full AOT apps require trimming and it is the source of both a lot of the performance improvements, and most of the incompatibility. Code generation capability is not meaningful if the needed code was removed from the application, and achieving the highest performance and lowest disk size will require removing excess code. To go into some more details
This is not true -- I've now annotated quite a lot of applications and many of them use reflection without dynamic code generation. Spectre.Console and Serilog, for example, had almost no dynamic code generation.
We believe that Native AOT can get much smaller and faster. I wouldn't use current numbers as indicators of potential improvements. That said, we also believe that there is a tradeoff here and people will be on either side.
We currently have a lot of deployment options and I think we will retain a large number of them, maybe swapping around implementations for performance. Over time we've been converging, rather than diverging, in implementation strategies. For example, Native AOT reuses large parts of the CoreCLR surface, including the JIT and GC, and builds on the same foundation as "crossgen."
I think this is mixed. Some people have provided feedback that they want a very small deployment size, while some people prioritize application startup on slow mobile devices. It seems likely that different people will prioritize different implementations. For rough metrics, I think you could easily see an order of magnitude different metrics for .NET form-factors on opposite ends of the spectrum -- so startup time with the limited form factor might be 10x better than the fullest-featured form factor. |
Hi, thanks for the responses. Some corrections:
Yes, ".NET Native" = "CoreCLR NativeAOT" in my post. I actually tried to figure out what's the right term now, but failed :)
The detailed description of that issue explains it's not only about full AOT, but also about profile-guided AOT - i.e. there are missing methods in both cases, and moreover, some of them appear in stats from Also,
Yes, I know about this. But it doesn't exist on MAUI for Android, for example, where it could be quite handy.
"Almost no" just supports my point here - i.e. they do use dynamic code generation. I'd say reflection w/o code execution (just to clarify, I assume property read/write is an example of such code execution, as well as other method calls) is mostly about attribute inspection. Which is handy, but way less handy than an ability to e.g. set property values, call methods and constructors. I can name a decent number of scenarios where delegate caching helps to get rid of boxing. E.g. Blazor uses this technique to component implement property comparison & writes. In maybe 90% of cases you don't even need -- Ok, I guess now it's time to rephrase my question a bit:
For the note, I care way less about |
That's true as well. If we name two edge cases, they are:
And there is a lot of options in between - e.g. WASM scenarios, or a code running in some form of container on edge servers. And it's also a lot about the app itself - I totally understand it's frustrating to see a few MB of artifacts produced for a tiny app. But my question goes more along this line: am I right that there is no long-term vision that renders profile-guided AOT & dynamic code execution obsolete? I'm asking it mostly because this is a huge downside for apps like the one we work on (likely, for any medium+ app, btw), and if that's the case, we'd certainly prefer to know this in advance. |
Yeah, I didn't mention trimming, but that's mostly because I assume it's a must-have pass :) + AFAIK right now it doesn't interact with AOT, i.e. the IL trimming happens before AOT codegen. |
Let me rephrase -- there was no dynamic code execution used in the core of those libraries. And no, property read/write is not an example of code execution. That is simply reflection. Code execution are things like
Definitely possible that you could see this in the future.
Unlikely. In our benchmarks with CoreCLR + R2R vs. Native AOT, we see very large wins in startup in full AOT that cannot be replicated by R2R. The problem is that the mere possibility of seeing non-AOTed code forces you to keep around a whole runtime that supports dynamic loading, and that incurs cost. Moreover, if your app never actually uses the interpreter fallback, there isn't much point in having it in the first place. Most apps that require the interpreter will end up using it and that will impact their performance. That said, some people value compatibility over performance, and that's fine! It seems likely that we will end up supporting configurations in all of these areas:
So the short answer to your question is: no, we don't have plans to obsolete everything except full AOT. The most likely scenario is one where full AOT is just one of a few options, and we continue to improve those other options as well. |
FYI, not true for Native AOT. In fact, one of the advantages of Native AOT is that it can trim more aggressively because it does trimming in conjunction with IL compilation. |
Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas |
Closing question as answered. |
Background and motivation
Currently there are following execution modes in MAUI:
Some of them mix together - e.g. JIT and interpreter can be used with any AOT mode. Each of these options has its pros and cons:
ILogger<T>
scenario withIServiceProvider
, which is quite common). There is no good way to automatically identify all possible generic instances in advance in any of such cases, so it's a choice between having full AOT + interpreter/JIT, or a full AOT which bans all dynamic invocations, including such things as invoking a constructor of a type, or creating a delegate for a specific generic method instance. In other words, it's a huge disadvantage, which kills a whole range of features we love .NET for. Lots of libraries (including Blazor) rely on reflection-generated delegate caching to speed up or even implement certain generic logic.IMO profile-guided AOT doesn't have any significant cons - except the fact its current implementation is definitely not perfect. E.g. we see a huge number of "AOT NOT FOUND" entries in Mono's debug output, and nearly all of them are also mentioned in AOT profile we use - in other words, maybe 50% of our startup code is still JITted. But I assume this can be addressed.
JIT alone is definitely not a good option for mobile apps. Nearly any non-toy app would require some form of AOT for at least startup portion of its code.
Interpreter, albeit being fairly slow, is still a good choice for many apps. Moreover, it's the only option you can use on iOS.
API Proposal
1. It's frustrating to see Microsoft invests a lot into .NET Native without clarifying what's the end goal there:
To clarify, all these items "go together", because they are absolutely needed to power the first one:
IServiceProvider
& similar scenarios), profile-guided AOT or its alternatives (I'll write more on this further) is absolutely necessary to speed up the startup process.Dynamic code execution is one of features which makes .NET so attractive. Yes, it's mostly invisible for regular developers, but if you look at the library code... Just look at this list and ask yourself, which of the top ones don't heavily rely on it: https://www.nuget.org/packages
It also worth mentioning that Reflection is almost useless w/o dynamic code execution - again, ask yourself, what would you use it for, if you can't invoke whatever you inspect.
2. I don't see a single reason to prefer full AOT (think .NET Native) vs profile-guided AOT for nearly any non-toy mobile app or desktop app.
Yes, some .NET Native example are nice, but the amount of code there is tiny (compared to what you have in real apps). I understand there are some cases where this option seems to be preferrable - e.g. AWS lambda and Azure Functions scenarios, but even these are hard to justify for me (e.g. if such a function runs just for 1 minute & its small enough, AOT may save less than 0.1% of CPU cost vs a case when it's simply JITted).
As for iOS, not only our full AOT builds for iOS don't work without an interpreter, but their .ipa size is shockingly huge: ~ 200MB+ vs ~ 27MB for "no AOT, interpreter-only" mode. So right now we stick to the second option (interpreter on iOS seem to work much faster than on Android).
--
As you can see, it's not exactly an API proposal, but more an ask to clarify what's Microsoft stance on future of AOT, JIT, and dynamic code execution.
If you read everything until this point, you may also notice that:
API Usage
Since it's not about the API, there is no example.
Alternative Designs
A good alternative to profile-guided AOT is an ability to cache JIT output (in app data / app-specific files / right along the app's executable).
I think it might be a great fit for most of Android & UWP apps - i.e. it's typically fine to start slower, if it happens just once (or once per every version). And no profiling data is required in this case; as for AWS Lambdas / Azure Functions, all you need is to run the app once before the deployment - to produce its JIT cache.
On a downside, this option won't work on iOS.
Risks
No response
The text was updated successfully, but these errors were encountered: