-
Notifications
You must be signed in to change notification settings - Fork 350
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
Memory Usage - Lambda closures in hot paths cause excessive allocations #2192
Comments
Thanks Clement, and thanks for adding these details. |
I was also intrigued why compiler is not optimizing this, the same way when we explicitly use the lambda. Obviously, I was expecting they had a reason to not have done it yet.
I think they will fix this eventually at the compiler, but perhaps not very soon because it is low priority for the reasons they state above,. Until then their recommendation is to use static code analyzers to detect and apply the workaround, which is what we are doing in AGS. |
We have more cases of closures causing excessive allocation in AGS. These are cases where we use a lambda expression that references a variable from the enclosing scope. This causes the compiler to create a new object (
DisplayClass
) that will store the referenced variables so that the lambda can still access them when it's called. In addition, the generatedFunc
orAction
is not cached, so a newFunc
orAction
is created each time.We can optimize these lambdas refactoring them such that all the variables that they need be passed as arguments. In the case of LINQ methods like
IEnumerable.Any()
we could consider using plain for loops.There's also another subtle source of "surprise" allocations when passing a static method as an argument to method expecting a func (or action).
Consider the following scenario:
Passing
Helpers.DoStuff
directly toHelpers.CallFilter
causes anew Func<bool>(Helpers.DoStuff)
to be created each time as you can see hereBut when you wrap the static method into a lambda as follows:
Then a new
Func
is created only once and cached as demonstrated here. I find this to be quite intriguing, I'm not sure why the compiler doesn't cache the Func when the static method is passed directly.Here are some of the "hot" spots for closure allocations:
WriterValidationUtils.ValidatePropertyDerivedTypeConstraint(PropertySerializationInfo)
DerivedTypeConstraints.Any(d => d == fullTypeName)
referencesfullTypeName
which is defined in the outer scopeEdm.ExtensionMethods.ReplaceAlias
FirstOrDefault
referencestypeAlias
which is defined outside the lambda.WriterValidationUtils.ValidateDerivedTypeConstraint
derivedTypeConstraints.Any
referencesfullTypeName
defined in outer scopeSelectedPropertiesNode.GetSelectedPropertiesForNavigationProperty
this.children.Any()
referencesnavigationPropertyName
defined in the outer scopeEdm.ExtensionMethods.FindType
RegistrationHelper.CreateAmbiguousTypeBinding
passed as the last argument toFindAcrossModels
causes a newFunc
to be created each time(first, second) => RegistrationHelper.CreateAmbiguousTypeBinding(first, second)
ODataWriterCore.CheckForNestedResourceInfoWithContent
this.InterceptException
referencescurrentNestedResourceInfo
which is defined in the outer scopethisParam.currentScope.Item
About that last issue, such allocations were fixed in this PR but I think this was inadvertently missed, since it's a quick fix. I don't have the figures for this in AGS since the PR that addressed most of such issues has not yet been deployed.
AGS:
Local profiler:
Assemblies affected
Microsoft.OData.Core 7.9.1
Reproduce steps
Create an app with an endpoint that uses the ODataWriter to write a resource set response with a large number of entities (eg. this experiment). Run a memory profiler against your app (e.g. the VS .NET Object Allocator Tracking Tool) then execute the endpoint with the profiler running, and check the allocation data compiled by the profiler during the request.
Expected result
Less number of allocations from closures
Actual result
A lot of allocations from closures
@joaocpaiva @Sreejithpin @mikepizzo @gathogojr
The text was updated successfully, but these errors were encountered: