I started to use the method group syntax a couple of years ago based on some suggestion from ReSharper and recently I gave a try to ClrHeapAllocationAnalyzer and it flagged every location where I was using a method group in a lambda with the issue HAA0603 - This will allocate a delegate instance
.
As I was curious to see if this suggestion was actually useful, I wrote a simple console app for the 2 cases.
Code1:
class Program
{
static void Main(string[] args)
{
var temp = args.AsEnumerable();
for (int i = 0; i < 10_000_000; i++)
{
temp = temp.Select(x => Foo(x));
}
Console.ReadKey();
}
private static string Foo(string x)
{
return x;
}
}
Code2:
class Program
{
static void Main(string[] args)
{
var temp = args.AsEnumerable();
for (int i = 0; i < 10_000_000; i++)
{
temp = temp.Select(Foo);
}
Console.ReadKey();
}
private static string Foo(string x)
{
return x;
}
}
Putting a break point on the Console.ReadKey();
of Code1 shows a memory consumption of ~500MB and on Code2 a consumption of ~800MB. Even if we can argue on whether this test case is good enough to explain something it actually shows a difference.
So I decided to have a look at the IL code produced to try to understand the difference between the 2 code.
IL Code1:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 75 (0x4b)
.maxstack 3
.entrypoint
.locals init (
[0] class [mscorlib]System.Collections.Generic.IEnumerable`1<string>,
[1] int32,
[2] bool
)
// temp = from x in temp
// select Foo(x);
IL_0000: nop
// IEnumerable<string> temp = args.AsEnumerable();
IL_0001: ldarg.0
IL_0002: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::AsEnumerable<string>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_0007: stloc.0
// for (int i = 0; i < 10000000; i++)
IL_0008: ldc.i4.0
IL_0009: stloc.1
// (no C# code)
IL_000a: br.s IL_0038
// loop start (head: IL_0038)
IL_000c: nop
IL_000d: ldloc.0
IL_000e: ldsfld class [mscorlib]System.Func`2<string, string> ConsoleApp1.Program/'<>c'::'<>9__0_0'
IL_0013: dup
IL_0014: brtrue.s IL_002d
IL_0016: pop
IL_0017: ldsfld class ConsoleApp1.Program/'<>c' ConsoleApp1.Program/'<>c'::'<>9'
IL_001c: ldftn instance string ConsoleApp1.Program/'<>c'::'<Main>b__0_0'(string)
IL_0022: newobj instance void class [mscorlib]System.Func`2<string, string>::.ctor(object, native int)
IL_0027: dup
IL_0028: stsfld class [mscorlib]System.Func`2<string, string> ConsoleApp1.Program/'<>c'::'<>9__0_0'
IL_002d: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!1> [System.Core]System.Linq.Enumerable::Select<string, string>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, !!1>)
IL_0032: stloc.0
IL_0033: nop
// for (int i = 0; i < 10000000; i++)
IL_0034: ldloc.1
IL_0035: ldc.i4.1
IL_0036: add
IL_0037: stloc.1
// for (int i = 0; i < 10000000; i++)
IL_0038: ldloc.1
IL_0039: ldc.i4 10000000
IL_003e: clt
IL_0040: stloc.2
// (no C# code)
IL_0041: ldloc.2
IL_0042: brtrue.s IL_000c
// end loop
// Console.ReadKey();
IL_0044: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0049: pop
// (no C# code)
IL_004a: ret
} // end of method Program::Main
IL Code2:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 56 (0x38)
.maxstack 3
.entrypoint
.locals init (
[0] class [mscorlib]System.Collections.Generic.IEnumerable`1<string>,
[1] int32,
[2] bool
)
// (no C# code)
IL_0000: nop
// IEnumerable<string> temp = args.AsEnumerable();
IL_0001: ldarg.0
IL_0002: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::AsEnumerable<string>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_0007: stloc.0
// for (int i = 0; i < 10000000; i++)
IL_0008: ldc.i4.0
IL_0009: stloc.1
// (no C# code)
IL_000a: br.s IL_0025
// loop start (head: IL_0025)
IL_000c: nop
// temp = temp.Select(Foo);
IL_000d: ldloc.0
IL_000e: ldnull
IL_000f: ldftn string ConsoleApp1.Program::Foo(string)
IL_0015: newobj instance void class [mscorlib]System.Func`2<string, string>::.ctor(object, native int)
IL_001a: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!1> [System.Core]System.Linq.Enumerable::Select<string, string>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>, class [mscorlib]System.Func`2<!!0, !!1>)
IL_001f: stloc.0
// (no C# code)
IL_0020: nop
// for (int i = 0; i < 10000000; i++)
IL_0021: ldloc.1
IL_0022: ldc.i4.1
IL_0023: add
IL_0024: stloc.1
// for (int i = 0; i < 10000000; i++)
IL_0025: ldloc.1
IL_0026: ldc.i4 10000000
IL_002b: clt
IL_002d: stloc.2
// (no C# code)
IL_002e: ldloc.2
IL_002f: brtrue.s IL_000c
// end loop
// Console.ReadKey();
IL_0031: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0036: pop
// (no C# code)
IL_0037: ret
} // end of method Program::Main
I have to admit I am not enough expert in IL code to actually fully understand the difference and that's why I am raising this thread.
As far as I have understood, the actual Select
seems to generate more instructions when not done through a method group (Code1) BUT is using some pointer to native functions. Is it reusing the method through the pointer compared to the other case which is always generating a new delegate?
Also I have noticed that the method group IL (Code2) is generating 3 comments linked to the for
loop compared to the IL code of Code1.
Any help in understanding the difference of allocation would be appreciated.
Delegates allow methods to be passed as parameters. Delegates can be used to define callback methods. Delegates can be chained together; for example, multiple methods can be called on a single event. Methods don't have to match the delegate type exactly.
A method group is the name for a set of methods (that might be just one) - i.e. in theory the ToString method may have multiple overloads (plus any extension methods): ToString() , ToString(string format) , etc - hence ToString by itself is a "method group".
Delegates can be invoke like a normal function or Invoke() method. Multiple methods can be assigned to the delegate using "+" or "+=" operator and removed using "-" or "-=" operator. It is called multicast delegate. If a multicast delegate returns a value then it returns the value from the last assigned target method.
Spending some more time understanding why ReSharper is recommending to use method group instead of lambdas and reading the articles quoted in the rule page description, I am now able to answer my own question.
For cases where the number of iterations is small enough, around 1M with the code snippet I provided (so probably the majority of cases), the difference in memory allocation is small enough so that the 2 implementations are equivalent. Besides, and as we can see in the 2 generated IL Codes the compilation is faster as there is less instructions to generate. Note this was clearly stated by ReSharper :
to achieve more compact syntax and prevent compile-time overhead caused by using lambdas.
Which explain ReSharper recommendation.
But if you know that the delegate is going to be heavily used then the lambda is a better choice.
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