-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Pay for an allocation/assignments of a capture class regardless of whether it's used #20777
Comments
I noticed that the code gen basically treats the capture class's field as if it was the parameter - if I use parameter elsewhere, then the compiler gens that as a read of the field from the capture class. Based on that, this looks very deliberate code gen and probably not overlooked. |
Correct. This behavior is deliberate. The capture logic is entirely scope based and as an optimization it merges empty capture scopes into scopes which capture actual variables. In this case the scope which allocations the delegate has no captured variables and is hence "empty". The compiler optimizes the situation by merging the captures into a single scope, the one where That being said, this is a frequent source of confusion amongst users. Additionally the logic around merging scopes was also done when we had a very different pattern for allocating closure types. It made a lot more sense in that context, possibly less in the current one. @agocke is looking at this as a possible source of optimization in 15.5 when we are revisiting our capture code whole sale. Will let him dupe this against any bugs he may already have filed here. |
CC @khyperia as an FYI as well |
Not only that it causes possible unnecessary allocations, it also causes errors during debugging which caused me more confusion than i'd like to admit, up to the point thinking that the compiler has an emit bug and dissecting everything, pulling my hair out, before realizing, it's my debugging technique coupled to this unfortunate codegen. For example, take this code class Program
{
private void Capture(bool value)
{
if (value)
{
var value2 = 12;
Method(() => value ? value2 : value2 + 12);
}
}
private void Method(Func<int> action)
{
action();
}
} If you're debugging and suppose |
Just ran into this earlier (the closed issue right above this) and was pointed to this as the original issue regarding closures allocating memory too early, so I figured I'd throw in my vote to have this looked at. The worst part isn't even the allocations, though they add up very fast if you're not expecting them, it's that the problem is completely hidden. The C# code looks innocuous, with the allocation hidden behind a branch off the hot path, while in reality it's contributing to almost every single allocation in your program. |
There is no good solution here as moving things around may cause other problems. The best thing to do is to potentially write an analyzer and have strict rules about separating your functions. A common pattern is to internally have a static local function which contains the code causing the closure. Being static means it itself doesn't accidentally cause a closure by capturing anything. And the compiler def won't create any closure anywhere in that case. For most people this doesn't matter and the defaults are good. For people where allocations are that impactful, the best thing to do is just be explicit and use only the patterns certain to be safe. The middle ground is likely to very much cause problems when perf is that critical. |
I'm curious why you say that, from the outside it seems that the solution is conceptually simple: don't allocate the closure type until it's actually used, ie inside the
That may be true, but it's only true because of the broken code generation. In fact that's how I solved my problem as well, but it's a work around only, it's not something I'd ever choose to write first.
I'd also be curious of what you mean here. I can tell you that the reason why I raised this issue earlier was because this generated IL was allocating over 2MB of 28 byte objects every single second (hot path in rendering code) with absolutely no indication that this could even be possible by just looking at the C# code. What could be worse, short of just plain incorrect code being generated? |
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
The local function avoid allocating a lambda when not used. For more details, see: dotnet/roslyn#20777
Version: Csc.exe 2.3.0.61907 (ee1637c)
Given:
The following is the code gen:
Notice that you pay for the creation/assignment of the capture class regardless of the branch in the method. This results in unexpected allocations, such as: #20775
The text was updated successfully, but these errors were encountered: