Open Neo-vortex opened 2 years ago
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.
This is possibly due to the code paths that ASP.NET Core's routing goes down on NativeAOT vs a JIT environment. In a JIT environment, ASP.NET Core uses runtime-compiled expression trees or JIT-compiled IL-emit, which can be quite fast. In an AOT environment, expression trees need to be interpreted and only traditional reflection can be used, not IL-emit, both of which are slower than their JIT equivalents.
cc: @davidfowl @dotnet/interop-contrib I believe this might relate to our earlier conversations about fast-reflection for ASP.NET Core.
@jkoritzinsky Can we source generate those code paths at compile time itself? I wish in .NET 7, the frameworks like aspnetcore take full advantage of source generators to address these kinda issues. That way we can solve problems for both JIT and AOT worlds! Am yet to see any issue which tracks source generator work for aspnetcore, but I hope that is on priority!
@jkoritzinsky Yep we should put this under a microscope. I have some ideas where the time is spent but it would be good to get some confirmation.
I believe once we integrate NativeAOT into our PerfLab infra (TE benchmarks, etc) with ability to get native traces and validate changes by sending new bits we will find low-hanging fruits there
We're spending a lot of time in the expression interpreter. The calls are coming from HostFilteringMiddleware.Invoke and HttpProtocol.ProcessRequests:
publishaot1!System_Linq_Expressions_System_Linq_Expressions_Interpreter_LightLambda__Run
publishaot1!S_P_CoreLib_System_Func_4__InvokeObjectArrayThunk
publishaot1!Microsoft_AspNetCore_HostFiltering_Microsoft_AspNetCore_HostFiltering_HostFilteringMiddleware__Invoke
publishaot1!Microsoft_AspNetCore_Server_Kestrel_Core_Microsoft_AspNetCore_Server_Kestrel_Core_Internal_Http_HttpProtocol__ProcessRequests_d__223_1__MoveNext
LINQ expressions won't have good perf with native AOT because we can't JIT them. Not much we can do from the runtime side.
MVC controller actions, minimal APIs, SignalR hub methods all have runtime generated thunks that use expression trees to quickly invoke methods. It might be worth having an alternative reflection invoke mode on NativeAOT. Here's one of the shared components used to invoke some of these generated thunks:
DI has a similar issue but we detect if dynamic code is supported and fallback to a reflection based strategy.
Maybe we can leave this open since we haven't invested in this as yet?
Sure we can keep this open. But do you expect the fix to be in this repo? EDIT: You just moved the issue as I was clicking comment
Looking at the ObjectMethodExecutor, the fix is basically (simplified):
public object? Execute(object target, object?[]? parameters)
{
if (LINQ is compiled)
{
Debug.Assert(_executor != null, "Sync execution is not supported.");
return _executor(target, parameters);
}
else
{
// New code
return MethodInfo.Invoke(target, parameters);
}
}
@MichalStrehovsky yes, something like that. Or possibly not even using the ObjectMethodExecutor
in some cases and using method info directly. I think it also make sense to track add NativeAOT variations to some of our benchmarks so we can observe these differences when we make the fixes.
Thanks for contacting us.
We're moving this issue to the .NET 7 Planning
milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.
Do we have stats on the full stack? It would seem reflection \ interpreted IL would be a factor, but not the only one for a 3x perf degradation.
FWIW during 5.0 I researched Blazor client perf around System.Text.Json and it turned out that interpreted IL in Mono was a bit faster than the native Mono runtime for reflection although that was a point-in-time measurement. Like expressions, System.Text.Json has both an IL.Emit and standard reflection approach depending on environment capabilities.
This is running the System.Linq.Expressions interpreter. NativeAOT doesn't have an IL interpreter like Mono has.
The IL interpreter that Mono has is pretty efficient compared to what the Expression interpreter is doing. The Mono interpreter doesn't interpret the IL directly, but converts it to an IR beforehand. So e.g. if you read a field with IL, the Mono interpreter bytecode will probably already have field offsets encoded in the IR instruction stream. Whereas reading a field with the Linq.Expression interpreter will always call FieldInfo.GetValue
because we don't have any more efficient reflection primitives that the expression interpreter could use.
We could make the Linq.Expression interpreter faster if we had better reflection primitives, but it will never be as fast as compiling the expressions into IL, and jitting them to native code (or jitting them to a more efficient IR that is runtime-specific).
Ohhh that sounds promising (more efficient expression tree interpreter 😄).
If your app or library needs fast expression trees execution, NativeAOT is not a good fit for it.
We can work on incremental perf improvements in expression tree interpreter. I do not expect it will move the needle enough. Also, major new work in expression tree interpreter is at odds with its archived status: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq.Expressions/README.md.
The code generation options are all have different tradeoffs:
Right now I'm thinking about a combination of generics and source generation to balance versioning (how much code exists in the app vs framework), but the generic constraints problem is a hard one to solve.
@davidfowl I remember us chatting early in .NET 7 about some ideas on how to improve the reflection primitives to reduce the maintainability burden of using generics. Maybe we'll be able to explore that route more in .NET 8?
@davidfowl I remember us chatting early in .NET 7 about some ideas on how to improve the reflection primitives to reduce the maintainability burden of using generics. Maybe we'll be able to explore that route more in .NET 8?
Yes I'd love to finish exploring that route and seeing where it leads. Basically dynamic call site generation APIs in the runtime that don't use ref emit.
@davidfowl this looks like a meta-issue tracking work on multiple teams, is it? I'm moving it to .NET 8 Planning milestone and will let you handle how it should be tracked from there.
.NET 8 makes sense and we should leave it here
A simple REST server as below, shows that NativeAOT is much slower than JIT version of the code.
Here is the the only controller in the app :
Here is Program.cs:
Here is Program.csproj
Here is Nuget.config:
JIT version runs on port
5247
NativeAOT version runs on port5000
Here is some benchmark results;
for JIT:
for NativeAOT
JIT version is build and run with
dotnet run
NativeAOT version is build withdotnet publish -r linux-x64 -c Release
dotnet :
6.0.101
OS :Linux pop-os 5.15.11-76051511-generic #202112220937~1640185481~21.10~b3a2c21 SMP Wed Dec 22 15:41:49 U x86_64 x86_64 x86_64 GNU/Linux
CPU :Intel Core i7 8700