几年前,我根据 ReSharper 的一些建议开始使用方法组语法,最近我尝试了 ClrHeapAllocationAnalyzer ,它标记了我在 lambda 中使用方法组的每个位置,问题是 HAA0603 - This will allocate a delegate instance

由于我很好奇这个建议是否真的有用,我为这两种情况编写了一个简单的控制台应用程序。

代码1:

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;
    }
}

代码2:
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;
    }
}

穿上的代码1 节目〜500MB 存储器消耗Console.ReadKey();和上代码2 〜800MB 消耗中断点。即使我们可以争论这个测试用例是否足以解释某些事情,但它实际上显示出差异。

因此,我决定查看生成的 IL 代码以尝试了解 2 个代码之间的区别。

IL 代码 1:
.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 代码 2:
.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

我不得不承认我在 IL 代码方面还不够专家,无法真正完全理解差异,这就是我提出这个线程的原因。

据我所知,实际的 Select 似乎在不通过方法组(Code1)完成但使用一些指向 native 函数的指针时生成更多指令。与始终生成新委托(delegate)的其他情况相比,它是否通过指针重用该方法?

我还注意到,与 Code1 的 IL 代码相比,方法组 IL (Code2) 生成了 3 个链接到 for 循环的注释。

任何有助于理解分配差异的帮助将不胜感激。

最佳答案

花更多时间了解为什么 ReSharper 建议使用方法组而不是 lambdas 并阅读 rule page description 中引用的文章,我现在可以回答我自己的问题。

对于迭代次数足够小的情况,我提供的代码片段大约为 1M(因此可能是大多数情况),内存分配的差异足够小,因此 2 个实现是等效的。此外,正如我们在 2 个生成的 IL 代码中看到的那样,编译速度更快,因为生成的指令更少。请注意,ReSharper 明确说明了这一点:



这解释了 ReSharper 的建议。

但是如果您知道委托(delegate)将被大量使用,那么 lambda 是更好的选择。

关于c# - 与方法组相比,委托(delegate)实例分配,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/53208516/

10-13 09:33