.NET Разработчик
6.54K subscribers
442 photos
3 videos
14 files
2.12K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День четыреста восемьдесят пятый. #PerformanceTips
Лучшие Практики по Производительности в C#. Начало
Недавно нашёл статью со списком «шаблонов кода, которых следует избегать, потому что они плохо работают в смысле производительности». Автор пишет, что все пункты в списке так или иначе вызывали проблемы с производительностью. Хотя, стоит оговориться, что не все советы могут работать во всех случаях.

1. Синхронное ожидание асинхронного кода
Не ожидайте синхронно незавершённых задач. Включая, но не ограничиваясь: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll. Более общий совет: любая синхронная зависимость между двумя потоками пула может вызвать истощение пула потоков.

2. ConfigureAwait
Если ваш код может быть исполнен без захвата контекста синхронизации, используйте ConfigureAwait(false) для каждого вызова await. Однако обратите внимание, что ConfigureAwait имеет смысл только при использовании ключевого слова await. Например, этот код не имеет смысла:
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();
Подробнее о ConfigureAwait в серии постов с тегом #AsyncAwaitFAQ

3. async void
Никогда не используйте async void. Исключение, выброшенное в таком методе, распространяется в контекст синхронизации и обычно приводит к сбою всего приложения. Если вы не можете вернуть задачу в свой метод (например, потому что вы реализуете интерфейс), переместите асинхронный код метод-обёртку и вызовите его:
interface IInterface {
void DoSomething();
}
class Implementation : IInterface {
public void DoSomething() {
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync() {
await Task.Delay(100);
}
}

4. По возможности избегайте слова 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, поскольку задача не ожидается внутри блока using, клиент может быть удален до завершения вызова GetAsync.

Продолжение следует…

Источник:
https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День четыреста восемьдесят шестой. #PerformanceTips
Лучшие Практики по Производительности в C#. Продолжение
5. Сравнения Строк Чувствительные к Культуре
Если у вас нет причин использовать чувствительные к культуре сравнения строк, всегда используйте нечувствительные сравнения (с параметром StringComparison.Ordinal). Хотя это и не имеет большого значения для латиницы из-за внутренних оптимизаций, сравнение происходит на порядок медленнее для других культур (до 2 порядков в Linux). Поскольку сравнение строк является частой операцией в большинстве приложений, такие мелкие задержки быстро накапливаются.

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

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

8. Используйте Лямбда-Выражения Вместо Чистого Предиката
Рассмотрим следующий код:
private static bool Filter(int i) {
return i % 2 == 0;
}
И два варианта вызова:
list.Where(i => Filter(i));
и
list.Where(Filter);
Второй вариант приводит к выделению памяти в куче при каждом вызове, компилируясь в следующую конструкцию:
list.Where(new Func<int,bool>(Filter));
Лямбда-выражение использует оптимизацию компилятора и кэширует делегат в статическое поле.
*Примечание: я быстренько протестировал оба варианта в консольном приложении на предмет быстродействия и использования памяти и не нашёл никаких отличий ни в Framework, ни в Core. Поэтому есть подозрение, что оптимизация происходит в любом случае, а этот совет либо устарел, либо не проверялся автором.

Продолжение следует…

Источник:
https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День четыреста восемьдесят седьмой. #PerformanceTips
Лучшие Практики по Производительности в C#. Продолжение
9. Преобразование Перечислений в Строку
Вызов Enum.ToString в .Net является дорогостоящим, поскольку для преобразования используется рефлексия, а вызов виртуального метода структуры приводит к боксингу. Этого следует по возможности избегать. Часто перечисления могут быть заменены константами:
public enum Numbers {
One, Two, …
}
public static class Numbers {
public const string One = "One";
public const string Two = "Two";

}
В обоих случаях можно использовать Numbers.One, Numbers.Two,…

10. Сравнения Перечислений
Примечание: это поведение оптимизировано, начиная с .Net Core 2.1
При использовании перечислений в качестве флагов может возникнуть соблазн использовать метод Enum.HasFlag:
[Flags]
public enum Options {
Opt1 = 1, Opt2 = 2, Opt3 = 4
}

private Options option;
public bool IsOpt2() => option.HasFlag(Options.Opt2);
Этот код приводит к боксингу для преобразования Options.Opt2 в Enum и для виртуального вызова HasFlag на структуре, делает код необоснованно дорогим. Вместо этого можно использовать бинарные операторы:
public bool IsOpt2() => (option & Options.Opt2 == Options.Opt2);
Подробнее про битовые флаги

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

12. Избегайте Ненужного Боксинга при Использовании Структур с Интерфейсами
Рассмотрим следующий код:
public class IntValue : IValue {}

public void SendValue(IValue value) {…}
public void LogValue(IValue value) {…}

public void DoStuff() {
var value = new IntValue();
LogValue(value);
SendValue(value);
}
Соблазнительно сделать IntValue структурой, чтобы избежать выделения памяти в куче. Но поскольку AddValue и SendValue принимают интерфейс, а интерфейсы имеют ссылочную семантику, значение будет упаковываться при каждом вызове, сводя на нет преимущества «оптимизации». На самом деле, производительность может быть даже хуже, чем если бы IntValue был классом. Если же вы создаёте API, которому может быть передана структура, попробуйте использовать обобщённые методы:
public void SendValue<T>(T value) where T : IValue {…}
public void LogValue<T>(T value) where T : IValue {…}
Хотя на первый взгляд создание таких методов выглядит бесполезным, на самом деле это позволяет избежать боксинга, когда IntValue является структурой.

Продолжение следует…

Источник:
https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День четыреста восемьдесят восьмой. #PerformanceTips
Лучшие Практики по Производительности в C#. Окончание
13. Код Подписчиков CancellationToken Всегда Встраивается
Когда вы отменяете задачу через CancellationTokenSource, код всех подписчиков будет выполняться в текущем потоке. Это может привести к незапланированным паузам или даже неочевидным взаимным блокировкам:
var cts = new CancellationTokenSource ();
cts.Token.Register (() => Thread.Sleep (5000));
cts.Cancel (); // Это вызов заблокируется на 5 секунд
Вы не можете отказаться от этого поведения. Поэтому, при отмене через CancellationTokenSource, спросите себя, можете ли вы позволить текущему потоку исполнять другой код. Если нет, оберните вызов Cancel в Task.Run, чтобы выполнить его в пуле потоков. Подробнее о CancellationToken.

14. Код Продолжений TaskCompletionSource Часто Встраивается
Код продолжений TaskCompletionSource также часто встраивается. Это хорошая оптимизация, но она может быть причиной неочевидных ошибок. Их можно избежать, передав параметру TaskCompletionSource параметр TaskCreationOptions.RunContinuationsAsynchronously.
Если у вас нет веских причин этого не делать, всегда используйте этот параметр при создании TaskCompletionSource.
Внимание: код также скомпилируется, если вы используете TaskContinuationOptions.RunContinuationsAsynchronously. Но этот параметр будет проигнорирован, и продолжения будут оставаться встроенными. Это удивительно распространенная ошибка, потому что TaskContinuationOptions предшествует TaskCreationOptions при автозаполнении.

15. 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)

Источник: https://medium.com/@kevingosse/performance-best-practices-in-c-b85a47bdd93a
День восемьсот сороковой. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно
Разработка программного обеспечения - это поиск компромиссов:
- нормализация против денормализации в реляционных базах данных,
- скорость разработки против качественного кода
и т.п.

Высокопроизводительный код C# обычно требует компромиссов. Разработчики могут пожертвовать удобством сопровождения или безопасностью кода, чтобы код работал быстрее. Но это применимо только к сценариям, в которых уже применяются все шаблоны производительности и лучшие практики, а производительность всё равно требует дальнейшего улучшения.
Существует множество подходов, которые могут помочь разработчикам значительно улучшить производительность приложений, ничем не жертвуя.
Начнём с очевидного:

1. Указывайте ёмкость коллекции
Рассмотрим два почти идентичных метода:
public void NonFixedCapacityTest()
{
var items = new List<decimal>();
for (int i = 0; i < 1000000; i++)
items.Add(i);
}

public void FixedCapacityTest()
{
const int capacity = 1000000;
var items = new List<decimal>(capacity);
for (int i = 0; i < capacity; i++)
items.Add(i);
}

Оба метода выполняют одну и ту же задачу - заполнение коллекции целыми числами с помощью цикла foreach. Единственное отличие состоит в том, что в методе FixedCapacityTest конструктор коллекции инициализируется некоторым числом. Этот простой трюк заставляет метод FixedCapacityTest работать в два раза быстрее, чем NonFixedCapacityTest.

|               Method |      Mean |
|--------------------- |----------:|
| NonFixedCapacityTest | 22.708 ms |
| FixedCapacityTest | 8.418 ms |
Производительность в два-три раза выше, потому что List<T> реализован таким образом, что хранит элементы в массиве, который представляет собой структуру данных фиксированного размера. Когда разработчик создает экземпляр List<T> без указания его ёмкости, выделяется массив ёмкости по умолчанию. Когда массив заполнен, выделяется новый массив большего размера, а значения из старого массива копируются в новый.
Предварительное указание ёмкости устраняет накладные расходы на выделение, копирование и сборку мусора использованных массивов. Разработчики должны всегда указывать ёмкость коллекции, если они заранее знают, сколько элементов будет в неё добавлено.
Параметр ёмкости работает не только с коллекцией List, но и с другими, такими как Dictionary<TKey, TValue>, HashSet<T> и т.п.

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок первый. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно

2. Используйте структуры вместо классов в некоторых случаях
Разработчикам часто может потребоваться выделить массив или список для хранения десятков тысяч объектов в памяти. Эту задачу можно решить с помощью класса или структуры.
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
public struct PointStruct
{
public int X { get; set; }
public int Y { get; set; }
}

public void ListOfObjectsTest()
{
const int length = 1000000;
var items = new List<PointClass>(length);
for (int i = 0; i < length; i++)
items.Add(new PointClass() { X = i, Y = i });
}

public void ListOfStructsTest()
{
const int length = 1000000;
var items = new List<PointStruct>(length);
for (int i = 0; i < length; i++)
items.Add(new PointStruct() { X = i, Y = i});
}

Как видите, единственная разница между ListOfObjectTest и ListOfStructsTest заключается в том, что первый создаёт экземпляры класса, а второй - экземпляры структур. Код PointClass идентичен коду PointStruct.

|            Method |      Mean |
|------------------ |----------:|
| ListOfObjectsTest | 67.724 ms |
| ListOfStructsTest | 5.136 ms |
Код, использующий структуры, работает в 10-15 раз быстрее, чем код, использующий классы. Такая большая разница во времени, объясняется тем, что в случае классов CLR должна выделить один миллион объектов в управляемой куче и сохранить ссылки на них в коллекции List<T>. В случае структур единственным объектом, размещённым в куче, будет экземпляр коллекции List<T>. Миллион структур будет встроен в этот единственный экземпляр коллекции.

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок второй. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно
3. Распараллеливание циклов
Часто бывает необходимо перебрать коллекцию с помощью цикла foreach и выполнить некоторую логику для каждого элемента.
public void ForeachTest()
{
var items = Enumerable.Range(0, 100).ToList();
foreach (var item in items)
{
//Симулируем длинную операцию
Thread.Sleep(1);
}
}

В этом примере итерации выполняются одна за другой в одном и том же потоке, поэтому общее время выполнения будет линейно расти с размером коллекции.

Производительность можно повысить, начав использовать параллельную версию цикла foreach, которую платформа предоставляет разработчикам.
public void ParallelForeachTest()
{
var items = Enumerable.Range(0, 100).ToList();

Parallel.ForEach(items, (item) =>
{
//Симулируем длинную операцию
Thread.Sleep(1);
});
}

Parallel.Foreach можно использовать на любой коллекции, которая реализует IEnumerable<T> как обычный цикл foreach. Реализация Parallel.Foreach выполнит всю работу по распараллеливанию за вас:
- разобьёт коллекцию на части,
- назначит и выполнит эти части в отдельных потоках.

|              Method |       Mean |
|-------------------- |-----------:|
| ForeachTest | 1,543.9 ms |
| ParallelForeachTest | 199.9 ms |
Надо отметить, что если коллекции небольшие и время выполнения одной итерации быстрое, переход с foreach на Parallel.Foreach может даже ухудшить производительность, особенно если используется синхронизация потоков из-за доступа к общим ресурсам.

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

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок третий. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно

4. Избегайте неявного линейного поиска
Линейный поиск - это один из простейших алгоритмов поиска, который перебирает все элементы коллекции один за другим, пока не будет найден указанный элемент.

Хотя разработчики обычно не реализуют алгоритм поиска явно, линейный поиск всё же часто вызывает снижение производительности.
public void LinearSearchTest()
{
var ids = Enumerable.Range(0, 10000000);
int idToFind = 9193513;
var exists = ids.Any(u => u == idToFind);
}

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

Разработчикам не обязательно знать, как реализован каждый из методов LINQ. Важно знать основы: в .NET коллекция List<T> базируется на массиве. А когда дело доходит до поиска значения в несортированном массиве, его сложность составляет O(n). Независимо от того, какой метод LINQ используется для поиска значения в массиве (Any, Contains или Where), сложность остается прежней.

Решением этой конкретной проблемы было бы использование структуры данных, подходящей для конкретной задачи. В нашем случае у нас есть идентификаторы, которые всегда уникальны. Это позволяет нам преобразовать коллекцию в HashSet<T>.
public void HashSetTest()
{
var ids = Enumerable.Range(0, 10000000).ToHashSet();
int idToFind = 9193513;
var exists = ids.Contains(idToFind);
}

| Method | Mean |
|----------------- |----------:|
| LinearSearchTest | 63.74 ms |
| HashSetTest | 334.34 ms |
Погодите-ка. HashSetTest оказался гораздо медленнее, чем LinearSearchTest. Это связано с тем, что время тратится и на создание коллекции HashSet<T>, что является трудоёмкой операцией для больших наборов.

Разработчики выиграют от преобразования в HashSet только в том случае, если они планируют часто вызывать на коллекции метод Contains. Если вынести создание коллекции из тестов производительности и измерить только время нахождения элемента, результаты будут кардинально отличаться. Поиск значения в HashSet<T> почти не занимает времени по сравнению с поиском значения в коллекции List<T>.

Продолжение следует…

Источник:
https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День восемьсот сорок четвёртый. #PerformanceTips
5 Способов Повысить Производительность Кода C# Бесплатно

5. Материализуйте запросы LINQ один раз
При написании запросов LINQ с использованием интерфейсов IEnumerable или IQueryable разработчики могут материализовать запрос (вызвать ToList, ToArray или аналогичные методы) или не делать этого, что позволяет лениво работать с коллекциями. Но иногда возникает необходимость перебрать одну и ту же коллекцию несколько раз. Если запрос не был материализован, повторный перебор коллекции повлияет на производительность.
public void NotMaterializedQueryTest()
{
var elements = Enumerable.Range(0, 50000000);
var filtered =
elements.Where(e => e % 100000 == 0);

foreach (var e in filtered)
{

}

foreach (var e in filtered)
{

}

foreach (var e in filtered)
{

}
}

В этом примере запрос Where не материализуется. Вызов метода Where просто возвращает объект, реализующий интерфейс IEnumerable. Методы GetEnumerator и MoveNext будут вызываться только при итерации по коллекции в цикле foreach.

Вот пример с материализованным запросом:
public void MaterializedQueryTest()
{
var elements = Enumerable.Range(0, 50000000);
var filtered =
elements.Where(e => e % 100000 == 0).ToList();

//остальной код такой же
}

Второй метод из-за материализации запроса с помощью ToList будет работать в 3 раза быстрее первого.
|                   Method |       Mean |
|------------------------- |-----------:|
| NotMaterializedQueryTest | 1,299.6 ms |
| MaterializedQueryTest | 495.5 ms |

Источник: https://levelup.gitconnected.com/5-ways-to-improve-the-performance-of-c-code-for-free-c89188eba5da
День 1593. #TipsAndTricks #PerformanceTips
Советы по Оптимизации Производительности
I. Управление Памятью и Сборка Мусора
Управление памятью и сборка мусора являются важными аспектами настройки производительности в C#, поэтому эти рекомендации помогут вам оптимизировать код для достижения максимальной эффективности.

1. Используйте интерфейс IDisposable
IDisposable помогает правильно управлять неуправляемыми ресурсами и обеспечивает эффективное использование памяти вашим приложением.
Плохо:
public class ResourceHolder
{
private Stream _stream;

public ResourceHolder(string filePath)
{
_stream = File.OpenRead(filePath);
}
//…
}

ResourceHolder не реализует интерфейс IDisposable, поэтому неуправляемые ресурсы могут не освобождаться, что приводит к утечке памяти.
Хорошо:
public class ResourceHolder : IDisposable
{
private Stream _stream;

public ResourceHolder(string filePath)
{
_stream = File.OpenRead(filePath);
}

public void Dispose()
{
_stream?.Dispose();
}
}

Реализуя интерфейс IDisposable, вы гарантируете, что неуправляемые ресурсы будут освобождены, когда они больше не нужны, что предотвратит утечку памяти и уменьшит нагрузку на сборщик мусора.

Хотя, с IDisposable не всегда бывает так просто. Подробнее об этом можно почитать в серии постов Мои любимые ошибки с IDisposable:
- 1
- 2
- 3

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

Слишком большое внимание микрооптимизациям может привести к сложному, загромождённому коду, который жертвует простотой поддержки ради незначительного повышения производительности. Преждевременные оптимизации могут усложнить поддержку кода, при этом не оказав существенного влияния на общую производительность или даже снизив производительность. Далеко не всё, что интуитивно кажется должно ускорить выполнение кода, в реальности его ускоряет.

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

См. подробнее про оптимизацию кода.

Источник: https://dev.to/bytehide/50-c-advanced-optimization-performance-tips-18l2
👍15