-
Notifications
You must be signed in to change notification settings - Fork 4.9k
[release/3.1] Improve Expression.Lambda<TDelegate> performance (dotnet/runtime#32768) #42874
Conversation
3acc104
to
1a51f68
Compare
cc @vchirikov |
@Anipik I naively assumed that servicing something in netcoreapp ( |
Yes we didn't require versioning changes if the library is a part of shared framework. I thought we do this for consistency but I verified and we have never done this before. @cston Can you please remove the assemblyVersion update. sry for the mix up. |
@Anipik, I've reverted the changes to packageIndex.json and Directory.Build.props. |
@Anipik could you explain why this has "no merge" label? |
Because the branch is currently closed for any changes. I will remove the label and merge the change when the branch will be open for 3.1.4 |
Ok, thanks for the explanation. |
Test with EF and we'll hold this for a while to determine if there are more customers hitting this. |
@leecow Did you miss |
@leecow actually every project must be hit to one or another degree if only this project uses LINQ. using System.Linq;
namespace CompilerGeneratedExpressionLambda
{
public class DataItem
{
public long Id { get; set; }
public string Value { get; set; }
}
class Program
{
static void Main(string[] args)
{
var allItems = Enumerable.Empty<DataItem>().AsQueryable();
var filteredItems =
from item in allItems
where item.Value == args[0]
select new {
item.Id,
item.Value
};
}
}
} This sample contains just single and very simple LINQ query but even here compiler included two calls of
As we can see just a few hundred methods like this one in a project could currently lead to almost 100% of cache misses in current |
@vchirikov in our tactics meeting discussion, they discussed risk vs benefit. Changing code in this library has historically had a higher regression rate because it is complex. Certainly this change is localized, but the ask was that we give it more time in master (it will go out in 5.0 previews for example) unless evidence emerges that it's having a significant impact on a wider range of customers. Do you believe that we're misreading risk or value here? |
I think that your experienced programmers can predict risk of this fix. |
FWIW, the perf regression caused by the original PR was certainly not intended. The change in that PR wanted to ensure that all lambda node creations are subject to parameter count checks in order to pick optimized variants of the type for low parameter counts. The original code was always newing up the most expensive variant. To centralize the layout optimization, all code ended up getting routed through CreateLambda helpers, which started to hit the cache that was added before. The intersection of both changes led to the regression. As long as the new code goes through Create helpers, it will be fine and still have the layout optimization. |
I'd like to share workaround we use to overcome this issue. It might be potentially interesting for someone who experiences the same issue but who can't wait until 5.0 for resolution. using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace LambdaFactoryCacheResizer
{
class Program
{
static void Main(string[] args)
{
ResizeLambdaFactoryDelegateCache(262_144);
}
// A temp. fix for https://github.com/dotnet/corefx/pull/42874
static void ResizeLambdaFactoryDelegateCache(int newSize)
{
const BindingFlags anyStatic = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
const BindingFlags anyInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
var lambdaFactoriesFieldInfo = typeof(Expression).GetField("s_lambdaFactories", anyStatic);
var cacheType = lambdaFactoriesFieldInfo.FieldType;
var cacheTypeCtor = cacheType.GetConstructors(anyInstance)
.SingleOrDefault(c => {
var parameters = c.GetParameters();
return parameters.Length == 1 && parameters[0].ParameterType == typeof(int);
});
var cache = cacheTypeCtor.Invoke(new [] { (object) newSize } );
lambdaFactoriesFieldInfo.SetValue(null, cache);
}
}
} As you can see we just replace internal factory delegate cache with another much bigger instance.
Also I want to show by this comment what ugly hacks we have to use to still have other .Net Core 3.1 benefits. |
@bartdesmet Yes, I'm totally agree. |
There is always risk. |
It's really strange to hear about risk of deploying the 1-line bug fix that was approved, when you doesn't check the deployed LTS sdk package :) So, If we don't see this fix in 3.1.3, and in 3.1.4, when it will be available in 3.1 LTS release? Could you give us a rough estimate when it will be released? |
That's just further evidence that every change, big or small, has the potential to break someone in some way, whether due to the change itself or just the act of trying to make a fix.
This issue has been reported only by you so far. You have a workaround. As Lee wrote earlier, "we'll hold this for a while to determine if there are more customers hitting this." Thanks for your interest around this. |
@stephentoub you are right, we have a workaround and we'll survive. But what makes us unhappy is Misrosoft's decision to keep this pretty major thing as is just because no one else reported it. I believe you all can see this certain issue is hardly discoverable. First of all this internal cache for 64 elements makes inevitably failed all attempts to isolate issue by locally generating load to problematic method. Secondly all these LINQ queries in vast majority of cases are used to access a database but database access often leads to unexpected spikes. People could just spend time optimizing their DB access layer rather than paying attention to very low level method compiler automatically puts into their code. This is what we did. And those optimization even give some improvements! Just because in many cases it is possible to use lesser amount of LINQ expressions in code to achieve the same goals. Finally we decided to route all problematic read-only production load onto two subsets of machines in parallel. Those was .Net Core 3.1 and .Net Framework 4.7.2 versions of the same code. We took traces on both and then we did a comparison. We expected to see something inside We were lucky we had these two production ready versions for different platforms to compare otherwise I even don't know how to approach this. You can observe perfect performance when load is low then when load is medium you can observe higher CPU (+10% - +15%) all response times are elevated but nothing too bad. But when load went to high you can observe huge spikes (up to 100x time slower) in response times sporadically. We were really excited when this PR has been created. It was very unusual to see how fast Microsoft moves trying to make developers life better. But now we can feel only huge disappointment, I'm sorry saying this guys. PS. As one good friend of mine says, "Everyone has to suffer!". :) |
We take LTS releases very seriously, with a very high bar, and we have to weigh the very real chance of breaking one party in exchange for fixing an issue for someone else. The more "someone else"s we know a fix will help, the more that outweighs the downside of a potential break. The vast majority of fixes we put into the master branch are never even reviewed for a servicing branch. In this case, it was reviewed, and the answer wasn't "no", the answer was "we'll take our time and wait and see". I hope you can appreciate the conditions we have to weigh. Thanks, again. |
This position is totally understandable, but my point is different. |
@cston, this PR is there for a long time. Did you consider close it and target .net 5 which is not LTS or .net 6? |
I am closing this as I'm not aware of a plan to take this forward. |
Port of dotnet/runtime#32768
Fixes dotnet/runtime#32767
Description
An
Expression
lambda delegate cache was added in .NET Core 2.0. If there are many calls toExpression.Lambda<TDelegate>()
overloads with distinct delegate types which result in cache misses, the result is a performance regression.Customer Impact
Performance regression with many calls to
Expression.Lambda<TDelegate>()
with distinct delegate types. Customer reported this impact in a migration from .NET Framework to .NET Core.Packaging Impact
None
Regression
Regression from .NET Core 1.0, and .NET Framework.
Testing
See benchmark testing in dotnet/runtime#32767.
Risk
Low. The change avoids the cache and allocates a distinct
Expression<TDelegate>
each time.cc @ericstj