Removing allocations by reducing closure scopes using local variables
Lambdas can use variables from the enclosing methods (documentation), unless the lambda is static
. When a variable is captured, the compiler generates code to create the bridge between the enclosing method and the lambda. Let's consider this simple example:
public void M()
{
var a = 0;
// Create a lambda that use "a" from the enclosing method
var func = () => Console.WriteLine(a);
func();
}
When compiling the code, the compiler rewrites the code as the following:
// Class to store all captured variables and the code of the lambda
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
// The variable "a" is captured
public int a;
// () => Console.WriteLine(a)
internal void <M>b__0() => Console.WriteLine(a);
}
public void M()
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.a = 0;
new Action(<>c__DisplayClass0_.<M>b__0)();
}
Let's take another example. The 2 following methods are very similar, except the second one creates a local variable in the else
block.
public void M1(object? o)
{
if (o == null)
{
Console.WriteLine("null");
}
else
{
Task.Run(() => Console.WriteLine(o));
}
}
public void M2(object? o)
{
if (o == null)
{
Console.WriteLine("null");
}
else
{
var scoped = o;
Task.Run(() => Console.WriteLine(scoped));
}
}
Both methods will do the same thing. But the second one, M2
, is more performant than the first one in the case the value is null
. The difference is the way the compiler rewrites the code. The compiler captures the variable at the beginning of the scope where it is declared. In this case the variable is a parameter, and so, is accessible everywhere in the method. So, the compiler captures the variable at the beginning of the method.
In the second method, the captured variable is not o
but scoped
. This variable is only available in the else
block, so the compiler needs to capture the variable in the else
block, and not at the beginning of the method.
The main difference is when o
is null
, the class doesn't need to be instantiated, so you can avoid one allocation.
public void M1(object? o)
{
// Allocated even when o is null
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.o = o;
if (<>c__DisplayClass0_.o == null)
{
Console.WriteLine("null");
}
else
{
Task.Run(new Action(<>c__DisplayClass0_.<M1>b__0));
}
}
public void M2(object? o)
{
if (o == null)
{
Console.WriteLine("null");
return;
}
// Allocated only when o is not null
<>c__DisplayClass1_0 <>c__DisplayClass1_ = new <>c__DisplayClass1_0();
<>c__DisplayClass1_.scoped = o;
Task.Run(new Action(<>c__DisplayClass1_.<M2>b__0));
}
Do you have a question or a suggestion about this post? Contact me!