In "Getting Started with Asynchronous Programming in .NET" by Filip Ekberg, in the "Asynchronous Programming Deep Dive/Working with Attached and Detached Tasks" chapter, he says that by using the service
value inside the async anonymous method, it introduces a closure and unnecessary allocation:
Next, he says it's better to pass services
as a parameter to the action delegate of the StartNew
method which avoids unnecessary allocations by avoiding a closure:
My question is:
What is the allocation the author says it's being saved by passing the services
as parameter?
To make it easier to investigate, I took a much simpler example and put it in the sharplab.io
:
Not passing the parameter:
using System;
public class C {
public void M() {
var i = 1;
Func<int> f = () => i + 1;
f();
}
}
which compiles to:
public class C
{
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public int i;
internal int <M>b__0()
{
return i + 1;
}
}
public void M()
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.i = 1;
new Func<int>(<>c__DisplayClass0_.<M>b__0)();
}
}
There are two allocations, one for the generated class and one for the Func<int>
delegate.
Passing the parameter:
using System;
public class C {
public void M() {
var i = 1;
Func<int, int> f = (x) => x + 1;
f(i);
}
}
which compiles to:
public class C
{
[Serializable]
[CompilerGenerated]
private sealed class <>c
{
public static readonly <>c <>9 = new <>c();
public static Func<int, int> <>9__0_0;
internal int <M>b__0_0(int x)
{
return x + 1;
}
}
public void M()
{
int arg = 1;
(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, int>(<>c.<>9.<M>b__0_0)))(arg);
}
}
Here, there are still two allocations, one for the generated class (note there's a static field in the class which creates an instance of the class) and another allocation for the Func<int>
delegate.
As far as I can see, in both cases the compiler generates a class and there are two allocations.
The only difference I can see is that in the first case, the generated class has a member:
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public int i;
This does increase the allocation size, because the generated class occupies more memory than in the 2nd case.
Did I understand it correctly, is this what the author refers to?
The difference is in caching.
In your original code, a new delegate instance is created on every call to M()
. In the "clever" version, only a single instance is every created, and stored in the static variable.
So if you only call M()
once, the same number of objects will be allocated. If you call M()
a million times, you'll have far more objects allocated with the first code than with the second.
This code:
(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, int>(<>c.<>9.<M>b__0_0)))(arg);
... should be read as effectively:
if (cachedDelegate == null)
{
cachedDelegate = new Func<int, int>(GeneratedClass.CachedInstance.Method);
}
cachedDelegate.Invoke(arg);
The instance of <>c
is also cached (referred to above as GeneratedClass.CachedInstance
) - only a single instance of that is created. (Although that's less important here, as it only needs to be created when the delegate is created... I'm not sure under what circumstances that compiler optimization is particularly useful.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With