Лучшие практики повышения производительности в C#

Original author: Kevin Gosse
  • Translation
Всем привет. Мы подготовили перевод еще одного полезного материала в преддверии старта курса «Разработчик С#». Приятного прочтения.




Поскольку недавно мне довелось составлять список лучших практик в C# для Criteo, я подумал, что было бы неплохо поделиться им публично. Цель этой статьи — предоставить неполный список шаблонов кода, которых следует избегать, либо потому что они сомнительны, либо потому что просто плохо работают. Список может показаться немного рандомным, потому что он слегка выдернут из контекста, но все его элементы в какой-то момент были обнаружены в нашем коде и вызывали проблемы в продакшене. Надеюсь, это послужит хорошей профилактикой и предотвратит ваши ошибки в будущем.

Также обратите внимание, что веб-сервисы Criteo полагаются на высокопроизводительный код, отсюда и необходимость избегать неэффективный код. В большинстве приложений не будет заметно ощутимой разницы от замены некоторых из этих шаблонов.

И последнее, но не менее важное: некоторые пункты (например, ConfigureAwait) уже обсуждались во многих статьях, поэтому я не буду подробно останавливаться на них. Цель заключается в том, чтобы сформировать компактный список моментов, на которые нужно обращать внимание, а не давать подробную техническую выкладку по каждому из них.

Синхронное ожидание асинхронного кода


Никогда не ожидайте синхронно незавершенные задачи. Это касается, но не ограничивается: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll.

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

ConfigureAwait


Если ваш код может быть вызван из контекста синхронизации, используйте ConfigureAwait(false) для каждого из ваших await вызовов.

Обратите внимание, что ConfigureAwait целесообразен только при использовании ключевого слова await.

Например, следующий код лишен какого-либо смысла:

// Использование ConfigureAwait никоим магическим образом не делает этот вызов безопаснее
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();

async void


Никогда не используйте async void. Исключение, выброшенное в async void методе, распространяется в контекст синхронизации и обычно приводит к сбою всего приложения.

Если вы не можете возвращать задачу в своем методе (например, потому что вы реализуете интерфейс), переместите асинхронный код в другой метод и вызывайте его:

interface IInterface
{
    void DoSomething();
}

class Implementation : IInterface
{
    public void DoSomething()
    {
        // Этот метод не может вернуть задачу,
        // делегируйте асинхронный код в другой метод
        _ = DoSomethingAsync();
    }

    private async Task DoSomethingAsync()
    {
        await Task.Delay(100);
    }
}

По возможности избегайте async


По привычке или из-за мышечной памяти вы можете написать что-то вроде:

public async Task CallAsync()
{
    var client = new Client();
    return await client.GetAsync();
}

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

public Task CallAsync()
{
    var client = new Client();
    return _client.GetAsync();
}

Однако имейте в виду, что вы не можете прибегнуть к этой оптимизации, когда ваш код обернут в блоки (например, try/catch или using):

public async Task Correct()
{
    using (var client = new Client())
    {
        return await client.GetAsync();
    }
}

public Task Incorrect()
{
    using (var client = new Client())
    {
        return client.GetAsync();
    }
}

В неправильной версии (Incorrect()), клиент может быть удален до завершения GetAsync вызова, поскольку задача внутри using блока не ожидается посредством await.

Сравнения с учетом региональных особенностей


Если у вас нет причин использовать сравнения с учетом региональных особенностей, всегда используйте порядковые сравнения. Хотя из-за внутренних оптимизаций это и не имеет большого значения для форм представления данных en-US, сравнение происходит на порядок медленнее для форм представления других регионов (и до двух порядков в Linux!). Поскольку сравнение строк является частой операцией в большинстве приложений, накладные расходы возрастают ощутимо.

ConcurrentBag<T>


Никогда не используйте ConcurrentBag<T> без бенчмаркинга. Эта коллекция была разработана для очень специфических случаев использования (когда большую часть времени элемент исключается из очереди потоком, поставившим его в очередь) и страдает от серьезных проблем с производительностью, если используется не по назначению. Если вам нужна потокобезопасная коллекция, предпочитайте ConcurrentQueue<T>.

ReaderWriterLock / ReaderWriterLockSlim<T>
Никогда не используйте ReaderWriterLock<T>/ReaderWriterLockSlim<T> без бенчмаркинга. Хотя использовать этот вид специализированного примитива синхронизации при работе с читателями и писателями может быть заманчиво, его стоимость намного выше простого Monitor (используемого с ключевым словом lock). Если количество читателей, выполняющих критическую секцию одновременно, не очень велико, параллелизма будет недостаточно для амортизации возросших накладных расходов, и код будет работать хуже.

Предпочитайте лямбда-функции вместо группы методов


Рассмотрим следующий код:

public IEnumerable<int> GetItems()
{
    return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Resharper предлагает переписать код без лямбда-функции, что может выглядеть немного чище:

public IEnumerable<int> GetItems()
{
    return _list.Where(Filter);
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

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

public IEnumerable<int> GetItems()
{
    return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
    return i % 2 == 0;
}

Это может оказать значительное влияние на производительность, если код вызывается в высоконагруженном участке.

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

private Predicate<int> _filter;

public Constructor()
{
    _filter = new Predicate<int>(Filter);
}

public IEnumerable<int> GetItems()
{
    return _list.Where(_filter);
}

private bool Filter(int element)
{
    return i % 2 == 0;
}

Преобразование перечислений в строки


Вызов Enum.ToString в .net является достаточно дорогостоящим, поскольку для преобразования внутри используется рефлексия, а вызов виртуального метода для структуры провоцирует упаковку. Этого следует избегать, насколько это возможно.

Часто перечисления могут быть заменены константными строками:

// В обоих случаях вы можете использовать Numbers.One, Numbers.Two, ...
public enum Numbers
{
    One,
    Two,
    Three
}

public static class Numbers
{
    public const string One = "One";
    public const string Two = "Two";
    public const string Three = "Three";
}

Если вам действительно необходимо использовать перечисление, рассмотрите возможность кэширования преобразованного значения в словаре, чтобы амортизировать накладные расходы.

Сравнение перечислений


Примечание: это больше не актуально в .net core, начиная с версии 2.1, оптимизация выполняется JIT автоматически.

При использовании перечислений в качестве флагов может возникнуть соблазн использовать метод Enum.HasFlag:

[Flags]
public enum Options
{
    Option1 = 1,
    Option2 = 2,
    Option3 = 4
}

private Options _option;

public bool IsOption2Enabled()
{
    return _option.HasFlag(Options.Option2);
}

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

public bool IsOption2Enabled()
{
    return (_option & Options.Option2) == Options.Option2;
}

Реализация методов сравнения для структур


При использовании структуры в сравнениях (например, при использовании в качестве ключа для словаря) вам необходимо переопределять методы Equals/GetHashCode. Реализация по умолчанию использует рефлексию и очень медленная. Реализация, сгенерированная Resharper, обычно достаточно хороша.

Узнать об этом больше можете здесь: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-c

Избегайте нецелесообразной упаковки при использовании структур с интерфейсами


Рассмотрим следующий код:

public class IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue(IValue value)
{
    // ...
}

public void LogValue(IValue value)
{
    // ...
}

Сделать IntValue структурой может быть соблазнительно, чтобы избежать выделения динамической памяти. Но поскольку AddValue и SendValue ожидают интерфейс, а интерфейсы имеют эталонную семантику, значение будет упаковываться при каждом вызове, сводя на нет преимущества этой «оптимизации». На самом деле, выделений памяти будет даже больше, чем если бы IntValue был классом, поскольку значение будет упаковано независимо для каждого вызова.

Если вы пишете API и ожидаете, что некоторые значения будут структурами, попробуйте использовать универсальные методы:

public struct IntValue : IValue
{
}

public void DoStuff()
{
    var value = new IntValue();

    LogValue(value);
    SendValue(value);
}

public void SendValue<T>(T value) where T : IValue
{
    // ...
}

public void LogValue<T>(T value) where T : IValue
{
    // ...
}

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

Подписки CancellationToken всегда инлайнятся


Когда вы отменяете CancellationTokenSource, все подписки будут выполняться внутри текущего потока. Это может привести к незапланированным паузам или даже неявным взаимным блокировкам.

var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel(); // Это вызовет блокировку на 5 секунд

Вы не можете избежать этого поведения. Поэтому, при отмене CancellationTokenSource, спросите себя, можете ли вы безопасно позволить своему текущему потоку быть захваченным. Если ответ отрицательный, оберните вызов Cancel внутри Task.Run, чтобы выполнить его в пуле потоков.

Континуации TaskCompletionSource зачастую инлайнятся


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

class Program
{
    private static ManualResetEventSlim _mutex = new ManualResetEventSlim();
    
    public static async Task Deadlock()
    {
        await ProcessAsync();
        _mutex.Wait();
    }
    
    private static Task ProcessAsync()
    {
        var tcs = new TaskCompletionSource<bool>();
        
        Task.Run(() =>
        {
            Thread.Sleep(2000); // Симулируем какую-нибудь работу
            tcs.SetResult(true);
            _mutex.Set();
        });
        
        return tcs.Task;
    }
    
    static void Main(string[] args)
    {
        Deadlock().Wait();
        Console.WriteLine("Will never get there");
    }
}

Вызов tcs.SetResult заставляет продолжение await ProcessAsync() выполниться в текущем потоке. Следовательно, оператор _mutex.Wait() выполняется тем же потоком, который должен вызывать _mutex.Set(), что приводит к взаимной блокировке. Этого можно избежать, передав параметр TaskCreationsOptions.RunContinuationsAsynchronously в TaskCompletionSource.

Если у вас нет веских причин для пренебрежения им, всегда используйте параметр TaskCreationsOptions.RunContinuationsAsynchronously при создании TaskCompletionSource.

Будьте осторожны: код также будет компилироваться, если вы используете TaskContinuationOptions.RunContinuationsAsynchronously вместо TaskCreationOptions.RunContinuationsAsynchronously, но параметры будут игнорироваться, а континуации будут все так же инлайниться. Это удивительно распространенная ошибка, потому что TaskContinuationOptions предшествует TaskCreationOptions в автозаполнении.

Task.Run / Task.Factory.StartNew


Если у вас нет причин использовать Task.Factory.StartNew, всегда выбирайте Task.Run для запуска фоновой задачи. Task.Run использует более безопасные значения по умолчанию, и что более важно, он автоматически распаковывает возвращаемую задачу, что может предотвратить неочевидные ошибки с асинхронными методами. Рассмотрим следующую программу:

class Program
{
    public static async Task ProcessAsync()
    {
        await Task.Delay(2000);
        Console.WriteLine("Processing done");
    }
    
    static async Task Main(string[] args)
    {
        await Task.Factory.StartNew(ProcessAsync);
        Console.WriteLine("End of program");
        Console.ReadLine();
    }
}

Несмотря на внешний вид, «End of program» будет отображено раньше, чем «Processing done». Это потому, что Task.Factory.StartNew будет возвращать Task<Task>, а код ожидает только внешнюю задачу. Корректным кодом могло бы быть либо await Task.Factory.StartNew(ProcessAsync).Unwrap(), либо await Task.Run(ProcessAsync).

Существует только три допустимых варианта использования Task.Factory.StartNew:

  • Запуск задачи в другом планировщике.
  • Выполнение задачи в выделенном потоке (с помощью TaskCreationOptions.LongRunning).
  • Размещение задачи в глобальной очереди пула потоков (с помощь TaskCreationOptions.PreferFairness).



Узнать подробнее о курсе.


OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Comments 16

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

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

        Пример из ссылки
        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))

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


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

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

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


          Использование лямбда-функций запускает оптимизацию компилятора, которая кэширует делегат в статическое поле, избегая аллокации. Это работает только если 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 в дотнете фигово реализован. Хотя, учитывая ошибки в материале, возможно автор и тут немного преувеличивает. А остальное, вроде, общеизвестно, хотя возможно кто-то узнал что-то новое.




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

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

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

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


              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.

          Only users with full accounts can post comments. Log in, please.