Распределение экземпляра делегата с группой методов по сравнению с - PullRequest
0 голосов
/ 08 ноября 2018

Я начал использовать синтаксис группы методов пару лет назад, основываясь на некотором предложении ReSharper, и недавно я попробовал ClrHeapAllocationAnalyzer , и он пометил каждое место, где я использовал группу методов в лямбда с вопросом HAA0603 - This will allocate a delegate instance.

Поскольку мне было любопытно узнать, было ли это предложение действительно полезным, я написал простое консольное приложение для 2 случаев.

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

Кодекса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;
    }
}

Установка точки останова на Console.ReadKey(); из Code1 показывает потребление памяти ~ 500 МБ , а при Code2 потребление ~ 800 МБ . Даже если мы можем поспорить, достаточно ли хорош этот тестовый пример, чтобы объяснить что-то, на самом деле он показывает разницу

Поэтому я решил взглянуть на код 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), НО использует некоторый указатель на нативные функции. Это повторное использование метода через указатель по сравнению с другим случаем, который всегда генерирует новый делегат?

Также я заметил, что группа методов IL (Code2) генерирует 3 комментария, связанных с циклом for, по сравнению с IL-кодом Code1.

Буду признателен за любую помощь в понимании разницы в распределении.

1 Ответ

0 голосов
/ 09 ноября 2018

Потратив немного больше времени на понимание, почему ReSharper рекомендует использовать группу методов вместо лямбд, и читая статьи, цитируемые в описании страницы правила , я теперь могу ответить на свой собственный вопрос.

Для случаев, когда количество итераций достаточно мало, около 1M с предоставленным мною фрагментом кода (так, вероятно, в большинстве случаев), разница в распределении памяти достаточно мала, так что 2 реализации эквивалентны.Кроме того, и, как мы видим из 2 сгенерированных IL-кодов, компиляция выполняется быстрее, так как меньше инструкций для генерации.Обратите внимание, что ReSharper четко указал:

для достижения более компактного синтаксиса и предотвращения накладных расходов во время компиляции, вызванных использованием lambdas.

Что объясняет рекомендацию ReSharper.

Но если вы знаете, что делегат будет активно использоваться, тогда лямбда - лучший выбор.

...