-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Prefer interpreted execution for AsQueryable #79698
Conversation
Tagging subscribers to this area: @cston Issue DetailsAsQueryable converts an IEnumerable to an IQueryable, allowing an abstraction over queries regardless of whether they're executed locally via LINQ to Objects (IEnumerable) or translated via a LINQ provider. When the IEnumerable-based IQueryable is executed, we internally compile the expression tree and then invoke it; the delegate is then discarded (single-use). This practice is also being done in EF, and benchmarking it shows that interpretation is very significantly more performant than compilation followed by single use. This PR applies Note that when trimming and using CoreCLR (not NativeAOT), this may cause a size increase since the interpreter is brought in where it could be trimmed (since by default, /cc @jkotas @vitek-karas @ajcvickers
|
Linker is not smart enough for this - it would require us to figure out all method callsites before we can process a given method, which we currently don't do. It's been discussed several times as there are other cases where this might be beneficial, but so far we didn't really invest in this. |
@vitek-karas so you're saying that the interpreter is always preserved when there's a call to Compile(), regardless of the platform (CoreCLR vs. NativeAOT) or the parameter (preferInterpretation)? I did a quick test, and the interpreter seems to add around 3.9MB. I'm not sure there's reason to invest in this specifically: for NativeAOT the interpreter is obviously required for Compile() in any case (so the current logic is fine), so this would be about trimming those additional 3.9MB on CoreCLR when the interpreter isn't needed (i.e. when the parameterless overload is used, or when preferInterpretation is false). I'm not yet sure whether that's significant for EF Core specifically (we're not necessarily prioritizing maximal trimming in the non-NativeAOT scenario at this point). |
I looked into it and currently it is always preserved but for a different reason. runtime/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/LambdaExpression.cs Line 24 in ca873ea
private static readonly MethodInfo s_expressionCompileMethodInfo = typeof(Expression<>).GetMethod("Compile", Type.EmptyTypes)!; The problem is that trimming is currently not smart enough to recognize the Otherwise the current AsQueryable only calls So if we wanted to fix this right now (before this change) - we could by either improving the trimmer to be smarter around That said, after your change this is a moot discussion as it will always go through |
What might make sense is to add an |
I do not expect that this is going to be the case for long-running queries. Once you get queries that run for more 10's milliseconds, I expect that the compilation is going to be more performant. |
@jkotas right, it's a question of whether we expect most cases of AsQueryable queries to be complex/long or not; the current situation disadvantages short/simple ones, this change would shift it the other way. I can do a bit more benchmarking for various query complexities to see more or less where the top-off is, if that helps. |
It helps as a data point, but I do not think it will help us to get confidence in this change. I am sure that some projects have long-running queries out there. If we merge this change and somebody reports blocking performance regression when upgrading to .NET 8, what are we going to tell them? |
You're right of course.. The question is whether there's a use-case percentage where doing this change would be acceptable. For example, if 90% of uses would be optimized, would we accept it? 99%? Of course, if we don't want to regress perf for anyone - even if the vast majority of our users would benefit - we can abandon this. Note that in the benchmarks I did, even with 1000 nodes in the expression tree, interpretation is still significantly more efficient. |
Changing the default would need to be treated as a breaking change (get breaking change template filled, etc.). Also, we would need to have mitigation for the breaking change. It would likely mean to make this configurable or introduce new APIs that allow you to pass in the config so that people can switch to old behavior if needed.
Interpretation vs. compilation depends on the amount of data that the query processes. It does not depend on the amount of code in the query (modulo some noise). |
OK, I'll close this then - thanks for giving it your time. |
AsQueryable converts an IEnumerable to an IQueryable, allowing an abstraction over queries regardless of whether they're executed locally via LINQ to Objects (IEnumerable) or translated via a LINQ provider.
When the IEnumerable-based IQueryable is executed, we internally compile the expression tree and then invoke it; the delegate is then discarded (single-use). This practice is also being done in EF, and benchmarking it shows that interpretation is very significantly more performant than compilation followed by single use. This PR applies
preferInterpretation: true
for those cases.Note that when trimming and using CoreCLR (not NativeAOT), this may cause a size increase since the interpreter is brought in where it could be trimmed (since by default,
Compile()
on CoreCLR doesn't use the interpreter). I have no idea if the linker is that smart though (on NativeAOT this wouldn't change anything since the interpreter must be used anyway)./cc @jkotas @vitek-karas @ajcvickers