Как стать автором
Обновить

Комментарии 16

Хотелось бы обратить внимание на нашу книгу «Высокопроизводительный код на платформе .NET. 2-е издание». Там речь не только о C#, но и о F#.
Более подробно с книгой можно ознакомиться на сайте издательства.
Извиняюсь за ошибочку, почти одновременно книги выпускались, немного перепутал. В книге выше всё О и НА C#.
А вот книга «Конкурентность и параллелизм на платформе .NET. Паттерны эффективного проектирования» действительно двуязычная. По тексту примеры реализации в т.ч. на F# и даже отдельные параграфы по необходимости заточены на F#. В приложениях ещё 35 страниц о функциональном программировании и F#.
Совет про лямбды vs группы методов несколько странный. На примере кода от автора не происходит никакого кэширования, точно так же создается System.Func<int32,bool> на каждом вызове. Разницы по сравнению с .Where(Filter) никакой.

В комментариях к оригинальной статье это тоже отмечено. Скорее всего автор оригинала промахнулся с прочтением il-а.
По поводу method group, здесь подробный issue с бенчмарками github.com/dotnet/roslyn/issues/39869
Может быть Вы какой-то специфичный кейс рассматриваете, но обычно лямбды все же кэшируются.

Пример из ссылки
using System;
using System.Collections.Generic;
using System.Linq;

public class C {
    public void M() {
        var lst = new List<int>() {1, 2, 3, 4};
        var x = lst.Where(v => v % 2 == 0).ToList();
    }
}


И декомпиляция:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class C
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        public static Func<int, bool> <>9__0_0;

        internal bool <M>b__0_0(int v)
        {
            return v % 2 == 0;
        }
    }

    public void M()
    {
        List<int> list = new List<int>();
        list.Add(1);
        list.Add(2);
        list.Add(3);
        list.Add(4);
        List<int> source = list;
        List<int> list2 = Enumerable.ToList(Enumerable.Where(source, <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, bool>(<>c.<>9.<M>b__0_0))));
    }
}


Как мы видим, лямбда создается лишь однажды: <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Func<int, bool>(<>c.<>9.b__0_0))

Да, сорри, вчитался в il — не заметил brtrue.s :(
    public void DoSomething()
    {
        // Этот метод не может вернуть задачу,
        // делегируйте асинхронный код в другой метод
        _ = DoSomethingAsync();
    }


Серьёзно? Где окажется исключение выброшенное в задаче, которую вернёт DoSomethingAsync?
Оно будет проглочено.
Это очень вредный совет: генерация исключения это серьёзное состояние, которое стоит обработать. «Проглатывание» исключений не дает возможности узнать, что они вообще случались.
В общем и целом async void тоже не best practice, но я знаю два распространенных сценария, когда хотелось бы асинхронщины в void функции:
1. обработчики событий (в т.ч. от UI)
2. get/set с запуском фоновых задач
Ну и тут надо смотреть логику работы: если нужна фоновая обработка задач то есть прекрасный пакет AmbientTasks, который не позволит потерять исключения и отказаться от использования async void, при этом запуская задачи в фоне без ожидания завершения оных. Это безумно удобно как при обработке событий от UI, так и для использования в сеттерах view-model для свойства используемого в двунаправленном binding которое требует некой фоновой асинхронно обработки.
В противном случае, если возврат управления уже несет полезную нагрузку, надо ждать. Или падать, если прилетело исключение.
Более того, это не поможет тем, кто использует .Net Framework версии 4.0 и ниже, а также тем, у кого в app.config есть
<ThrowUnobservedTaskExceptions enabled="true" />

Правильный вариант, как по мне — ждать и обрабатывать, либо ждать и падать.

Спасибо за перевод, но местами автор конечно даёт.


Использование лямбда-функций запускает оптимизацию компилятора, которая кэширует делегат в статическое поле, избегая аллокации. Это работает только если Filter статичный. Если нет, вы можете кэшировать делегат самостоятельно

Ок, давайте проверим:


public static IEnumerable<int> GetItems(List<int> _list) => _list.Where(Filter);
public static IEnumerable<int> GetItemsFast(List<int> _list) => _list.Where(x => Filter(x));
private static bool Filter(int i) => i % 2 == 0;

Смотрим на IL:


  .method public hidebysig static class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>
    GetItems(
      class [System.Collections]System.Collections.Generic.List`1<int32> _list
    ) cil managed noinlining
  {
    .maxstack 8

    // [18 9 - 18 36]
    IL_0000: ldarg.0      // _list
    IL_0001: ldnull
    IL_0002: ldftn        bool C::Filter(int32)
    IL_0008: newobj       instance void class [System.Runtime]System.Func`2<int32, bool>::.ctor(object, native int)
    IL_000d: call         class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/> [System.Linq]System.Linq.Enumerable::Where<int32>(class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/>, class [System.Runtime]System.Func`2<!!0/*int32*/, bool>)
    IL_0012: ret

  } // end of method C::GetItems

  .method public hidebysig static class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>
    GetItemsFast(
      class [System.Collections]System.Collections.Generic.List`1<int32> _list
    ) cil managed noinlining
  {
    .maxstack 8

    // [24 9 - 24 44]
    IL_0000: ldarg.0      // _list
    IL_0001: ldsfld       class [System.Runtime]System.Func`2<int32, bool> C/'<>c'::'<>9__2_0'
    IL_0006: dup
    IL_0007: brtrue.s     IL_0020
    IL_0009: pop
    IL_000a: ldsfld       class C/'<>c' C/'<>c'::'<>9'
    IL_000f: ldftn        instance bool C/'<>c'::'<GetItemsFast>b__2_0'(int32)
    IL_0015: newobj       instance void class [System.Runtime]System.Func`2<int32, bool>::.ctor(object, native int)
    IL_001a: dup
    IL_001b: stsfld       class [System.Runtime]System.Func`2<int32, bool> C/'<>c'::'<>9__2_0'
    IL_0020: call         class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/> [System.Linq]System.Linq.Enumerable::Where<int32>(class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0/*int32*/>, class [System.Runtime]System.Func`2<!!0/*int32*/, bool>)
    IL_0025: ret

  } // end of method C::GetItemsFast

Видим описанное поведение, но в том же ассемблере после всех оптимизаций первый вариант кмк будет шустрее (хотя я не эксперт, мб кто-то подробнее разберет какой вариант лучшее):


00007FFA5CB21690  push        rbp  
00007FFA5CB21691  sub         rsp,30h  
00007FFA5CB21695  lea         rbp,[rsp+30h]  
00007FFA5CB2169A  xor         eax,eax  
00007FFA5CB2169C  mov         qword ptr [rbp-8],rax  
00007FFA5CB216A0  mov         qword ptr [rbp+10h],rcx  
00007FFA5CB216A4  mov         rcx,7FFA5CBE3B10h  
00007FFA5CB216AE  call        00007FFABC637710  
00007FFA5CB216B3  mov         qword ptr [rbp-8],rax  
00007FFA5CB216B7  mov         r8,7FFA5CB1C6A0h  
00007FFA5CB216C1  mov         rcx,qword ptr [rbp-8]  
00007FFA5CB216C5  xor         edx,edx  
00007FFA5CB216C7  mov         r9,7FFA5C9ED070h  
00007FFA5CB216D1  call        00007FFA5CB13F60  
00007FFA5CB216D6  mov         rcx,qword ptr [rbp+10h]  
00007FFA5CB216DA  mov         rdx,qword ptr [rbp-8]  
00007FFA5CB216DE  call        00007FFA5CB20F60  
00007FFA5CB216E3  nop  
00007FFA5CB216E4  lea         rsp,[rbp]  
00007FFA5CB216E8  pop         rbp  
00007FFA5CB216E9  ret  

00007FFA5CB22533  mov         rcx,7FFA5CBBFB50h  
00007FFA5CB2253D  mov         edx,3  
00007FFA5CB22542  call        00007FFABC637B10  
00007FFA5CB22547  mov         rcx,296D3342C50h  
00007FFA5CB22551  mov         rcx,qword ptr [rcx]  
00007FFA5CB22554  mov         qword ptr [rbp-18h],rcx  
00007FFA5CB22558  mov         rcx,qword ptr [rbp+10h]  
00007FFA5CB2255C  mov         qword ptr [rbp-20h],rcx  
00007FFA5CB22560  mov         rcx,qword ptr [rbp-18h]  
00007FFA5CB22564  mov         qword ptr [rbp-28h],rcx  
00007FFA5CB22568  cmp         qword ptr [rbp-18h],0  
00007FFA5CB2256D  jne         00007FFA5CB225F1  
00007FFA5CB22573  mov         rcx,7FFA5CBE3B10h  
00007FFA5CB2257D  call        00007FFABC637710  
00007FFA5CB22582  mov         qword ptr [rbp-30h],rax  
00007FFA5CB22586  mov         rcx,7FFA5CBBFB50h  
00007FFA5CB22590  mov         edx,3  
00007FFA5CB22595  call        00007FFABC637B10  
00007FFA5CB2259A  mov         rdx,296D3342C48h  
00007FFA5CB225A4  mov         rdx,qword ptr [rdx]  
00007FFA5CB225A7  mov         qword ptr [rbp-38h],rdx  
00007FFA5CB225AB  mov         rdx,qword ptr [rbp-38h]  
00007FFA5CB225AF  mov         r8,7FFA5CB22138h  
00007FFA5CB225B9  mov         rcx,qword ptr [rbp-30h]  
00007FFA5CB225BD  call        00007FFA5CB13F48  
00007FFA5CB225C2  mov         rcx,7FFA5CBBFB50h  
00007FFA5CB225CC  mov         edx,3  
00007FFA5CB225D1  call        00007FFABC637B10  
00007FFA5CB225D6  mov         rcx,296D3342C50h  
00007FFA5CB225E0  mov         rdx,qword ptr [rbp-30h]  

Идём дальше:


Этот код провоцирует две упаковки с аллокацией: одна для преобразования Options.Option2 в Enum, а другая для виртуального вызова HasFlag для структуры. Это делает этот код непропорционально дорогостоящим. Вместо этого вам следует пожертвовать читаемостью и использовать бинарные операторы

Смотрим в IL.


    .method public hidebysig 
        instance bool IsOption2Enabled (
            valuetype Options _option
        ) cil managed 
    {
        // Method begins at RVA 0x2052
        // Code size 18 (0x12)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: box Options
        IL_0006: ldc.i4.2
        IL_0007: box Options
        IL_000c: call instance bool [System.Private.CoreLib]System.Enum::HasFlag(class [System.Private.CoreLib]System.Enum)
        IL_0011: ret
    } // end of method C::IsOption2Enabled

    .method public hidebysig 
        instance bool IsOption2EnabledFast (
            valuetype Options _option
        ) cil managed 
    {
        // Method begins at RVA 0x2065
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: ldc.i4.2
        IL_0002: and
        IL_0003: ldc.i4.2
        IL_0004: ceq
        IL_0006: ret
    } // end of method C::IsOption2EnabledFast

Ой да, действительно, боксы, ужас-ужас. А что у нас в ASM?


C.IsOption2Enabled(Options)
    L0000: test dl, 2
    L0003: setne al
    L0006: movzx eax, al
    L0009: ret

C.IsOption2EnabledFast(Options)
    L0000: test dl, 2
    L0003: setne al
    L0006: movzx eax, al
    L0009: ret

Внезапно, никаких боксов.


Как и подписки CancellationToken, континуации TaskCompletionSource зачастую инлайнятся. Это хорошая оптимизация, но она может быть причиной неявных ошибок. Например, рассмотрим следующую программу

Просто в 2020 году нужно уже разучиться писать блокирующий Wait в коде.


Ну и так далее. Для себя вынес, что ReadWriter в дотнете фигово реализован. Хотя, учитывая ошибки в материале, возможно автор и тут немного преувеличивает. А остальное, вроде, общеизвестно, хотя возможно кто-то узнал что-то новое.




В общем, могу посоветовать порекомендовать перепроверять высказывания "экспертов", чтобы после ускорения программа не начала работать в полтора раза медленнее чем до.

Потратил ещё немного времени: если рассахарить ещё немного лямбды (и руками прописать то, что генерирует компилятор), то получится вот такое


https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKGIAYACY8lAbhvqfIDoAZASwB2AR3bUOAZiakGAYQYBvGgxVMpzJFwkAeIRgB8DAOIwMASQwx8uABQDcGXYIMMA+gBt+DgJTLVS6lUgpgB2N08HbgB1AAsYWBsAMX53SyhvMSCAXxo/FQAHKH4AN2xLLk1gCAh3BmTU+Js9Bn5fQP884OIw/gYAUgYZAF4hhjpM1RzxdoKi0vLcGGx3GAATaQYwToDg1WJ1ck1YbFWIQXcAT02WwQdsQTAYMU6g/YqmAFYnDDQGKprDAAzACuD2eM2CenigmWf2qtQAoggYGAms4GAg2rsVDtsa8wvU0jZMRNdlNsrkIW8NFpvoYTOZLNZEngMHYvI49IYPBysR0IfiGAjBMD8PFsMAVtE4gkeQ5fmBuCCHgwAPyqhg2RXKsAMUaCGAAd0+31+/3c+i13CEdweMG4SJR3mdpIYUyyQA=


Видно, что во втором случае ассемблера на четверть больше, виднеются ошметки MulticastDelegate и прочая непотребщина.


В общем, то что такая замена дает профит в перфомансе считаю можно считать опровергнутым. Метод группы и чище, и производительнее.

c.func ?? (c.func = new Func<int, bool>(c.instance.Exec))

вот этот обвес экономит ровно одно создание объекта на каждом вызове GetItemsFast. Экономия на спичках, но формально она есть.
Ой да, действительно, боксы, ужас-ужас. А что у нас в ASM? Внезапно, никаких боксов.

Так ведь вы в .NET Core посмотрели. Автор и пишет, что начиная с .NET Core 2.1 это уже не актуально.

Хотя код семантически корректен, использование ключевого слова async здесь не требуется и может привести к значительным накладным расходам в высоконагруженной среде. Старайтесь избегать его, когда это возможно

О каких накладных расходах тут речь?
Рискну предположить, что автор имеет ввиду создание и запуск стейт-машины.

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


Do not elide by default. Use the async and await for natural, easy-to-read code.
Do consider eliding when the method is just a passthrough or overload.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий